Skip to content

Theme export and theme details flicker fix when duplicating and then canceling #1920

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Nov 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading