Skip to content

Commit 7048a28

Browse files
feat(settings): Add Theme export; Theme details flicker fix when duplicating and then canceling (#1920)
* Fixed theme details flicker when duplicating and then canceling. * Fixed bug when renaming a theme where the theme disappeared briefly and then reappeared shortly after with the new name. * Added the ability to export individual custom themes and export all custom themes at once. * Added confermation alert to the delete theme action. * Added delete alert to theme details sheet.
1 parent ffb0f8b commit 7048a28

File tree

5 files changed

+153
-57
lines changed

5 files changed

+153
-57
lines changed

CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel+CRUD.swift

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,12 @@ extension ThemeModel {
208208
self.save(self.themes[index])
209209
}
210210

211+
self.previousTheme = self.selectedTheme
212+
211213
activateTheme(self.themes[index])
212214

213215
self.detailsTheme = self.themes[index]
216+
self.detailsIsPresented = true
214217
}
215218
} catch {
216219
print("Error adding theme: \(error.localizedDescription)")
@@ -238,16 +241,11 @@ extension ThemeModel {
238241
iterator += 1
239242
}
240243

244+
let isActive = self.getThemeActive(theme)
245+
241246
try filemanager.moveItem(at: oldURL, to: finalURL)
242247

243248
try self.loadThemes()
244-
245-
if let index = themes.firstIndex(where: { $0.fileURL == finalURL }) {
246-
themes[index].displayName = finalName
247-
themes[index].fileURL = finalURL
248-
themes[index].name = finalName.lowercased().replacingOccurrences(of: " ", with: "-")
249-
}
250-
251249
} catch {
252250
print("Error renaming theme: \(error.localizedDescription)")
253251
}

CodeEdit/Features/Settings/Pages/ThemeSettings/Models/ThemeModel.swift

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import SwiftUI
9+
import UniformTypeIdentifiers
910

1011
/// The Theme View Model. Accessible via the singleton "``ThemeModel/shared``".
1112
///
@@ -72,7 +73,7 @@ final class ThemeModel: ObservableObject {
7273
}
7374
}
7475

75-
@Published var presentingDetails: Bool = false
76+
@Published var detailsIsPresented: Bool = false
7677

7778
@Published var isAdding: Bool = false
7879

@@ -87,10 +88,11 @@ final class ThemeModel: ObservableObject {
8788
DispatchQueue.main.async {
8889
Settings[\.theme].selectedTheme = self.selectedTheme?.name
8990
}
90-
updateAppearanceTheme()
9191
}
9292
}
9393

