From a5e681c16865d63c6d3d37ec3d9abdea684e77e1 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sun, 14 Jun 2026 14:07:07 +0300 Subject: [PATCH 1/3] feat(theme): standardize startup fallback when selected theme fails Restore persisted registry themes safely on load with Querya Dark as the active fallback, a stable Preferences error message, and settings kept intact. --- lib/core/theme/theme_controller.dart | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/lib/core/theme/theme_controller.dart b/lib/core/theme/theme_controller.dart index fbe168bf..75c52892 100644 --- a/lib/core/theme/theme_controller.dart +++ b/lib/core/theme/theme_controller.dart @@ -20,6 +20,10 @@ class ThemeController extends ChangeNotifier { static const String builtinQueryaDarkId = 'querya-dark'; static const String builtinQueryaLightId = 'querya-light'; + /// Shown in Preferences when persisted registry selection cannot be restored. + static const String selectedThemeStartupFallbackMessage = + 'Selected theme failed to load. Using Querya Dark.'; + static const ThemeDefinition builtinQueryaDarkDefinition = ThemeDefinition( id: builtinQueryaDarkId, name: 'Querya Dark', @@ -245,16 +249,22 @@ class ThemeController extends ChangeNotifier { path: _selectedThemePath, ); if (stillAvailable == null) { - _selectedThemeLoadError = - 'Selected theme "$selectedId" is not available.'; + _markRegistrySelectionFailed(); return; } if (_registryTheme != null) { _selectedThemeLoadError = null; + _registrySelectionFailed = false; } } + void _markRegistrySelectionFailed() { + _registryTheme = null; + _registrySelectionFailed = true; + _selectedThemeLoadError = selectedThemeStartupFallbackMessage; + } + Future setThemeById(String id) async { if (id == builtinQueryaDarkId) { await _applyBuiltinPreset(QueryaThemePreset.queryaDark); @@ -486,9 +496,7 @@ class ThemeController extends ChangeNotifier { path: _selectedThemePath, ); if (definition == null) { - _registrySelectionFailed = true; - _selectedThemeLoadError = - 'Selected theme "${_selectedThemeId!}" is not available.'; + _markRegistrySelectionFailed(); return; } @@ -498,12 +506,13 @@ class ThemeController extends ChangeNotifier { _registryTheme = theme; _selectedThemeId = definition.id; _selectedThemePath = definition.path; + _registrySelectionFailed = false; + _selectedThemeLoadError = null; _themeMode = theme.brightness == Brightness.light ? ThemeMode.light : ThemeMode.dark; - case ThemeLoadFailure(:final message): - _registrySelectionFailed = true; - _selectedThemeLoadError = message; + case ThemeLoadFailure(): + _markRegistrySelectionFailed(); } } From 51e1ccdf4c8cb7cf9cd2f4b4ccfecc7dd2f493cd Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sun, 14 Jun 2026 14:07:07 +0300 Subject: [PATCH 2/3] test(theme): cover missing, invalid, and recovery startup theme paths Closes #122 --- test/core/theme/theme_controller_test.dart | 61 +++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/test/core/theme/theme_controller_test.dart b/test/core/theme/theme_controller_test.dart index f71315df..2830361b 100644 --- a/test/core/theme/theme_controller_test.dart +++ b/test/core/theme/theme_controller_test.dart @@ -244,7 +244,10 @@ void main() { await c.load(); expect(c.activeTheme, QueryaTheme.darkDefault); - expect(c.selectedThemeLoadError, isNotNull); + expect( + c.selectedThemeLoadError, + ThemeController.selectedThemeStartupFallbackMessage, + ); expect(await AppSettings.instance.getSelectedThemeId(), 'missing-theme'); expect( await AppSettings.instance.getThemePreset(), @@ -252,6 +255,62 @@ void main() { ); }); + test('missing theme file on startup falls back to Querya Dark', () async { + final c = ThemeController.instance; + final themeFile = File(p.join(themesDir.path, 'querya_custom_dark.json')); + await _copyFixture('querya_custom_dark.json', themeFile); + await c.load(); + await c.setThemeById('fixture-custom-dark'); + await themeFile.delete(); + + await c.load(); + + expect(c.activeTheme, QueryaTheme.darkDefault); + expect( + c.selectedThemeLoadError, + ThemeController.selectedThemeStartupFallbackMessage, + ); + expect(c.selectedThemeId, 'fixture-custom-dark'); + expect(await AppSettings.instance.getSelectedThemeId(), 'fixture-custom-dark'); + }); + + test('invalid theme file skipped on startup falls back to Querya Dark', + () async { + final c = ThemeController.instance; + final themeFile = File(p.join(themesDir.path, 'broken-theme.json')); + await _copyFixture('querya_custom_invalid_missing_id.json', themeFile); + await AppSettings.instance.setSelectedThemeId('broken-theme'); + await AppSettings.instance.setSelectedThemeSource('filesystem'); + await AppSettings.instance.setSelectedThemePath(themeFile.path); + + await c.load(); + + expect(c.activeTheme, QueryaTheme.darkDefault); + expect( + c.selectedThemeLoadError, + ThemeController.selectedThemeStartupFallbackMessage, + ); + expect(await AppSettings.instance.getSelectedThemeId(), 'broken-theme'); + }); + + test('valid theme selection after startup failure clears error', () async { + final c = ThemeController.instance; + await AppSettings.instance.setSelectedThemeId('missing-theme'); + await AppSettings.instance.setSelectedThemeSource('filesystem'); + await c.load(); + expect(c.selectedThemeLoadError, isNotNull); + + await _copyFixture( + 'querya_custom_dark.json', + File(p.join(themesDir.path, 'querya_custom_dark.json')), + ); + await c.loadAvailableThemes(); + await c.setThemeById('fixture-custom-dark'); + + expect(c.selectedThemeLoadError, isNull); + expect(c.activeTheme.colorScheme.primary, parseQueryaThemeColor('#38BDF8')); + }); + test('setPreset clears registry selection', () async { final c = ThemeController.instance; await _copyFixture( From d294329a2fac34a78bf4b273711b1cd3c05e7bd9 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sun, 14 Jun 2026 14:10:35 +0300 Subject: [PATCH 3/3] fix(theme): keep in-memory theme when refresh loses disk file Do not clear the active registry theme on loadAvailableThemes when the scanned list no longer includes the selection but the theme is already loaded. --- lib/core/theme/theme_controller.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/core/theme/theme_controller.dart b/lib/core/theme/theme_controller.dart index 75c52892..9c15218c 100644 --- a/lib/core/theme/theme_controller.dart +++ b/lib/core/theme/theme_controller.dart @@ -249,6 +249,11 @@ class ThemeController extends ChangeNotifier { path: _selectedThemePath, ); if (stillAvailable == null) { + if (_registryTheme != null) { + // Keep the in-memory active theme; only the on-disk scan lost the file. + _selectedThemeLoadError = selectedThemeStartupFallbackMessage; + return; + } _markRegistrySelectionFailed(); return; }