| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- // 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 "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())
- }
- }
|