77
88import SwiftUI
99
10+ private struct TabDescriptor : Identifiable {
11+ let id : String
12+ let title : String
13+ let systemImage : String
14+ let builder : ( ) -> AnyView
15+ }
16+
17+ extension Notification . Name {
18+ static let switchToTab = Notification . Name ( " MainTabSwitchNotification " )
19+ }
20+
1021struct MainTabView : View {
1122 @AppStorage ( " customAccentColor " ) private var customAccentColorHex : String = " "
1223 @AppStorage ( " appTheme " ) private var appThemeRaw : String = AppTheme . system. rawValue
13- @State private var selection : Int = 0
24+ @AppStorage ( TabConfiguration . storageKey) private var enabledTabIdentifiers : String = TabConfiguration . defaultRawValue
25+ @AppStorage ( " primaryTabSelection " ) private var selection : String = TabConfiguration . defaultIDs. first ?? " home "
26+ @State private var switchObserver : Any ?
27+ @State private var detachedTab : TabDescriptor ?
28+ @State private var didSetInitialHome = false
1429
1530 // Update checking
1631 @State private var showForceUpdate : Bool = false
@@ -27,41 +42,109 @@ struct MainTabView: View {
2742 }
2843
2944 private var isAppStoreBuild : Bool {
30- themeExpansion? . isAppStoreBuild ?? true
45+ #if APPSTORE
46+ return true
47+ #else
48+ return false
49+ #endif
3150 }
3251
52+ private let configurableTabs : [ TabDescriptor ] = [
53+ TabDescriptor ( id: " home " , title: " Home " , systemImage: " house " ) { AnyView ( HomeView ( ) ) } ,
54+ TabDescriptor ( id: " console " , title: " Console " , systemImage: " terminal " ) { AnyView ( ConsoleLogsView ( ) ) } ,
55+ TabDescriptor ( id: " scripts " , title: " Scripts " , systemImage: " scroll " ) { AnyView ( ScriptListView ( ) ) } ,
56+ TabDescriptor ( id: " profiles " , title: " Profiles " , systemImage: " magazine.fill " ) { AnyView ( ProfileView ( ) ) } ,
57+ TabDescriptor ( id: " processes " , title: " Processes " , systemImage: " rectangle.stack.person.crop " ) { AnyView ( ProcessInspectorView ( ) ) } ,
58+ TabDescriptor ( id: " deviceinfo " , title: " Device Info " , systemImage: " iphone.and.arrow.forward " ) { AnyView ( DeviceInfoView ( ) ) } ,
59+ TabDescriptor ( id: " location " , title: " Location " , systemImage: " location " ) { AnyView ( LocationSimulationView ( ) ) }
60+ ]
61+
62+ private var availableTabs : [ TabDescriptor ] {
63+ configurableTabs. filter { descriptor in
64+ descriptor. id != " location " || !isAppStoreBuild
65+ }
66+ }
67+
68+ private let settingsTab = TabDescriptor ( id: " settings " , title: " Settings " , systemImage: " gearshape.fill " ) {
69+ AnyView ( SettingsView ( ) )
70+ }
71+
72+ private var selectedTabDescriptors : [ TabDescriptor ] {
73+ let ids = TabConfiguration . sanitize ( raw: enabledTabIdentifiers)
74+ return ids. compactMap { id in
75+ availableTabs. first ( where: { $0. id == id } )
76+ }
77+ }
78+
79+ private func ensureSelectionIsValid( ) {
80+ let ids = selectedTabDescriptors. map { $0. id }
81+ if ids. contains ( selection) || selection == settingsTab. id {
82+ return
83+ }
84+ selection = ids. first ?? settingsTab. id
85+ }
86+
3387 var body : some View {
3488 ZStack {
3589 // Allow global themed background to show
3690 Color . clear. ignoresSafeArea ( )
3791
3892 // Main tabs
3993 TabView ( selection: $selection) {
40- HomeView ( )
41- . tabItem { Label ( " Home " , systemImage: " house " ) }
42- . tag ( 0 )
43-
44- ConsoleLogsView ( )
45- . tabItem { Label ( " Console " , systemImage: " terminal " ) }
46- . tag ( 1 )
47-
48- ScriptListView ( )
49- . tabItem { Label ( " Scripts " , systemImage: " scroll " ) }
50- . tag ( 2 )
94+ ForEach ( selectedTabDescriptors) { descriptor in
95+ descriptor. builder ( )
96+ . tabItem { Label ( descriptor. title, systemImage: descriptor. systemImage) }
97+ . tag ( descriptor. id)
98+ }
5199
52- ProfileView ( )
53- . tabItem { Label ( " Profiles " , systemImage: " magazine.fill " ) }
54- . tag ( 3 )
55-
56- SettingsView ( )
57- . tabItem { Label ( " Settings " , systemImage: " gearshape.fill " ) }
58- . tag ( 4 )
100+ settingsTab. builder ( )
101+ . tabItem { Label ( settingsTab. title, systemImage: settingsTab. systemImage) }
102+ . tag ( settingsTab. id)
59103 }
60104 . id ( ( themeExpansion? . hasThemeExpansion == true ) ? customAccentColorHex : " default-accent " )
61105 . tint ( accentColor)
62106 . preferredColorScheme ( preferredScheme)
63107 . onAppear {
108+ enabledTabIdentifiers = TabConfiguration . serialize ( TabConfiguration . sanitize ( raw: enabledTabIdentifiers) )
109+ ensureSelectionIsValid ( )
110+ if !didSetInitialHome {
111+ if selectedTabDescriptors. contains ( where: { $0. id == " home " } ) {
112+ selection = " home "
113+ } else if let descriptor = availableTabs. first ( where: { $0. id == " home " } ) {
114+ detachedTab = descriptor
115+ }
116+ didSetInitialHome = true
117+ }
64118 checkForUpdate ( )
119+ switchObserver = NotificationCenter . default. addObserver ( forName: . switchToTab, object: nil , queue: . main) { note in
120+ guard let id = note. object as? String else { return }
121+ if selectedTabDescriptors. contains ( where: { $0. id == id } ) {
122+ selection = id
123+ } else if let descriptor = availableTabs. first ( where: { $0. id == id } ) {
124+ detachedTab = descriptor
125+ }
126+ }
127+ }
128+ . onDisappear {
129+ if let observer = switchObserver {
130+ NotificationCenter . default. removeObserver ( observer)
131+ switchObserver = nil
132+ }
133+ }
134+ . onChange ( of: enabledTabIdentifiers) { _ in
135+ ensureSelectionIsValid ( )
136+ }
137+ . sheet ( item: $detachedTab) { descriptor in
138+ NavigationStack {
139+ descriptor. builder ( )
140+ . toolbar {
141+ ToolbarItem ( placement: . cancellationAction) {
142+ Button ( " Close " ) {
143+ detachedTab = nil
144+ }
145+ }
146+ }
147+ }
65148 }
66149
67150 if showForceUpdate {
0 commit comments