Skip to content

Commit

Permalink
feat(settings): Add Theme export; Theme details flicker fix when dupl…
Browse files Browse the repository at this point in the history
…icating 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.
  • Loading branch information
austincondiff authored Nov 2, 2024
1 parent ffb0f8b commit 7048a28
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,12 @@ extension ThemeModel {
self.save(self.themes[index])
}

self.previousTheme = self.selectedTheme

activateTheme(self.themes[index])

self.detailsTheme = self.themes[index]
self.detailsIsPresented = true
}
} catch {
print("Error adding theme: \(error.localizedDescription)")
Expand Down Expand Up @@ -238,16 +241,11 @@ extension ThemeModel {
iterator += 1
}

let isActive = self.getThemeActive(theme)

try filemanager.moveItem(at: oldURL, to: finalURL)

try self.loadThemes()

if let index = themes.firstIndex(where: { $0.fileURL == finalURL }) {
themes[index].displayName = finalName
themes[index].fileURL = finalURL
themes[index].name = finalName.lowercased().replacingOccurrences(of: " ", with: "-")
}

} catch {
print("Error renaming theme: \(error.localizedDescription)")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import SwiftUI
import UniformTypeIdentifiers

/// The Theme View Model. Accessible via the singleton "``ThemeModel/shared``".
///
Expand Down Expand Up @@ -72,7 +73,7 @@ final class ThemeModel: ObservableObject {
}
}

@Published var presentingDetails: Bool = false
@Published var detailsIsPresented: Bool = false

@Published var isAdding: Bool = false

Expand All @@ -87,10 +88,11 @@ final class ThemeModel: ObservableObject {
DispatchQueue.main.async {
Settings[\.theme].selectedTheme = self.selectedTheme?.name
}
updateAppearanceTheme()
}
}

@Published var previousTheme: Theme?

