@@ -2,9 +2,11 @@ import 'dart:convert';
22import 'dart:io' ;
33
44import 'package:flutter/foundation.dart' ;
5+ import 'package:flutter/services.dart' show rootBundle;
56import 'package:path/path.dart' as p;
67
78import '../storage/app_settings.dart' ;
9+ import 'builtin_theme_assets.dart' ;
810import 'parser/jsonc_preprocessor.dart' ;
911import 'parser/querya_theme_from_manifest.dart' ;
1012import '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 );
0 commit comments