Skip to content

Commit 74e19e9

Browse files
committed
fix: eliminate theme flicker on update, add isEditable guards, fix localization
1 parent 462c00f commit 74e19e9

4 files changed

Lines changed: 32 additions & 21 deletions

File tree

TablePro/Theme/ThemeRegistryInstaller.swift

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -57,27 +57,12 @@ internal final class ThemeRegistryInstaller {
5757
// MARK: - Uninstall
5858

5959
func uninstall(registryPluginId: String) throws {
60-
var meta = ThemeStorage.loadRegistryMeta()
61-
let themesToRemove = meta.installed.filter { $0.registryPluginId == registryPluginId }
62-
63-
// Update meta first so state is always consistent even if file cleanup fails
64-
meta.installed.removeAll { $0.registryPluginId == registryPluginId }
65-
try ThemeStorage.saveRegistryMeta(meta)
66-
67-
// Best-effort file cleanup
68-
for entry in themesToRemove {
69-
do {
70-
try ThemeStorage.deleteRegistryTheme(id: entry.id)
71-
} catch {
72-
Self.logger.warning("Failed to delete registry theme file \(entry.id): \(error)")
73-
}
74-
}
60+
let removedThemeIds = try removeRegistryFiles(for: registryPluginId)
7561

7662
ThemeEngine.shared.reloadAvailableThemes()
7763

7864
// Fall back if the active theme was uninstalled
79-
let activeId = ThemeEngine.shared.activeTheme.id
80-
if themesToRemove.contains(where: { $0.id == activeId }) {
65+
if removedThemeIds.contains(ThemeEngine.shared.activeTheme.id) {
8166
ThemeEngine.shared.activateTheme(id: "tablepro.default-light")
8267
}
8368

@@ -95,9 +80,10 @@ internal final class ThemeRegistryInstaller {
9580
// Download, verify, and decode new themes first (no side effects yet)
9681
let stagedThemes = try await downloadAndDecode(plugin, progress: progress)
9782

98-
// New version is fully staged — now safe to remove old and write new
99-
try uninstall(registryPluginId: plugin.id)
83+
// Remove old files without triggering theme reload or fallback
84+
_ = try removeRegistryFiles(for: plugin.id)
10085

86+
// Write new themes
10187
var installedThemes: [InstalledRegistryTheme] = []
10288
for theme in stagedThemes {
10389
try ThemeStorage.saveRegistryTheme(theme)
@@ -113,16 +99,38 @@ internal final class ThemeRegistryInstaller {
11399
meta.installed.append(contentsOf: installedThemes)
114100
try ThemeStorage.saveRegistryMeta(meta)
115101

102+
// Single reload after swap is complete — no intermediate flicker
116103
ThemeEngine.shared.reloadAvailableThemes()
117104

118-
// Re-activate if the user had a theme from this plugin active
119105
if ThemeEngine.shared.availableThemes.contains(where: { $0.id == activeId }) {
120106
ThemeEngine.shared.activateTheme(id: activeId)
121107
}
122108

123109
Self.logger.info("Updated \(installedThemes.count) theme(s) for registry plugin: \(plugin.id)")
124110
}
125111

112+
/// Removes meta entries and files for a registry plugin. Returns removed theme IDs.
113+
/// Does NOT reload ThemeEngine or trigger fallback — callers manage that.
114+
@discardableResult
115+
private func removeRegistryFiles(for registryPluginId: String) throws -> Set<String> {
116+
var meta = ThemeStorage.loadRegistryMeta()
117+
let themesToRemove = meta.installed.filter { $0.registryPluginId == registryPluginId }
118+
let removedIds = Set(themesToRemove.map(\.id))
119+
120+
meta.installed.removeAll { $0.registryPluginId == registryPluginId }
121+
try ThemeStorage.saveRegistryMeta(meta)
122+
123+
for entry in themesToRemove {
124+
do {
125+
try ThemeStorage.deleteRegistryTheme(id: entry.id)
126+
} catch {
127+
Self.logger.warning("Failed to delete registry theme file \(entry.id): \(error)")
128+
}
129+
}
130+
131+
return removedIds
132+
}
133+
126134
// MARK: - Query
127135

128136
func isInstalled(_ registryPluginId: String) -> Bool {

TablePro/Views/Settings/Appearance/ThemeEditorColorsSection.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ internal struct ThemeEditorColorsSection: View {
252252
Binding(
253253
get: { theme[keyPath: keyPath] },
254254
set: { newValue in
255+
guard theme.isEditable else { return }
255256
var updated = theme
256257
updated[keyPath: keyPath] = newValue
257258
try? engine.saveUserTheme(updated)

TablePro/Views/Settings/Appearance/ThemeEditorLayoutSection.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ internal struct ThemeEditorLayoutSection: View {
9393
Binding<CGFloat>(
9494
get: { theme[keyPath: keyPath] },
9595
set: { newValue in
96+
guard theme.isEditable else { return }
9697
var updated = theme
9798
updated[keyPath: keyPath] = newValue
9899
try? engine.saveUserTheme(updated)
@@ -104,6 +105,7 @@ internal struct ThemeEditorLayoutSection: View {
104105
Binding<Double>(
105106
get: { theme[keyPath: keyPath] },
106107
set: { newValue in
108+
guard theme.isEditable else { return }
107109
var updated = theme
108110
updated[keyPath: keyPath] = newValue
109111
try? engine.saveUserTheme(updated)

TablePro/Views/Settings/Appearance/ThemeListView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ internal struct ThemeListView: View {
127127
Button(String(localized: "Cancel"), role: .cancel) {}
128128
} message: {
129129
let name = engine.availableThemes.first(where: { $0.id == selectedThemeId })?.name ?? ""
130-
Text("Are you sure you want to delete \"\(name)\"?")
130+
Text(String(localized: "Are you sure you want to delete \"\(name)\"?"))
131131
}
132132
.alert(String(localized: "Error"), isPresented: $showError) {
133133
Button(String(localized: "OK")) {}

0 commit comments

Comments
 (0)