/// Only themes where ``Theme/appearance`` == ``Theme/ThemeType/dark``
var darkThemes: [Theme] {
themes.filter { $0.appearance == .dark }
Expand Down Expand Up @@ -127,48 +129,80 @@ final class ThemeModel: ObservableObject {
}

/// Initialize to the app's current appearance.
@Published var selectedAppearance: ThemeSettingsAppearances = {
var selectedAppearance: ThemeSettingsAppearances {
NSApp.effectiveAppearance.name == .darkAqua ? .dark : .light
}()
}

enum ThemeSettingsAppearances: String, CaseIterable {
case light = "Light Appearance"
case dark = "Dark Appearance"
}

func getThemeActive(_ theme: Theme) -> Bool {
if settings.matchAppearance {
return selectedAppearance == .dark
? selectedDarkTheme == theme
: selectedAppearance == .light
? selectedLightTheme == theme
: selectedTheme == theme
}
return selectedTheme == theme
}

/// Activates the current theme, setting ``selectedTheme`` and ``selectedLightTheme``/``selectedDarkTheme`` as
/// necessary.
/// - Parameter theme: The theme to activate.
func activateTheme(_ theme: Theme) {
if settings.matchAppearance {
if selectedAppearance == .dark {
selectedDarkTheme = theme
} else if selectedAppearance == .light {
selectedLightTheme = theme
}
if (selectedAppearance == .dark && colorScheme == .dark)
|| (selectedAppearance == .light && colorScheme == .light) {
selectedTheme = theme
}
} else {
selectedTheme = theme
if colorScheme == .light {
selectedLightTheme = theme
}
if colorScheme == .dark {
selectedDarkTheme = theme
selectedTheme = theme
if colorScheme == .light {
selectedLightTheme = theme
}
if colorScheme == .dark {
selectedDarkTheme = theme
}
}

func exportTheme(_ theme: Theme) {
guard let themeFileURL = theme.fileURL else {
print("Theme file URL not found.")
return
}

let savePanel = NSSavePanel()
savePanel.allowedContentTypes = [UTType(filenameExtension: "cetheme")!]
savePanel.nameFieldStringValue = theme.displayName
savePanel.prompt = "Export"
savePanel.canCreateDirectories = true

savePanel.begin { response in
if response == .OK, let destinationURL = savePanel.url {
do {
try FileManager.default.copyItem(at: themeFileURL, to: destinationURL)
print("Theme exported successfully to \(destinationURL.path)")
} catch {
print("Failed to export theme: \(error.localizedDescription)")
}
}
}
}

func exportAllCustomThemes() {
let openPanel = NSOpenPanel()
openPanel.prompt = "Export"
openPanel.canChooseFiles = false
openPanel.canChooseDirectories = true
openPanel.allowsMultipleSelection = false

openPanel.begin { result in
if result == .OK, let exportDirectory = openPanel.url {
let customThemes = self.themes.filter { !$0.isBundled }

for theme in customThemes {
guard let sourceURL = theme.fileURL else { continue }

let destinationURL = exportDirectory.appendingPathComponent("\(theme.displayName).cetheme")

do {
try FileManager.default.copyItem(at: sourceURL, to: destinationURL)
print("Exported \(theme.displayName) to \(destinationURL.path)")
} catch {
print("Failed to export \(theme.displayName): \(error.localizedDescription)")
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ struct ThemeSettingsThemeRow: View {

@ObservedObject private var themeModel: ThemeModel = .shared

@State private var presentingDetails: Bool = false

@State private var isHovering = false

@State private var deleteConfirmationIsPresented = false

var body: some View {
HStack {
Image(systemName: "checkmark")
Expand All @@ -42,15 +42,20 @@ struct ThemeSettingsThemeRow: View {
Menu {
Button("Details...") {
themeModel.detailsTheme = theme
themeModel.detailsIsPresented = true
}
Button("Duplicate") {
Button("Duplicate...") {
if let fileURL = theme.fileURL {
themeModel.duplicate(fileURL)
}
}
Button("Export...") {
themeModel.exportTheme(theme)
}
.disabled(theme.isBundled)
Divider()
Button("Delete") {
themeModel.delete(theme)
Button("Delete...") {
deleteConfirmationIsPresented = true
}
.disabled(theme.isBundled)
} label: {
Expand All @@ -63,5 +68,18 @@ struct ThemeSettingsThemeRow: View {
.onHover { hovering in
isHovering = hovering
}
.alert(
Text("Are you sure you want to delete the theme “\(theme.displayName)”?"),
isPresented: $deleteConfirmationIsPresented
) {
Button("Delete Theme") {
themeModel.delete(theme)
}
Button("Cancel") {
deleteConfirmationIsPresented = false
}
} message: {
Text("This action cannot be undone.")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ struct ThemeSettingsThemeDetails: View {

@StateObject private var themeModel: ThemeModel = .shared

@State private var duplicatingTheme: Theme?

@State private var deleteConfirmationIsPresented = false

var isActive: Bool {
themeModel.getThemeActive(theme)
}

init(theme: Binding<Theme>) {
_theme = theme
originalTheme = theme.wrappedValue
Expand Down Expand Up @@ -168,26 +176,27 @@ struct ThemeSettingsThemeDetails: View {
.accessibilityLabel("Warning: Duplicate this theme to make changes.")
} else if !themeModel.isAdding {
Button(role: .destructive) {
themeModel.delete(theme)
dismiss()
deleteConfirmationIsPresented = true
} label: {
Text("Delete")
Text("Delete...")
.foregroundStyle(.red)
.frame(minWidth: 56)
}
Button {
if let fileURL = theme.fileURL {
duplicatingTheme = theme
themeModel.duplicate(fileURL)
}
} label: {
Text("Duplicate")
Text("Duplicate...")
.frame(minWidth: 56)
}
}
Spacer()
if !themeModel.isAdding && theme.isBundled {
Button {
if let fileURL = theme.fileURL {
duplicatingTheme = theme
themeModel.duplicate(fileURL)
}
} label: {
Expand All @@ -197,12 +206,28 @@ struct ThemeSettingsThemeDetails: View {
} else {
Button {
if themeModel.isAdding {
themeModel.delete(theme)
if let previousTheme = themeModel.previousTheme {
themeModel.activateTheme(previousTheme)
}
if let duplicatingWithinDetails = duplicatingTheme {
let duplicateTheme = theme
themeModel.detailsTheme = duplicatingWithinDetails
themeModel.delete(duplicateTheme)
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
themeModel.delete(theme)
}
}
} else {
themeModel.cancelDetails(theme)
}

dismiss()
if duplicatingTheme == nil {
dismiss()
} else {
duplicatingTheme = nil
themeModel.isAdding = false
}
} label: {
Text("Cancel")
.frame(minWidth: 56)
Expand All @@ -223,5 +248,19 @@ struct ThemeSettingsThemeDetails: View {
.padding()
}
.constrainHeightToWindow()
.alert(
Text("Are you sure you want to delete the theme “\(theme.displayName)”?"),
isPresented: $deleteConfirmationIsPresented
) {
Button("Delete Theme") {
themeModel.delete(theme)
dismiss()
}
Button("Cancel") {
deleteConfirmationIsPresented = false
}
} message: {
Text("This action cannot be undone.")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ struct ThemeSettingsView: View {
Text("Import Theme...")
}
Button {
// TODO: #1874
themeModel.exportAllCustomThemes()
} label: {
Text("Export All Custom Themes...")
}.disabled(true)
}
}
})
.padding(.horizontal, 5)
Expand Down Expand Up @@ -90,30 +90,37 @@ struct ThemeSettingsView: View {
}
.padding(.top, 10)
}
.sheet(item: $themeModel.detailsTheme) {
themeModel.isAdding = false
} content: { theme in
if let index = themeModel.themes.firstIndex(where: {
.sheet(isPresented: $themeModel.detailsIsPresented, onDismiss: {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
themeModel.isAdding = false
}
}, content: {
if let theme = themeModel.detailsTheme, let index = themeModel.themes.firstIndex(where: {
$0.fileURL?.absoluteString == theme.fileURL?.absoluteString
}) {
ThemeSettingsThemeDetails(theme: Binding(
get: { themeModel.themes[index] },
set: { newValue in
themeModel.themes[index] = newValue
themeModel.save(newValue)
if settings.selectedTheme == theme.name {
themeModel.activateTheme(newValue)
if themeModel.detailsIsPresented {
themeModel.themes[index] = newValue
themeModel.save(newValue)
if settings.selectedTheme == theme.name {
themeModel.activateTheme(newValue)
}
}
}
))
}
}
})
.onAppear {
updateFilteredThemes()
}
.onChange(of: themeSearchQuery) { _ in
updateFilteredThemes()
}
.onChange(of: themeModel.themes) { _ in
updateFilteredThemes()
}
.onChange(of: colorScheme) { newColorScheme in
updateFilteredThemes(overrideColorScheme: newColorScheme)
}
Expand Down

0 comments on commit 7048a28

Please sign in to comment.