Skip to content

Commit 5f38c2e

Browse files
JerrySmithJerrySmith
authored andcommitted
fix(macOS): address PR review regressions
1 parent 7e7f7aa commit 5f38c2e

7 files changed

Lines changed: 488 additions & 69 deletions

File tree

Sources/Lithe/LitheApp.swift

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -438,19 +438,26 @@ private struct SettingsWindow: View {
438438
@ObservedObject var model: AppModel
439439
@ObservedObject var settings: AppSettings
440440
@StateObject private var windowReference = SettingsWindowReference()
441+
@StateObject private var viewState: SettingsViewState
442+
443+
init(model: AppModel, settings: AppSettings) {
444+
self.model = model
445+
self.settings = settings
446+
_viewState = StateObject(wrappedValue: SettingsViewState(
447+
initialCategory: model.requestedSettingsCategory
448+
))
449+
}
441450

442451
var body: some View {
443-
SettingsView(
444-
settings: settings,
445-
initialCategory: model.requestedSettingsCategory,
446-
onDismiss: close
447-
)
448-
.environmentObject(model)
449-
// Rebuild the SwiftUI theme environment when the preference changes.
450-
// Without a new identity, a Window scene can retain the previous
451-
// Light/Dark color scheme even after the AppKit window has switched.
452-
.id(settings.themePreference.rawValue)
453-
.preferredColorScheme(settings.themePreference.preferredColorScheme)
452+
SettingsAppearanceContainer(themePreference: settings.themePreference) {
453+
SettingsView(
454+
settings: settings,
455+
viewState: viewState,
456+
initialCategory: model.requestedSettingsCategory,
457+
onDismiss: close
458+
)
459+
.environmentObject(model)
460+
}
454461
.background(
455462
SettingsWindowAccessor(
456463
reference: windowReference,
@@ -469,6 +476,23 @@ private struct SettingsWindow: View {
469476
}
470477
}
471478

479+
struct SettingsAppearanceContainer<Content: View>: View {
480+
let themePreference: AppThemePreference
481+
let content: Content
482+
483+
init(
484+
themePreference: AppThemePreference,
485+
@ViewBuilder content: () -> Content
486+
) {
487+
self.themePreference = themePreference
488+
self.content = content()
489+
}
490+
491+
var body: some View {
492+
content.preferredColorScheme(themePreference.preferredColorScheme)
493+
}
494+
}
495+
472496
@MainActor
473497
private final class SettingsWindowReference: ObservableObject {
474498
weak var window: NSWindow?

Sources/Lithe/Views/App/SettingsView.swift

Lines changed: 47 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,41 @@ import LitheCoreContracts
33
import LitheGitModule
44
import 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+
620
struct 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

Comments
 (0)