Skip to content

Commit d378b46

Browse files
committed
fix: address review issues in theme registry installer
1 parent e6e715b commit d378b46

3 files changed

Lines changed: 49 additions & 32 deletions

File tree

TablePro/Theme/RegistryThemeMeta.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import Foundation
22

3-
struct RegistryThemeMeta: Codable {
3+
internal struct RegistryThemeMeta: Codable {
44
var installed: [InstalledRegistryTheme]
55

66
init(installed: [InstalledRegistryTheme] = []) {
77
self.installed = installed
88
}
99
}
1010

11-
struct InstalledRegistryTheme: Codable, Identifiable {
11+
internal struct InstalledRegistryTheme: Codable, Identifiable {
1212
let id: String
1313
let registryPluginId: String
1414
let version: String

TablePro/Theme/ThemeRegistryInstaller.swift

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -72,22 +72,23 @@ final class ThemeRegistryInstaller {
7272

7373
progress(0.7)
7474

75-
// Extract ZIP
75+
// Extract ZIP off main thread
7676
let extractDir = tempDir.appendingPathComponent("extracted", isDirectory: true)
7777
try FileManager.default.createDirectory(at: extractDir, withIntermediateDirectories: true)
7878

7979
let zipPath = tempDir.appendingPathComponent("theme.zip")
8080
try FileManager.default.moveItem(at: tempDownloadURL, to: zipPath)
8181

82-
let process = Process()
83-
process.executableURL = URL(fileURLWithPath: "/usr/bin/ditto")
84-
process.arguments = ["-xk", zipPath.path, extractDir.path]
85-
try process.run()
86-
process.waitUntilExit()
87-
88-
guard process.terminationStatus == 0 else {
89-
throw PluginError.installFailed("Failed to extract theme archive")
90-
}
82+
try await Task.detached(priority: .userInitiated) {
83+
let process = Process()
84+
process.executableURL = URL(fileURLWithPath: "/usr/bin/ditto")
85+
process.arguments = ["-xk", zipPath.path, extractDir.path]
86+
try process.run()
87+
process.waitUntilExit()
88+
guard process.terminationStatus == 0 else {
89+
throw PluginError.installFailed("Failed to extract theme archive")
90+
}
91+
}.value
9192

9293
// Find all JSON files in extracted directory
9394
let jsonFiles = try findJsonFiles(in: extractDir)
@@ -97,20 +98,25 @@ final class ThemeRegistryInstaller {
9798

9899
progress(0.9)
99100

100-
// Validate and install each theme
101-
var installedThemes: [InstalledRegistryTheme] = []
101+
// Decode all themes first to validate before writing any files
102102
let decoder = JSONDecoder()
103+
var decodedThemes: [ThemeDefinition] = []
103104

104105
for jsonURL in jsonFiles {
105106
let data = try Data(contentsOf: jsonURL)
106107
var theme = try decoder.decode(ThemeDefinition.self, from: data)
107108

108-
// Rewrite ID to registry namespace
109-
let originalSuffix = theme.id.contains(".") ?
110-
String(theme.id.split(separator: ".").last ?? Substring(theme.id)) :
111-
theme.id
112-
theme.id = "registry.\(plugin.id).\(originalSuffix)"
109+
// Rewrite ID to registry namespace using full original ID (dots replaced with hyphens)
110+
let sanitizedId = theme.id.replacingOccurrences(of: ".", with: "-")
111+
theme.id = "registry.\(plugin.id).\(sanitizedId)"
112+
113+
decodedThemes.append(theme)
114+
}
115+
116+
// All decoded successfully — now write atomically
117+
var installedThemes: [InstalledRegistryTheme] = []
113118

119+
for theme in decodedThemes {
114120
try ThemeStorage.saveRegistryTheme(theme)
115121

116122
installedThemes.append(InstalledRegistryTheme(
@@ -138,13 +144,19 @@ final class ThemeRegistryInstaller {
138144
var meta = ThemeStorage.loadRegistryMeta()
139145
let themesToRemove = meta.installed.filter { $0.registryPluginId == registryPluginId }
140146

141-
for entry in themesToRemove {
142-
try ThemeStorage.deleteRegistryTheme(id: entry.id)
143-
}
144-
147+
// Update meta first so state is always consistent even if file cleanup fails
145148
meta.installed.removeAll { $0.registryPluginId == registryPluginId }
146149
try ThemeStorage.saveRegistryMeta(meta)
147150

151+
// Best-effort file cleanup
152+
for entry in themesToRemove {
153+
do {
154+
try ThemeStorage.deleteRegistryTheme(id: entry.id)
155+
} catch {
156+
Self.logger.warning("Failed to delete registry theme file \(entry.id): \(error)")
157+
}
158+
}
159+
148160
ThemeEngine.shared.reloadAvailableThemes()
149161

150162
// Fall back if the active theme was uninstalled

TablePro/Views/Settings/Appearance/ThemeListView.swift

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ struct ThemeListView: View {
88
private var engine: ThemeEngine { ThemeEngine.shared }
99

1010
@State private var showDeleteConfirmation = false
11-
@State private var importError: String?
12-
@State private var showImportError = false
11+
@State private var errorMessage: String?
12+
@State private var showError = false
1313

1414
private var builtInThemes: [ThemeDefinition] {
1515
engine.availableThemes.filter(\.isBuiltIn)
@@ -129,11 +129,11 @@ struct ThemeListView: View {
129129
let name = engine.availableThemes.first(where: { $0.id == selectedThemeId })?.name ?? ""
130130
Text("Are you sure you want to delete \"\(name)\"?")
131131
}
132-
.alert(String(localized: "Import Error"), isPresented: $showImportError) {
132+
.alert(String(localized: "Error"), isPresented: $showError) {
133133
Button(String(localized: "OK")) {}
134134
} message: {
135-
if let importError {
136-
Text(importError)
135+
if let errorMessage {
136+
Text(errorMessage)
137137
}
138138
}
139139
}
@@ -157,8 +157,13 @@ struct ThemeListView: View {
157157
guard let theme = selectedTheme, theme.isRegistry else { return }
158158
let meta = ThemeStorage.loadRegistryMeta()
159159
guard let entry = meta.installed.first(where: { $0.id == theme.id }) else { return }
160-
try? engine.uninstallRegistryTheme(registryPluginId: entry.registryPluginId)
161-
selectedThemeId = engine.activeTheme.id
160+
do {
161+
try engine.uninstallRegistryTheme(registryPluginId: entry.registryPluginId)
162+
selectedThemeId = engine.activeTheme.id
163+
} catch {
164+
errorMessage = error.localizedDescription
165+
showError = true
166+
}
162167
}
163168

164169
private func exportActiveTheme() {
@@ -181,8 +186,8 @@ struct ThemeListView: View {
181186
engine.activateTheme(imported)
182187
selectedThemeId = imported.id
183188
} catch {
184-
importError = error.localizedDescription
185-
showImportError = true
189+
errorMessage = error.localizedDescription
190+
showError = true
186191
}
187192
}
188193
}

0 commit comments

Comments
 (0)