Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions lib/core/theme/theme_registry_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -42,6 +47,73 @@ class ThemeRegistryService {
return List.unmodifiable(definitions);
}

/// Parses a scanned [definition] into a runtime [QueryaTheme].
Future<ThemeLoadResult> 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<void> _scanDirectory(
Directory directory,
ThemeSource source,
Expand Down
69 changes: 69 additions & 0 deletions test/core/theme/theme_registry_service_test.dart
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<ThemeLoadSuccess>());
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<ThemeLoadSuccess>());
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<ThemeLoadFailure>());
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<ThemeLoadFailure>());
expect((result as ThemeLoadFailure).message, contains('id'));
});
});
}
Loading