@@ -3,31 +3,41 @@ import LitheCoreContracts
33import LitheGitModule
44import LitheModuleAPI
55
6+ @MainActor
7+ final class SettingsViewState : ObservableObject {
8+ @Published var selection : SettingsCategory
9+ @Published var searchQuery = " "
10+ @Published var hiddenDirectoriesDraft = " "
11+ @Published var hiddenFilePatternsDraft = " "
12+ @Published var aiAPIKeyDraft = " "
13+ @Published var isFormatPickerPresented = false
14+
15+ init ( initialCategory: SettingsCategory ) {
16+ selection = initialCategory
17+ }
18+ }
19+
620struct SettingsView : View {
721 @Environment ( \. dismiss) private var dismiss
822 @Environment ( \. accessibilityReduceMotion) private var accessibilityReduceMotion
923 @EnvironmentObject private var model : AppModel
1024 @EnvironmentObject private var updateChecker : UpdateChecker
1125 @ObservedObject var settings : AppSettings
12- @State private var selection : SettingsCategory
13- @State private var searchQuery = " "
14- @State private var hiddenDirectoriesDraft = " "
15- @State private var hiddenFilePatternsDraft = " "
16- @State private var aiAPIKeyDraft = " "
17- @State private var isFormatPickerPresented = false
26+ @ObservedObject var viewState : SettingsViewState
1827 let initialCategory : SettingsCategory
1928 private let onDismiss : ( ( ) -> Void ) ?
2029 private static let footerActionLabelWidth : CGFloat = 52
2130
2231 init (
2332 settings: AppSettings ,
33+ viewState: SettingsViewState ,
2434 initialCategory: SettingsCategory = . general,
2535 onDismiss: ( ( ) -> Void ) ? = nil
2636 ) {
2737 self . settings = settings
38+ self . viewState = viewState
2839 self . initialCategory = initialCategory
2940 self . onDismiss = onDismiss
30- _selection = State ( initialValue: initialCategory)
3141 }
3242
3343 var body : some View {
@@ -54,13 +64,13 @@ struct SettingsView: View {
5464 . onChange ( of: settings. hiddenFilePatterns) { _ in syncVisibilityDrafts ( ) }
5565 . onChange ( of: settings. commitMessageAI. activeProviderID) { _ in syncAIProviderDraft ( ) }
5666 . onChange ( of: initialCategory) { category in
57- searchQuery = " "
58- selection = category
67+ viewState . searchQuery = " "
68+ viewState . selection = category
5969 }
60- . onChange ( of: searchQuery) { _ in
61- guard !filteredCategories. contains ( selection) ,
70+ . onChange ( of: viewState . searchQuery) { _ in
71+ guard !filteredCategories. contains ( viewState . selection) ,
6272 let firstMatch = filteredCategories. first else { return }
63- selection = firstMatch
73+ viewState . selection = firstMatch
6474 }
6575 . environment ( \. locale, settings. language. locale)
6676 }
@@ -100,13 +110,13 @@ struct SettingsView: View {
100110 }
101111
102112 private var settingsSearchField : some View {
103- LitheSettingsSearchField ( " Search settings " , text: $searchQuery)
113+ LitheSettingsSearchField ( " Search settings " , text: $viewState . searchQuery)
104114 }
105115
106116 private func categoryButton( _ category: SettingsCategory ) -> some View {
107- let isSelected = selection == category
117+ let isSelected = viewState . selection == category
108118 return Button {
109- selection = category
119+ viewState . selection = category
110120 } label: {
111121 HStack ( spacing: 10 ) {
112122 Image ( systemName: category. icon)
@@ -128,7 +138,7 @@ struct SettingsView: View {
128138 }
129139
130140 private var filteredCategories : [ SettingsCategory ] {
131- let query = searchQuery. trimmingCharacters ( in: . whitespacesAndNewlines)
141+ let query = viewState . searchQuery. trimmingCharacters ( in: . whitespacesAndNewlines)
132142 guard !query. isEmpty else { return SettingsCategory . allCases }
133143
134144 return SettingsCategory . allCases. filter { category in
@@ -179,10 +189,10 @@ struct SettingsView: View {
179189 }
180190 . foregroundStyle ( LitheTheme . secondaryText)
181191 . frame ( maxWidth: . infinity, maxHeight: . infinity)
182- } else if selection == . lsp {
192+ } else if viewState . selection == . lsp {
183193 LSPControlCenterView ( )
184194 . frame ( maxWidth: . infinity, maxHeight: . infinity)
185- } else if selection == . keymap {
195+ } else if viewState . selection == . keymap {
186196 KeyboardShortcutSettingsView (
187197 feature: model. keyboardShortcutFeature,
188198 language: settings. language
@@ -191,12 +201,12 @@ struct SettingsView: View {
191201 } else {
192202 ScrollView {
193203 VStack ( alignment: . leading, spacing: 8 ) {
194- Text ( LocalizedStringKey ( selection. rawValue) )
204+ Text ( LocalizedStringKey ( viewState . selection. rawValue) )
195205 . font ( . system( size: 22 , weight: . semibold) )
196206 . foregroundStyle ( LitheTheme . primaryText)
197207 . padding ( . bottom, 8 )
198208
199- switch selection {
209+ switch viewState . selection {
200210 case . general: generalSettings
201211 case . editor: editorSettings
202212 case . keymap: EmptyView ( )
@@ -315,7 +325,7 @@ struct SettingsView: View {
315325
316326 Text ( " Directories " )
317327 . font ( . system( size: 11.5 , weight: . medium) )
318- TextEditor ( text: $hiddenDirectoriesDraft)
328+ TextEditor ( text: $viewState . hiddenDirectoriesDraft)
319329 . font ( . system( size: 12 , design: . monospaced) )
320330 . frame ( height: 66 )
321331 . padding ( 5 )
@@ -327,7 +337,7 @@ struct SettingsView: View {
327337
328338 Text ( " File patterns " )
329339 . font ( . system( size: 11.5 , weight: . medium) )
330- TextEditor ( text: $hiddenFilePatternsDraft)
340+ TextEditor ( text: $viewState . hiddenFilePatternsDraft)
331341 . font ( . system( size: 12 , design: . monospaced) )
332342 . frame ( height: 52 )
333343 . padding ( 5 )
@@ -570,11 +580,11 @@ struct SettingsView: View {
570580 . disabled ( model. activeCommitMessageCredentialIsConfigurationManaged)
571581
572582 HStack ( spacing: 8 ) {
573- SecureField ( " API key or token " , text: $aiAPIKeyDraft)
583+ SecureField ( " API key or token " , text: $viewState . aiAPIKeyDraft)
574584 . litheSettingsTextField ( )
575585 . disabled ( model. activeCommitMessageCredentialIsConfigurationManaged)
576586 Button ( " Save Key " ) {
577- model. saveActiveCommitMessageAPIKey ( aiAPIKeyDraft)
587+ model. saveActiveCommitMessageAPIKey ( viewState . aiAPIKeyDraft)
578588 }
579589 . buttonStyle ( LitheSecondaryButtonStyle ( ) )
580590 . disabled ( model. activeCommitMessageCredentialIsConfigurationManaged)
@@ -815,24 +825,24 @@ struct SettingsView: View {
815825 Image ( systemName: " chevron.down " )
816826 . font ( . system( size: 10 , weight: . semibold) )
817827 . foregroundStyle ( LitheTheme . secondaryText)
818- . rotationEffect ( . degrees( isFormatPickerPresented ? 180 : 0 ) )
819- . animation ( formatPickerAnimation, value: isFormatPickerPresented)
828+ . rotationEffect ( . degrees( viewState . isFormatPickerPresented ? 180 : 0 ) )
829+ . animation ( formatPickerAnimation, value: viewState . isFormatPickerPresented)
820830 }
821831 . padding ( . horizontal, 10 )
822832 . frame ( maxWidth: . infinity, minHeight: 48 , alignment: . leading)
823- . background ( isFormatPickerPresented ? LitheTheme . inputBackground. opacity ( 0.9 ) : LitheTheme . inputBackground)
833+ . background ( viewState . isFormatPickerPresented ? LitheTheme . inputBackground. opacity ( 0.9 ) : LitheTheme . inputBackground)
824834 . overlay {
825835 RoundedRectangle ( cornerRadius: LitheTheme . Metrics. controlCornerRadius)
826836 . stroke (
827- isFormatPickerPresented ? LitheTheme . inputFocusBorder : LitheTheme . inputBorder,
837+ viewState . isFormatPickerPresented ? LitheTheme . inputFocusBorder : LitheTheme . inputBorder,
828838 lineWidth: 1
829839 )
830840 }
831841 . clipShape ( RoundedRectangle ( cornerRadius: LitheTheme . Metrics. controlCornerRadius) )
832842 }
833843 . buttonStyle ( . plain)
834844 . lithePointer ( )
835- . popover ( isPresented: $isFormatPickerPresented, arrowEdge: . bottom) {
845+ . popover ( isPresented: $viewState . isFormatPickerPresented, arrowEdge: . bottom) {
836846 formatPickerPopover
837847 }
838848
@@ -857,7 +867,7 @@ struct SettingsView: View {
857867 Spacer ( minLength: 8 )
858868
859869 Button {
860- isFormatPickerPresented = false
870+ viewState . isFormatPickerPresented = false
861871 } label: {
862872 Image ( systemName: " xmark " )
863873 . font ( . system( size: 10 , weight: . semibold) )
@@ -984,14 +994,14 @@ struct SettingsView: View {
984994
985995 private func toggleFormatPicker( ) {
986996 withAnimation ( formatPickerAnimation) {
987- isFormatPickerPresented. toggle ( )
997+ viewState . isFormatPickerPresented. toggle ( )
988998 }
989999 }
9901000
9911001 private func selectFormat( _ format: CommitMessageFormat ) {
9921002 withAnimation ( formatPickerAnimation) {
9931003 settings. commitMessageAI. format = format
994- isFormatPickerPresented = false
1004+ viewState . isFormatPickerPresented = false
9951005 }
9961006 }
9971007
@@ -1124,7 +1134,7 @@ struct SettingsView: View {
11241134 }
11251135
11261136 private func syncAIProviderDraft( ) {
1127- aiAPIKeyDraft = model. activeCommitMessageAPIKey
1137+ viewState . aiAPIKeyDraft = model. activeCommitMessageAPIKey
11281138 }
11291139
11301140 private func providerTitle( _ id: UUID ) -> String {
@@ -1206,13 +1216,13 @@ struct SettingsView: View {
12061216 }
12071217
12081218 private func syncVisibilityDrafts( ) {
1209- hiddenDirectoriesDraft = settings. hiddenDirectoryNames. joined ( separator: " \n " )
1210- hiddenFilePatternsDraft = settings. hiddenFilePatterns. joined ( separator: " \n " )
1219+ viewState . hiddenDirectoriesDraft = settings. hiddenDirectoryNames. joined ( separator: " \n " )
1220+ viewState . hiddenFilePatternsDraft = settings. hiddenFilePatterns. joined ( separator: " \n " )
12111221 }
12121222
12131223 private func applyVisibilityDrafts( ) {
1214- settings. hiddenDirectoryNames = entries ( from: hiddenDirectoriesDraft)
1215- settings. hiddenFilePatterns = entries ( from: hiddenFilePatternsDraft)
1224+ settings. hiddenDirectoryNames = entries ( from: viewState . hiddenDirectoriesDraft)
1225+ settings. hiddenFilePatterns = entries ( from: viewState . hiddenFilePatternsDraft)
12161226 }
12171227
12181228 private func entries( from text: String ) -> [ String ] {
0 commit comments