// LoopFollow // TabCustomizationModal.swift import SwiftUI // Represents either a TabItem or Settings in the drag-and-drop list enum TabCustomizationItem: Identifiable, Equatable, Hashable { case tabItem(TabItem) case settings var id: String { switch self { case let .tabItem(item): return item.rawValue case .settings: return "settings" } } var displayName: String { switch self { case let .tabItem(item): return item.displayName case .settings: return String(localized: "Menu") } } var icon: String { switch self { case let .tabItem(item): return item.icon case .settings: return "line.3.horizontal" } } } struct TabCustomizationModal: View { // All items including Settings - top 4 go to tab bar, rest to menu @State private var allItems: [TabCustomizationItem] private let originalItems: [TabCustomizationItem] init() { let sortedTabItems = TabItem.movableItems.sorted { item1, item2 in let pos1 = Storage.shared.position(for: item1).normalized let pos2 = Storage.shared.position(for: item2).normalized let isInTabBar1 = TabPosition.customizablePositions.contains(pos1) let isInTabBar2 = TabPosition.customizablePositions.contains(pos2) // Tab bar positions (1-4) come before menu if isInTabBar1, isInTabBar2 { return (pos1.tabIndex ?? 99) < (pos2.tabIndex ?? 99) } else if isInTabBar1 { return true // pos1 is in tab bar, pos2 is in menu } else if isInTabBar2 { return false // pos2 is in tab bar, pos1 is in menu } else { // Both in menu - maintain original order from movableItems let idx1 = TabItem.movableItems.firstIndex(of: item1) ?? 0 let idx2 = TabItem.movableItems.firstIndex(of: item2) ?? 0 return idx1 < idx2 } } // Convert to TabCustomizationItem array and add Settings at the start of menu items var items: [TabCustomizationItem] = sortedTabItems.map { .tabItem($0) } // Find where menu items start (after position 4) let menuStartIndex = items.firstIndex { item in if case let .tabItem(tabItem) = item { let pos = Storage.shared.position(for: tabItem).normalized return !TabPosition.customizablePositions.contains(pos) } return false } ?? items.count // Insert Settings at the start of menu items items.insert(.settings, at: menuStartIndex) _allItems = State(initialValue: items) originalItems = items } var body: some View { List { // Instructions Section { VStack(alignment: .leading, spacing: 8) { Text("Drag to reorder") .font(.subheadline) .fontWeight(.medium) Text("The top 4 items appear in the tab bar. The Menu can always open every feature.") .font(.caption) .foregroundColor(.secondary) } .padding(.vertical, 4) } // All items - Settings appears at position 5 as a divider Section { // Build display list: first 4 TabItems, then Settings, then remaining TabItems let tabItems = allItems.compactMap { item -> TabItem? in if case let .tabItem(tabItem) = item { return tabItem } return nil } // Display items in order: tab bar items, Settings, menu items ForEach(Array(allItems.enumerated()), id: \.element) { _, item in switch item { case let .tabItem(tabItem): // Determine if this TabItem is in tab bar or menu let tabItemIndex = tabItems.firstIndex(of: tabItem) ?? 0 let isInTabBar = tabItemIndex < 4 TabItemRow( item : tabItem, position: isInTabBar ? tabItemIndex + 1: nil, isInMenu: !isInTabBar ) case .settings: SettingsRow() .moveDisabled(true) } } .onMove { source, destination in // Check if Settings (at index 4) is being moved - prevent it if source.contains(4) { return } // Get all TabItems (excluding Settings) var tabItemsOnly: [TabItem] = allItems.compactMap { item -> TabItem? in if case let .tabItem(tabItem) = item { return tabItem } return nil } // Adjust source indices: if any are after Settings (index 4), subtract 1 var adjustedSource = source if source.contains(where: { $0 > 4 }) { adjustedSource = IndexSet(source.map { $0 > 4 ? $0 - 1 : $0 }) } // Adjust destination: if it's after Settings (position 5), subtract 1 let adjustedDestination = destination > 4 ? destination - 1 : destination // Move TabItems tabItemsOnly.move(fromOffsets: adjustedSource, toOffset: adjustedDestination) // Rebuild allItems with Settings at position 5 var newItems: [TabCustomizationItem] = [] for (index, tabItem) in tabItemsOnly.enumerated() { newItems.append(.tabItem(tabItem)) // Insert Settings after the 4th TabItem (at position 5) if index == 3 { newItems.append(.settings) } } // If there are fewer than 4 TabItems, add Settings after the last one if tabItemsOnly.count < 4 { newItems.append(.settings) } allItems = newItems persistTabOrder(tabItemsOnly) } } header: { Text("Tab Order") } } .listStyle(.insetGrouped) .environment(\.editMode, .constant(.active)) .navigationTitle("Tabs") .navigationBarTitleDisplayMode(.inline) .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) } // MARK: - Actions private func persistTabOrder(_ tabItems: [TabItem]) { for (index, tabItem) in tabItems.enumerated() { let position: TabPosition switch index { case 0: position = .position1 case 1: position = .position2 case 2: position = .position3 case 3: position = .position4 default: position = .menu } Storage.shared.setPosition(position, for: tabItem) } } } // MARK: - Row Views struct SettingsRow: View { var body: some View { HStack { Image(systemName: "line.3.horizontal") .frame(width: 28) .foregroundColor(.secondary) Text("Menu") .foregroundColor(.secondary) Spacer() Text("Tab 5") .font(.caption) .foregroundColor(.white) .padding(.horizontal, 8) .padding(.vertical, 4) .background(Color.gray) .cornerRadius(4) } .contentShape(Rectangle()) } } struct TabItemRow: View { let item: TabItem let position: Int? let isInMenu: Bool var body: some View { HStack { Image(systemName: item.icon) .frame(width: 28) .foregroundColor(isInMenu ? .secondary : .accentColor) VStack(alignment: .leading, spacing: 2) { Text(item.displayName) .foregroundColor(isInMenu ? .secondary : .primary) } Spacer() if let pos = position { Text("Tab \(pos)") .font(.caption) .foregroundColor(.white) .padding(.horizontal, 8) .padding(.vertical, 4) .background(Color.accentColor) .cornerRadius(4) } else { Text("In Menu") .font(.caption) .foregroundColor(.orange) .padding(.horizontal, 8) .padding(.vertical, 4) .background(Color.orange.opacity(0.15)) .cornerRadius(4) } } .contentShape(Rectangle()) } }