diff --git a/lib/core/theme/theme_controller.dart b/lib/core/theme/theme_controller.dart index fbe168bf..9c15218c 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,27 @@ class ThemeController extends ChangeNotifier { path: _selectedThemePath, ); if (stillAvailable == null) { - _selectedThemeLoadError = - 'Selected theme "$selectedId" is not available.'; + if (_registryTheme != null) { + // Keep the in-memory active theme; only the on-disk scan lost the file. + _selectedThemeLoadError = selectedThemeStartupFallbackMessage; + return; + } + _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 +501,7 @@ class ThemeController extends ChangeNotifier { path: _selectedThemePath, ); if (definition == null) { - _registrySelectionFailed = true; - _selectedThemeLoadError = - 'Selected theme "${_selectedThemeId!}" is not available.'; + _markRegistrySelectionFailed(); return; } @@ -498,12 +511,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(); } } 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(