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
96 changes: 96 additions & 0 deletions assets/themes/cyberpunk-neon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
{
"name": "Querya Cyberpunk Neon",
"type": "dark",
"colors": {
"activityBar.background": "#050508",
"statusBar.background": "#050508",
"sideBar.background": "#0c0820",
"sideBar.foreground": "#8b7cf8",
"tab.activeBackground": "#14102a",
"panel.background": "#14102a",
"input.background": "#14102a",
"editor.background": "#0a0a14",
"editor.foreground": "#e8f4ff",
"editor.selectionBackground": "#ff2a6d44",
"editorLineNumber.foreground": "#4a3f7a",
"editorBracketMatch.background": "#00f5ff33",
"editorWidget.border": "#00f5ff66",
"focusBorder": "#00f5ff",
"list.hoverBackground": "#ff2a6d22",
"gitDecoration.modifiedResourceForeground": "#fcee09",
"gitDecoration.untrackedResourceForeground": "#39ff14"
},
"tokenColors": [
{
"name": "Comments",
"scope": ["comment", "comment.line", "comment.block", "punctuation.definition.comment"],
"settings": {
"foreground": "#5c4d8a",
"fontStyle": "italic"
}
},
{
"name": "Keywords",
"scope": [
"keyword",
"keyword.control",
"keyword.operator.logical",
"storage.type",
"storage.modifier"
],
"settings": {
"foreground": "#ff2a6d",
"fontStyle": "bold"
}
},
{
"name": "Strings",
"scope": ["string", "string.quoted.single", "string.quoted.double"],
"settings": {
"foreground": "#fcee09"
}
},
{
"name": "Numbers",
"scope": ["constant.numeric", "constant.language"],
"settings": {
"foreground": "#bd00ff"
}
},
{
"name": "Functions",
"scope": ["entity.name.function", "support.function"],
"settings": {
"foreground": "#00f5ff"
}
},
{
"name": "Types / classes",
"scope": ["entity.name.type", "support.type"],
"settings": {
"foreground": "#8b7cf8"
}
},
{
"name": "Variables",
"scope": ["variable", "variable.other"],
"settings": {
"foreground": "#e8f4ff"
}
},
{
"name": "JSON keys",
"scope": ["support.type.property-name.json"],
"settings": {
"foreground": "#00f5ff"
}
},
{
"name": "JSON strings",
"scope": ["string.quoted.double.json"],
"settings": {
"foreground": "#39ff14"
}
}
]
}
11 changes: 11 additions & 0 deletions lib/core/theme/builtin_theme_assets.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/// Bundled theme JSON files shipped in the Flutter asset bundle.
abstract final class BuiltinThemeAssets {
static const directory = 'assets/themes';

/// File names under [directory] that are registered as built-in themes.
static const bundledFiles = <String>[
'cyberpunk-neon.json',
];

static String assetPath(String fileName) => '$directory/$fileName';
}
4 changes: 3 additions & 1 deletion lib/core/theme/theme_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,9 @@ class ThemeController extends ChangeNotifier {
_userOverrides = const {};
_importedThemeName = null;
_themeAnimationEnabled = false;
_availableThemes = List.unmodifiable(_builtinThemeDefinitions);
_availableThemes = _mergeBuiltinThemes(
await _registryService.loadThemeDefinitions(),
);
_selectedThemeId = null;
_selectedThemePath = null;
_selectedThemeLoadError = null;
Expand Down
143 changes: 117 additions & 26 deletions lib/core/theme/theme_registry_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import 'dart:convert';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:path/path.dart' as p;

import '../storage/app_settings.dart';
import 'builtin_theme_assets.dart';
import 'parser/jsonc_preprocessor.dart';
import 'parser/querya_theme_from_manifest.dart';
import 'parser/querya_theme_from_vscode.dart';
Expand All @@ -21,17 +23,24 @@ class ThemeRegistryService {
ThemeRegistryService({
Future<Directory> Function()? userThemesDirectory,
Future<Directory> Function()? importedThemesDirectory,
Future<String> Function(String assetPath)? assetLoader,
List<String>? bundledThemeAssetFiles,
int maxCacheEntries = 16,
}) : _userThemesDirectory =
userThemesDirectory ?? ThemePaths.userThemesDirectory,
_importedThemesDirectory =
importedThemesDirectory ?? ThemePaths.importedThemesDirectory,
_assetLoader = assetLoader ?? ((path) => rootBundle.loadString(path)),
_bundledThemeAssetFiles =
bundledThemeAssetFiles ?? BuiltinThemeAssets.bundledFiles,
_themeCache = _ThemeLruCache(maxEntries: maxCacheEntries);

static const defaultMaxCacheEntries = 16;

final Future<Directory> Function() _userThemesDirectory;
final Future<Directory> Function() _importedThemesDirectory;
final Future<String> Function(String assetPath) _assetLoader;
final List<String> _bundledThemeAssetFiles;
final _ThemeLruCache _themeCache;
int _themeParseCount = 0;

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

await _loadBuiltinAssetDefinitions(definitions);

await _scanDirectory(
await _userThemesDirectory(),
ThemeSource.filesystem,
Expand Down Expand Up @@ -171,22 +182,21 @@ class ThemeRegistryService {
);
}

final file = File(path);
if (!await file.exists()) {
return ThemeLoadFailure(
definition: definition,
message: 'Theme file not found.',
);
}

final cacheKey = definition.stableCacheKey;
final cachedTheme = _themeCache.get(cacheKey);
if (cachedTheme != null) {
return ThemeLoadSuccess(definition: definition, theme: cachedTheme);
}

try {
final raw = await file.readAsString();
final raw = await _readThemeRaw(definition);
if (raw == null) {
return ThemeLoadFailure(
definition: definition,
message: 'Theme file not found.',
);
}

final theme = switch (definition.format) {
ThemeFormat.queryaCustom => _loadCustomTheme(raw),
ThemeFormat.vscode => _loadVsCodeTheme(raw),
Expand Down Expand Up @@ -305,6 +315,56 @@ class ThemeRegistryService {
return null;
}

Future<void> _loadBuiltinAssetDefinitions(List<ThemeDefinition> out) async {
for (final fileName in _bundledThemeAssetFiles) {
final assetPath = BuiltinThemeAssets.assetPath(fileName);
try {
final raw = await _readAssetString(assetPath);
final hash = _contentHash(raw);
final json = _decodeRoot(raw);
if (json == null) {
_logScanError(assetPath, 'Invalid JSON');
continue;
}

final definition = _definitionFromRaw(
json: json,
path: assetPath,
fileBaseName: p.basenameWithoutExtension(fileName),
source: ThemeSource.builtin,
contentHash: hash,
);
if (definition != null) {
out.add(definition);
}
} on Object catch (e) {
_logScanError(assetPath, e);
}
}
}

Future<String?> _readThemeRaw(ThemeDefinition definition) async {
final path = definition.path;
if (path == null || path.isEmpty) return null;

if (definition.source == ThemeSource.builtin && _isAssetPath(path)) {
try {
return await _readAssetString(path);
} on Object {
return null;
}
}

final file = File(path);
if (!await file.exists()) return null;
return file.readAsString();
}

Future<String> _readAssetString(String assetPath) =>
_assetLoader(assetPath);

static bool _isAssetPath(String path) => path.startsWith('assets/');

Future<void> _scanDirectory(
Directory directory,
ThemeSource source,
Expand Down Expand Up @@ -349,45 +409,76 @@ class ThemeRegistryService {

final schema = json['schema']?.toString();
if (schema == queryaThemeSchemaV1) {
return _customDefinition(
return _definitionFromRaw(
json: json,
file: file,
path: file.path,
fileBaseName: p.basenameWithoutExtension(file.path),
source: source,
lastModified: stat.modified,
contentHash: hash,
lastModified: stat.modified,
);
}

return _vscodeDefinition(
return _definitionFromRaw(
json: json,
file: file,
path: file.path,
fileBaseName: p.basenameWithoutExtension(file.path),
source: source,
lastModified: stat.modified,
contentHash: hash,
lastModified: stat.modified,
);
} on Object catch (e) {
_logScanError(file.path, e);
return null;
}
}

ThemeDefinition? _definitionFromRaw({
required Map<String, dynamic> json,
required String path,
required String fileBaseName,
required ThemeSource source,
required String contentHash,
DateTime? lastModified,
}) {
final schema = json['schema']?.toString();
if (schema == queryaThemeSchemaV1) {
return _customDefinition(
json: json,
source: source,
contentHash: contentHash,
path: path,
lastModified: lastModified,
);
}

return _vscodeDefinition(
json: json,
source: source,
contentHash: contentHash,
fileBaseName: fileBaseName,
path: path,
lastModified: lastModified,
);
}

ThemeDefinition? _customDefinition({
required Map<String, dynamic> json,
required File file,
required ThemeSource source,
required DateTime lastModified,
required String contentHash,
required String path,
DateTime? lastModified,
}) {
final id = json['id']?.toString().trim();
final name = json['name']?.toString().trim();
final type = json['type']?.toString().trim().toLowerCase();

if (id == null || id.isEmpty || name == null || name.isEmpty) {
_logScanError(file.path, 'Missing required custom theme fields');
_logScanError(path, 'Missing required custom theme fields');
return null;
}
if (type != 'dark' && type != 'light') {
_logScanError(file.path, 'Invalid custom theme type "$type"');
_logScanError(path, 'Invalid custom theme type "$type"');
return null;
}

Expand All @@ -397,31 +488,31 @@ class ThemeRegistryService {
source: source,
format: ThemeFormat.queryaCustom,
isDark: type == 'dark',
path: file.path,
path: path,
lastModified: lastModified,
contentHash: contentHash,
);
}

ThemeDefinition? _vscodeDefinition({
required Map<String, dynamic> json,
required File file,
required ThemeSource source,
required DateTime lastModified,
required String contentHash,
required String fileBaseName,
required String path,
DateTime? lastModified,
}) {
final fileId = p.basenameWithoutExtension(file.path);
final rawName = json['name']?.toString().trim();
final name = rawName != null && rawName.isNotEmpty ? rawName : fileId;
final name = rawName != null && rawName.isNotEmpty ? rawName : fileBaseName;
final type = json['type']?.toString().trim().toLowerCase();

return ThemeDefinition(
id: fileId,
id: fileBaseName,
name: name,
source: source,
format: ThemeFormat.vscode,
isDark: type == 'dark',
path: file.path,
path: path,
lastModified: lastModified,
contentHash: contentHash,
);
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ flutter:
uses-material-design: true
assets:
- assets/images/
- assets/themes/
Loading
Loading