Skip to content

Commit bc010eb

Browse files
Merge pull request #150 from QueryaHub/issue/119-builtin-theme-assets
TP-24: Add built-in theme assets
2 parents 7f29de1 + 8121c2e commit bc010eb

12 files changed

Lines changed: 473 additions & 27 deletions

assets/themes/cyberpunk-neon.json

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
{
2+
"name": "Querya Cyberpunk Neon",
3+
"type": "dark",
4+
"colors": {
5+
"activityBar.background": "#050508",
6+
"statusBar.background": "#050508",
7+
"sideBar.background": "#0c0820",
8+
"sideBar.foreground": "#8b7cf8",
9+
"tab.activeBackground": "#14102a",
10+
"panel.background": "#14102a",
11+
"input.background": "#14102a",
12+
"editor.background": "#0a0a14",
13+
"editor.foreground": "#e8f4ff",
14+
"editor.selectionBackground": "#ff2a6d44",
15+
"editorLineNumber.foreground": "#4a3f7a",
16+
"editorBracketMatch.background": "#00f5ff33",
17+
"editorWidget.border": "#00f5ff66",
18+
"focusBorder": "#00f5ff",
19+
"list.hoverBackground": "#ff2a6d22",
20+
"gitDecoration.modifiedResourceForeground": "#fcee09",
21+
"gitDecoration.untrackedResourceForeground": "#39ff14"
22+
},
23+
"tokenColors": [
24+
{
25+
"name": "Comments",
26+
"scope": ["comment", "comment.line", "comment.block", "punctuation.definition.comment"],
27+
"settings": {
28+
"foreground": "#5c4d8a",
29+
"fontStyle": "italic"
30+
}
31+
},
32+
{
33+
"name": "Keywords",
34+
"scope": [
35+
"keyword",
36+
"keyword.control",
37+
"keyword.operator.logical",
38+
"storage.type",
39+
"storage.modifier"
40+
],
41+
"settings": {
42+
"foreground": "#ff2a6d",
43+
"fontStyle": "bold"
44+
}
45+
},
46+
{
47+
"name": "Strings",
48+
"scope": ["string", "string.quoted.single", "string.quoted.double"],
49+
"settings": {
50+
"foreground": "#fcee09"
51+
}
52+
},
53+
{
54+
"name": "Numbers",
55+
"scope": ["constant.numeric", "constant.language"],
56+
"settings": {
57+
"foreground": "#bd00ff"
58+
}
59+
},
60+
{
61+
"name": "Functions",
62+
"scope": ["entity.name.function", "support.function"],
63+
"settings": {
64+
"foreground": "#00f5ff"
65+
}
66+
},
67+
{
68+
"name": "Types / classes",
69+
"scope": ["entity.name.type", "support.type"],
70+
"settings": {
71+
"foreground": "#8b7cf8"
72+
}
73+
},
74+
{
75+
"name": "Variables",
76+
"scope": ["variable", "variable.other"],
77+
"settings": {
78+
"foreground": "#e8f4ff"
79+
}
80+
},
81+
{
82+
"name": "JSON keys",
83+
"scope": ["support.type.property-name.json"],
84+
"settings": {
85+
"foreground": "#00f5ff"
86+
}
87+
},
88+
{
89+
"name": "JSON strings",
90+
"scope": ["string.quoted.double.json"],
91+
"settings": {
92+
"foreground": "#39ff14"
93+
}
94+
}
95+
]
96+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/// Bundled theme JSON files shipped in the Flutter asset bundle.
2+
abstract final class BuiltinThemeAssets {
3+
static const directory = 'assets/themes';
4+
5+
/// File names under [directory] that are registered as built-in themes.
6+
static const bundledFiles = <String>[
7+
'cyberpunk-neon.json',
8+
];
9+
10+
static String assetPath(String fileName) => '$directory/$fileName';
11+
}

lib/core/theme/theme_controller.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,9 @@ class ThemeController extends ChangeNotifier {
459459
_userOverrides = const {};
460460
_importedThemeName = null;
461461
_themeAnimationEnabled = false;
462-
_availableThemes = List.unmodifiable(_builtinThemeDefinitions);
462+
_availableThemes = _mergeBuiltinThemes(
463+
await _registryService.loadThemeDefinitions(),
464+
);
463465
_selectedThemeId = null;
464466
_selectedThemePath = null;
465467
_selectedThemeLoadError = null;

lib/core/theme/theme_registry_service.dart

Lines changed: 117 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import 'dart:convert';
22
import 'dart:io';
33

44
import 'package:flutter/foundation.dart';
5+
import 'package:flutter/services.dart' show rootBundle;
56
import 'package:path/path.dart' as p;
67

78
import '../storage/app_settings.dart';
9+
import 'builtin_theme_assets.dart';
810
import 'parser/jsonc_preprocessor.dart';
911
import 'parser/querya_theme_from_manifest.dart';
1012
import 'parser/querya_theme_from_vscode.dart';
@@ -21,17 +23,24 @@ class ThemeRegistryService {
2123
ThemeRegistryService({
2224
Future<Directory> Function()? userThemesDirectory,
2325
Future<Directory> Function()? importedThemesDirectory,
26+
Future<String> Function(String assetPath)? assetLoader,
27+
List<String>? bundledThemeAssetFiles,
2428
int maxCacheEntries = 16,
2529
}) : _userThemesDirectory =
2630
userThemesDirectory ?? ThemePaths.userThemesDirectory,
2731
_importedThemesDirectory =
2832
importedThemesDirectory ?? ThemePaths.importedThemesDirectory,
33+
_assetLoader = assetLoader ?? ((path) => rootBundle.loadString(path)),
34+
_bundledThemeAssetFiles =
35+
bundledThemeAssetFiles ?? BuiltinThemeAssets.bundledFiles,
2936
_themeCache = _ThemeLruCache(maxEntries: maxCacheEntries);
3037

3138
static const defaultMaxCacheEntries = 16;
3239

3340
final Future<Directory> Function() _userThemesDirectory;
3441
final Future<Directory> Function() _importedThemesDirectory;
42+
final Future<String> Function(String assetPath) _assetLoader;
43+
final List<String> _bundledThemeAssetFiles;
3544
final _ThemeLruCache _themeCache;
3645
int _themeParseCount = 0;
3746

@@ -48,6 +57,8 @@ class ThemeRegistryService {
4857
Future<List<ThemeDefinition>> loadThemeDefinitions() async {
4958
final definitions = <ThemeDefinition>[];
5059

60+
await _loadBuiltinAssetDefinitions(definitions);
61+
5162
await _scanDirectory(
5263
await _userThemesDirectory(),
5364
ThemeSource.filesystem,
@@ -171,22 +182,21 @@ class ThemeRegistryService {
171182
);
172183
}
173184

174-
final file = File(path);
175-
if (!await file.exists()) {
176-
return ThemeLoadFailure(
177-
definition: definition,
178-
message: 'Theme file not found.',
179-
);
180-
}
181-
182185
final cacheKey = definition.stableCacheKey;
183186
final cachedTheme = _themeCache.get(cacheKey);
184187
if (cachedTheme != null) {
185188
return ThemeLoadSuccess(definition: definition, theme: cachedTheme);
186189
}
187190

188191
try {
189-
final raw = await file.readAsString();
192+
final raw = await _readThemeRaw(definition);
193+
if (raw == null) {
194+
return ThemeLoadFailure(
195+
definition: definition,
196+
message: 'Theme file not found.',
197+
);
198+
}
199+
190200
final theme = switch (definition.format) {
191201
ThemeFormat.queryaCustom => _loadCustomTheme(raw),
192202
ThemeFormat.vscode => _loadVsCodeTheme(raw),
@@ -305,6 +315,56 @@ class ThemeRegistryService {
305315
return null;
306316
}
307317

318+
Future<void> _loadBuiltinAssetDefinitions(List<ThemeDefinition> out) async {
319+
for (final fileName in _bundledThemeAssetFiles) {
320+
final assetPath = BuiltinThemeAssets.assetPath(fileName);
321+
try {
322+
final raw = await _readAssetString(assetPath);
323+
final hash = _contentHash(raw);
324+
final json = _decodeRoot(raw);
325+
if (json == null) {
326+
_logScanError(assetPath, 'Invalid JSON');
327+
continue;
328+
}
329+
330+
final definition = _definitionFromRaw(
331+
json: json,
332+
path: assetPath,
333+
fileBaseName: p.basenameWithoutExtension(fileName),
334+
source: ThemeSource.builtin,
335+
contentHash: hash,
336+
);
337+
if (definition != null) {
338+
out.add(definition);
339+
}
340+
} on Object catch (e) {
341+
_logScanError(assetPath, e);
342+
}
343+
}
344+
}
345+
346+
Future<String?> _readThemeRaw(ThemeDefinition definition) async {
347+
final path = definition.path;
348+
if (path == null || path.isEmpty) return null;
349+
350+
if (definition.source == ThemeSource.builtin && _isAssetPath(path)) {
351+
try {
352+
return await _readAssetString(path);
353+
} on Object {
354+
return null;
355+
}
356+
}
357+
358+
final file = File(path);
359+
if (!await file.exists()) return null;
360+
return file.readAsString();
361+
}
362+
363+
Future<String> _readAssetString(String assetPath) =>
364+
_assetLoader(assetPath);
365+
366+
static bool _isAssetPath(String path) => path.startsWith('assets/');
367+
308368
Future<void> _scanDirectory(
309369
Directory directory,
310370
ThemeSource source,
@@ -349,45 +409,76 @@ class ThemeRegistryService {
349409

350410
final schema = json['schema']?.toString();
351411
if (schema == queryaThemeSchemaV1) {
352-
return _customDefinition(
412+
return _definitionFromRaw(
353413
json: json,
354-
file: file,
414+
path: file.path,
415+
fileBaseName: p.basenameWithoutExtension(file.path),
355416
source: source,
356-
lastModified: stat.modified,
357417
contentHash: hash,
418+
lastModified: stat.modified,
358419
);
359420
}
360421

361-
return _vscodeDefinition(
422+
return _definitionFromRaw(
362423
json: json,
363-
file: file,
424+
path: file.path,
425+
fileBaseName: p.basenameWithoutExtension(file.path),
364426
source: source,
365-
lastModified: stat.modified,
366427
contentHash: hash,
428+
lastModified: stat.modified,
367429
);
368430
} on Object catch (e) {
369431
_logScanError(file.path, e);
370432
return null;
371433
}
372434
}
373435

436+
ThemeDefinition? _definitionFromRaw({
437+
required Map<String, dynamic> json,
438+
required String path,
439+
required String fileBaseName,
440+
required ThemeSource source,
441+
required String contentHash,
442+
DateTime? lastModified,
443+
}) {
444+
final schema = json['schema']?.toString();
445+
if (schema == queryaThemeSchemaV1) {
446+
return _customDefinition(
447+
json: json,
448+
source: source,
449+
contentHash: contentHash,
450+
path: path,
451+
lastModified: lastModified,
452+
);
453+
}
454+
455+
return _vscodeDefinition(
456+
json: json,
457+
source: source,
458+
contentHash: contentHash,
459+
fileBaseName: fileBaseName,
460+
path: path,
461+
lastModified: lastModified,
462+
);
463+
}
464+
374465
ThemeDefinition? _customDefinition({
375466
required Map<String, dynamic> json,
376-
required File file,
377467
required ThemeSource source,
378-
required DateTime lastModified,
379468
required String contentHash,
469+
required String path,
470+
DateTime? lastModified,
380471
}) {
381472
final id = json['id']?.toString().trim();
382473
final name = json['name']?.toString().trim();
383474
final type = json['type']?.toString().trim().toLowerCase();
384475

385476
if (id == null || id.isEmpty || name == null || name.isEmpty) {
386-
_logScanError(file.path, 'Missing required custom theme fields');
477+
_logScanError(path, 'Missing required custom theme fields');
387478
return null;
388479
}
389480
if (type != 'dark' && type != 'light') {
390-
_logScanError(file.path, 'Invalid custom theme type "$type"');
481+
_logScanError(path, 'Invalid custom theme type "$type"');
391482
return null;
392483
}
393484

@@ -397,31 +488,31 @@ class ThemeRegistryService {
397488
source: source,
398489
format: ThemeFormat.queryaCustom,
399490
isDark: type == 'dark',
400-
path: file.path,
491+
path: path,
401492
lastModified: lastModified,
402493
contentHash: contentHash,
403494
);
404495
}
405496

406497
ThemeDefinition? _vscodeDefinition({
407498
required Map<String, dynamic> json,
408-
required File file,
409499
required ThemeSource source,
410-
required DateTime lastModified,
411500
required String contentHash,
501+
required String fileBaseName,
502+
required String path,
503+
DateTime? lastModified,
412504
}) {
413-
final fileId = p.basenameWithoutExtension(file.path);
414505
final rawName = json['name']?.toString().trim();
415-
final name = rawName != null && rawName.isNotEmpty ? rawName : fileId;
506+
final name = rawName != null && rawName.isNotEmpty ? rawName : fileBaseName;
416507
final type = json['type']?.toString().trim().toLowerCase();
417508

418509
return ThemeDefinition(
419-
id: fileId,
510+
id: fileBaseName,
420511
name: name,
421512
source: source,
422513
format: ThemeFormat.vscode,
423514
isDark: type == 'dark',
424-
path: file.path,
515+
path: path,
425516
lastModified: lastModified,
426517
contentHash: contentHash,
427518
);

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,4 @@ flutter:
4242
uses-material-design: true
4343
assets:
4444
- assets/images/
45+
- assets/themes/

0 commit comments

Comments
 (0)