diff --git a/lib/core/theme/theme_registry_service.dart b/lib/core/theme/theme_registry_service.dart index 2330c14b..219fef16 100644 --- a/lib/core/theme/theme_registry_service.dart +++ b/lib/core/theme/theme_registry_service.dart @@ -5,8 +5,13 @@ import 'package:flutter/foundation.dart'; import 'package:path/path.dart' as p; import 'parser/jsonc_preprocessor.dart'; +import 'parser/querya_theme_from_manifest.dart'; +import 'parser/querya_theme_from_vscode.dart'; import 'parser/querya_theme_manifest.dart'; +import 'parser/vscode_theme_manifest.dart'; +import 'querya_theme.dart'; import 'theme_definition.dart'; +import 'theme_load_result.dart'; import 'theme_paths.dart'; /// Scans theme directories and exposes lightweight [ThemeDefinition] metadata. @@ -42,6 +47,73 @@ class ThemeRegistryService { return List.unmodifiable(definitions); } + /// Parses a scanned [definition] into a runtime [QueryaTheme]. + Future loadTheme(ThemeDefinition definition) async { + final path = definition.path; + if (path == null || path.isEmpty) { + return ThemeLoadFailure( + definition: definition, + message: 'Theme file path is missing.', + ); + } + + final file = File(path); + if (!await file.exists()) { + return ThemeLoadFailure( + definition: definition, + message: 'Theme file not found.', + ); + } + + try { + final raw = await file.readAsString(); + final theme = switch (definition.format) { + ThemeFormat.queryaCustom => _loadCustomTheme(raw), + ThemeFormat.vscode => _loadVsCodeTheme(raw), + }; + return ThemeLoadSuccess(definition: definition, theme: theme); + } on QueryaThemeManifestParseException catch (e) { + return ThemeLoadFailure( + definition: definition, + message: e.message, + error: e, + ); + } on VsCodeThemeParseException catch (e) { + return ThemeLoadFailure( + definition: definition, + message: e.message, + error: e, + ); + } on IOException catch (e) { + return ThemeLoadFailure( + definition: definition, + message: 'Failed to read theme file.', + error: e, + ); + } on Object catch (e) { + return ThemeLoadFailure( + definition: definition, + message: 'Failed to load theme.', + error: e, + ); + } + } + + QueryaTheme _loadCustomTheme(String raw) { + final manifest = QueryaThemeManifest.fromJsonString(raw); + return queryaThemeFromManifest(manifest); + } + + QueryaTheme _loadVsCodeTheme(String raw) { + final manifest = VsCodeThemeManifest.fromJsonString(raw); + if (manifest.colors.isEmpty) { + throw VsCodeThemeParseException( + 'Theme file has no "colors" section to import.', + ); + } + return buildQueryaThemeFromVsCodeManifest(manifest); + } + Future _scanDirectory( Directory directory, ThemeSource source, diff --git a/test/core/theme/theme_registry_service_test.dart b/test/core/theme/theme_registry_service_test.dart index b94ffdcf..ae305dfd 100644 --- a/test/core/theme/theme_registry_service_test.dart +++ b/test/core/theme/theme_registry_service_test.dart @@ -1,9 +1,12 @@ import 'dart:io'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as p; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; +import 'package:querya_desktop/core/theme/parser/color_parser.dart'; import 'package:querya_desktop/core/theme/theme_definition.dart'; +import 'package:querya_desktop/core/theme/theme_load_result.dart'; import 'package:querya_desktop/core/theme/theme_registry_service.dart'; class _FakePathProvider extends PathProviderPlatform { @@ -155,4 +158,70 @@ void main() { expect(definitions, hasLength(1)); }); }); + + group('ThemeRegistryService.loadTheme', () { + test('loads custom theme successfully', () async { + await _copyFixture( + 'querya_custom_dark.json', + File(p.join(themesDir.path, 'querya_custom_dark.json')), + ); + + final definition = (await registry.loadThemeDefinitions()).single; + final result = await registry.loadTheme(definition); + + expect(result, isA()); + final success = result as ThemeLoadSuccess; + expect(success.definition, definition); + expect(success.theme.brightness, Brightness.dark); + expect(success.theme.colorScheme.primary, parseQueryaThemeColor('#38BDF8')); + }); + + test('loads VS Code theme successfully', () async { + await _copyFixture( + 'dark_subset.json', + File(p.join(themesDir.path, 'dark_subset.json')), + ); + + final definition = (await registry.loadThemeDefinitions()).single; + final result = await registry.loadTheme(definition); + + expect(result, isA()); + final success = result as ThemeLoadSuccess; + expect(success.theme.editor.background, parseQueryaThemeColor('#1e1e1e')); + }); + + test('returns failure for deleted file', () async { + const definition = ThemeDefinition( + id: 'missing-theme', + name: 'Missing Theme', + source: ThemeSource.filesystem, + format: ThemeFormat.queryaCustom, + isDark: true, + path: '/no/such/theme.json', + ); + + final result = await registry.loadTheme(definition); + + expect(result, isA()); + expect((result as ThemeLoadFailure).message, 'Theme file not found.'); + }); + + test('returns failure for invalid custom file', () async { + final file = File(p.join(themesDir.path, 'broken.json')); + await _copyFixture('querya_custom_invalid_missing_id.json', file); + final definition = ThemeDefinition( + id: 'broken', + name: 'Broken', + source: ThemeSource.filesystem, + format: ThemeFormat.queryaCustom, + isDark: true, + path: file.path, + ); + + final result = await registry.loadTheme(definition); + + expect(result, isA()); + expect((result as ThemeLoadFailure).message, contains('id')); + }); + }); }