@@ -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 {
0 commit comments