94+
@Published var previousTheme: Theme?
95+
9496
/// Only themes where ``Theme/appearance`` == ``Theme/ThemeType/dark``
9597
var darkThemes: [Theme] {
9698
themes.filter { $0.appearance == .dark }
@@ -127,48 +129,80 @@ final class ThemeModel: ObservableObject {
127129
}
128130

129131
/// Initialize to the app's current appearance.
130-
@Published var selectedAppearance: ThemeSettingsAppearances = {
132+
var selectedAppearance: ThemeSettingsAppearances {
131133
NSApp.effectiveAppearance.name == .darkAqua ? .dark : .light
132-
}()
134+
}
133135

134136
enum ThemeSettingsAppearances: String, CaseIterable {
135137
case light = "Light Appearance"
136138
case dark = "Dark Appearance"
137139
}
138140

139141
func getThemeActive(_ theme: Theme) -> Bool {
140-
if settings.matchAppearance {
141-
return selectedAppearance == .dark
142-
? selectedDarkTheme == theme
143-
: selectedAppearance == .light
144-
? selectedLightTheme == theme
145-
: selectedTheme == theme
146-
}
147142
return selectedTheme == theme
148143
}
149144

150145
/// Activates the current theme, setting ``selectedTheme`` and ``selectedLightTheme``/``selectedDarkTheme`` as
151146
/// necessary.
152147
/// - Parameter theme: The theme to activate.
153148
func activateTheme(_ theme: Theme) {
154-
if settings.matchAppearance {
155-
if selectedAppearance == .dark {
156-
selectedDarkTheme = theme
157-
} else if selectedAppearance == .light {
158-
selectedLightTheme = theme
159-
}
160-
if (selectedAppearance == .dark && colorScheme == .dark)
161-
|| (selectedAppearance == .light && colorScheme == .light) {
162-
selectedTheme = theme
163-
}
164-
} else {
165-
selectedTheme = theme
166-
if colorScheme == .light {
167-
selectedLightTheme = theme
168-
}
169-
if colorScheme == .dark {
170-
selectedDarkTheme = theme
149+
selectedTheme = theme
150+
if colorScheme == .light {
151+
selectedLightTheme = theme
152+
}
153+
if colorScheme == .dark {
154+
selectedDarkTheme = theme
155+
}
156+
}
157+
158+
func exportTheme(_ theme: Theme) {
159+
guard let themeFileURL = theme.fileURL else {
160+
print("Theme file URL not found.")
161+
return
162+
}
163+
164+
let savePanel = NSSavePanel()
165+
savePanel.allowedContentTypes = [UTType(filenameExtension: "cetheme")!]
166+
savePanel.nameFieldStringValue = theme.displayName
167+
savePanel.prompt = "Export"
168+
savePanel.canCreateDirectories = true
169+
170+
savePanel.begin { response in
171+
if response == .OK, let destinationURL = savePanel.url {
172+
do {
173+
try FileManager.default.copyItem(at: themeFileURL, to: destinationURL)
174+
print("Theme exported successfully to \(destinationURL.path)")
175+
} catch {
176+
print("Failed to export theme: \(error.localizedDescription)")
177+
}
171178
}
172179
}
173180
}
181+
182+
func exportAllCustomThemes() {
183+
let openPanel = NSOpenPanel()
184+
openPanel.prompt = "Export"
185+
openPanel.canChooseFiles = false
186+
openPanel.canChooseDirectories = true
187+
openPanel.allowsMultipleSelection = false
188+
189+
openPanel.begin { result in
190+
if result == .OK, let exportDirectory = openPanel.url {
191+
let customThemes = self.themes.filter { !$0.isBundled }
192+
193+
for theme in customThemes {
194+
guard let sourceURL = theme.fileURL else { continue }
195+
196+
let destinationURL = exportDirectory.appendingPathComponent("\(theme.displayName).cetheme")
197+
198+
do {
199+
try FileManager.default.copyItem(at: sourceURL, to: destinationURL)
200+
print("Exported \(theme.displayName) to \(destinationURL.path)")
201+
} catch {
202+
print("Failed to export \(theme.displayName): \(error.localizedDescription)")
203+
}
204+
}
205+
}
206+
}
207+
}
174208
}

CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingThemeRow.swift

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ struct ThemeSettingsThemeRow: View {
1313

1414
@ObservedObject private var themeModel: ThemeModel = .shared
1515

16-
@State private var presentingDetails: Bool = false
17-
1816
@State private var isHovering = false
1917

18+
@State private var deleteConfirmationIsPresented = false
19+
2020
var body: some View {
2121
HStack {
2222
Image(systemName: "checkmark")
@@ -42,15 +42,20 @@ struct ThemeSettingsThemeRow: View {
4242
Menu {
4343
Button("Details...") {
4444
themeModel.detailsTheme = theme
45+
themeModel.detailsIsPresented = true
4546
}
46-
Button("Duplicate") {
47+
Button("Duplicate...") {
4748
if let fileURL = theme.fileURL {
4849
themeModel.duplicate(fileURL)
4950
}
5051
}
52+
Button("Export...") {
53+
themeModel.exportTheme(theme)
54+
}
55+
.disabled(theme.isBundled)
5156
Divider()
52-
Button("Delete") {
53-
themeModel.delete(theme)
57+
Button("Delete...") {
58+
deleteConfirmationIsPresented = true
5459
}
5560
.disabled(theme.isBundled)
5661
} label: {
@@ -63,5 +68,18 @@ struct ThemeSettingsThemeRow: View {
6368
.onHover { hovering in
6469
isHovering = hovering
6570
}
71+
.alert(
72+
Text("Are you sure you want to delete the theme “\(theme.displayName)”?"),
73+
isPresented: $deleteConfirmationIsPresented
74+
) {
75+
Button("Delete Theme") {
76+
themeModel.delete(theme)
77+
}
78+
Button("Cancel") {
79+
deleteConfirmationIsPresented = false
80+
}
81+
} message: {
82+
Text("This action cannot be undone.")
83+
}
6684
}
6785
}

CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsThemeDetails.swift

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ struct ThemeSettingsThemeDetails: View {
2020

2121
@StateObject private var themeModel: ThemeModel = .shared
2222

23+
@State private var duplicatingTheme: Theme?
24+
25+
@State private var deleteConfirmationIsPresented = false
26+
27+
var isActive: Bool {
28+
themeModel.getThemeActive(theme)
29+
}
30+
2331
init(theme: Binding<Theme>) {
2432
_theme = theme
2533
originalTheme = theme.wrappedValue
@@ -168,26 +176,27 @@ struct ThemeSettingsThemeDetails: View {
168176
.accessibilityLabel("Warning: Duplicate this theme to make changes.")
169177
} else if !themeModel.isAdding {
170178
Button(role: .destructive) {
171-
themeModel.delete(theme)
172-
dismiss()
179+
deleteConfirmationIsPresented = true
173180
} label: {
174-
Text("Delete")
181+
Text("Delete...")
175182
.foregroundStyle(.red)
176183
.frame(minWidth: 56)
177184
}
178185
Button {
179186
if let fileURL = theme.fileURL {
187+
duplicatingTheme = theme
180188
themeModel.duplicate(fileURL)
181189
}
182190
} label: {
183-
Text("Duplicate")
191+
Text("Duplicate...")
184192
.frame(minWidth: 56)
185193
}
186194
}
187195
Spacer()
188196
if !themeModel.isAdding && theme.isBundled {
189197
Button {
190198
if let fileURL = theme.fileURL {
199+
duplicatingTheme = theme
191200
themeModel.duplicate(fileURL)
192201
}
193202
} label: {
@@ -197,12 +206,28 @@ struct ThemeSettingsThemeDetails: View {
197206
} else {
198207
Button {
199208
if themeModel.isAdding {
200-
themeModel.delete(theme)
209+
if let previousTheme = themeModel.previousTheme {
210+
themeModel.activateTheme(previousTheme)
211+
}
212+
if let duplicatingWithinDetails = duplicatingTheme {
213+
let duplicateTheme = theme
214+
themeModel.detailsTheme = duplicatingWithinDetails
215+
themeModel.delete(duplicateTheme)
216+
} else {
217+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
218+
themeModel.delete(theme)
219+
}
220+
}
201221
} else {
202222
themeModel.cancelDetails(theme)
203223
}
204224

205-
dismiss()
225+
if duplicatingTheme == nil {
226+
dismiss()
227+
} else {
228+
duplicatingTheme = nil
229+
themeModel.isAdding = false
230+
}
206231
} label: {
207232
Text("Cancel")
208233
.frame(minWidth: 56)
@@ -223,5 +248,19 @@ struct ThemeSettingsThemeDetails: View {
223248
.padding()
224249
}
225250
.constrainHeightToWindow()
251+
.alert(
252+
Text("Are you sure you want to delete the theme “\(theme.displayName)”?"),
253+
isPresented: $deleteConfirmationIsPresented
254+
) {
255+
Button("Delete Theme") {
256+
themeModel.delete(theme)
257+
dismiss()
258+
}
259+
Button("Cancel") {
260+
deleteConfirmationIsPresented = false
261+
}
262+
} message: {
263+
Text("This action cannot be undone.")
264+
}
226265
}
227266
}

CodeEdit/Features/Settings/Pages/ThemeSettings/ThemeSettingsView.swift

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ struct ThemeSettingsView: View {
4949
Text("Import Theme...")
5050
}
5151
Button {
52-
// TODO: #1874
52+
themeModel.exportAllCustomThemes()
5353
} label: {
5454
Text("Export All Custom Themes...")
55-
}.disabled(true)
55+
}
5656
}
5757
})
5858
.padding(.horizontal, 5)
@@ -90,30 +90,37 @@ struct ThemeSettingsView: View {
9090
}
9191
.padding(.top, 10)
9292
}
93-
.sheet(item: $themeModel.detailsTheme) {
94-
themeModel.isAdding = false
95-
} content: { theme in
96-
if let index = themeModel.themes.firstIndex(where: {
93+
.sheet(isPresented: $themeModel.detailsIsPresented, onDismiss: {
94+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
95+
themeModel.isAdding = false
96+
}
97+
}, content: {
98+
if let theme = themeModel.detailsTheme, let index = themeModel.themes.firstIndex(where: {
9799
$0.fileURL?.absoluteString == theme.fileURL?.absoluteString
98100
}) {
99101
ThemeSettingsThemeDetails(theme: Binding(
100102
get: { themeModel.themes[index] },
101103
set: { newValue in
102-
themeModel.themes[index] = newValue
103-
themeModel.save(newValue)
104-
if settings.selectedTheme == theme.name {
105-
themeModel.activateTheme(newValue)
104+
if themeModel.detailsIsPresented {
105+
themeModel.themes[index] = newValue
106+
themeModel.save(newValue)
107+
if settings.selectedTheme == theme.name {
108+
themeModel.activateTheme(newValue)
109+
}
106110
}
107111
}
108112
))
109113
}
110-
}
114+
})
111115
.onAppear {
112116
updateFilteredThemes()
113117
}
114118
.onChange(of: themeSearchQuery) { _ in
115119
updateFilteredThemes()
116120
}
121+
.onChange(of: themeModel.themes) { _ in
122+
updateFilteredThemes()
123+
}
117124
.onChange(of: colorScheme) { newColorScheme in
118125
updateFilteredThemes(overrideColorScheme: newColorScheme)
119126
}

0 commit comments

Comments
 (0)