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
11 changes: 11 additions & 0 deletions docs/theme-import.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ Hex strings as in VS Code: `#RRGGBB`, `#RRGGBBAA`, `#RGB`, `#RGBA` (see

Comments and trailing commas are stripped before parse (`stripJsonc`).

## Preferences UI (#43)

In **Preferences → Appearance**:

- **Theme mode** — Dark / Light / System
- **Color preset** — Querya Dark, Querya Light, or imported theme name
- **Import theme…** — pick `.json` / `.jsonc` (VS Code format)
- **Reset appearance** — clears import, overrides, returns to Querya Dark

Imported files are copied to app data (`themes/imported.json`) and survive restarts.

## User overrides (#45)

User customizations are stored as VS Code keys → hex strings in
Expand Down
95 changes: 95 additions & 0 deletions lib/core/storage/app_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ abstract final class AppSettingsKeys {
static const themeMode = 'theme_mode';
static const themePreset = 'theme_preset';
static const themeOverridesJson = 'theme_overrides_json';
static const themeImportPath = 'theme_import_path';
static const themeImportName = 'theme_import_name';
static const themeImportedColorsJson = 'theme_imported_colors_json';
}

/// Bumps [listenable] when any preference is persisted so open screens can reload.
Expand Down Expand Up @@ -201,19 +204,110 @@ class AppSettings {
final v = await LocalDb.instance.getAppSetting(AppSettingsKeys.themePreset);
return switch (v) {
'querya_light' => QueryaThemePreset.queryaLight,
'imported' => QueryaThemePreset.imported,
_ => QueryaThemePreset.queryaDark,
};
}

Future<void> setThemePreset(QueryaThemePreset preset) async {
final stored = switch (preset) {
QueryaThemePreset.queryaLight => 'querya_light',
QueryaThemePreset.imported => 'imported',
QueryaThemePreset.queryaDark => 'querya_dark',
};
await LocalDb.instance.setAppSetting(AppSettingsKeys.themePreset, stored);
AppSettingsRevision.bump();
}

Future<String?> getThemeImportName() async {
return LocalDb.instance.getAppSetting(AppSettingsKeys.themeImportName);
}

Future<void> setThemeImportName(String? name) async {
if (name == null || name.isEmpty) {
await LocalDb.instance.deleteAppSetting(AppSettingsKeys.themeImportName);
} else {
await LocalDb.instance.setAppSetting(
AppSettingsKeys.themeImportName,
name,
);
}
AppSettingsRevision.bump();
}

Future<String?> getThemeImportPath() async {
return LocalDb.instance.getAppSetting(AppSettingsKeys.themeImportPath);
}

Future<void> setThemeImportPath(String? path) async {
if (path == null || path.isEmpty) {
await LocalDb.instance.deleteAppSetting(AppSettingsKeys.themeImportPath);
} else {
await LocalDb.instance.setAppSetting(
AppSettingsKeys.themeImportPath,
path,
);
}
AppSettingsRevision.bump();
}

Future<Map<String, String>> getThemeImportedColors() async {
final v = await LocalDb.instance.getAppSetting(
AppSettingsKeys.themeImportedColorsJson,
);
if (v == null || v.isEmpty) return {};
try {
final decoded = jsonDecode(v);
if (decoded is! Map) return {};
final out = <String, String>{};
for (final entry in decoded.entries) {
final key = entry.key?.toString();
final value = entry.value?.toString();
if (key != null &&
key.isNotEmpty &&
value != null &&
value.isNotEmpty) {
out[key] = value;
}
}
return out;
} on FormatException {
return {};
}
}

Future<void> setThemeImportedColors(Map<String, String> colors) async {
if (colors.isEmpty) {
await LocalDb.instance.deleteAppSetting(
AppSettingsKeys.themeImportedColorsJson,
);
} else {
await LocalDb.instance.setAppSetting(
AppSettingsKeys.themeImportedColorsJson,
jsonEncode(colors),
);
}
AppSettingsRevision.bump();
}

Future<void> clearThemeImport() async {
await LocalDb.instance.deleteAppSetting(AppSettingsKeys.themeImportPath);
await LocalDb.instance.deleteAppSetting(AppSettingsKeys.themeImportName);
await LocalDb.instance.deleteAppSetting(
AppSettingsKeys.themeImportedColorsJson,
);
AppSettingsRevision.bump();
}

/// Clears import metadata without bumping (for batched clears).
Future<void> deleteThemeImportKeys() async {
await LocalDb.instance.deleteAppSetting(AppSettingsKeys.themeImportPath);
await LocalDb.instance.deleteAppSetting(AppSettingsKeys.themeImportName);
await LocalDb.instance.deleteAppSetting(
AppSettingsKeys.themeImportedColorsJson,
);
}

Future<Map<String, String>> getThemeColorOverrides() async {
final v =
await LocalDb.instance.getAppSetting(AppSettingsKeys.themeOverridesJson);
Expand Down Expand Up @@ -259,6 +353,7 @@ class AppSettings {
await LocalDb.instance.deleteAppSetting(AppSettingsKeys.themeMode);
await LocalDb.instance.deleteAppSetting(AppSettingsKeys.themePreset);
await LocalDb.instance.deleteAppSetting(AppSettingsKeys.themeOverridesJson);
await deleteThemeImportKeys();
AppSettingsRevision.bump();
}
}
5 changes: 4 additions & 1 deletion lib/core/theme/querya_theme_preset.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/// Built-in theme presets (imported VS Code themes — #44).
/// Built-in and imported theme presets.
enum QueryaThemePreset {
queryaDark,
queryaLight,

/// VS Code theme imported from a `.json` / `.jsonc` file (#43).
imported,
}
101 changes: 88 additions & 13 deletions lib/core/theme/theme_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'parser/querya_theme_from_vscode.dart';
import 'parser/vscode_colors_merge.dart';
import 'querya_theme.dart';
import 'querya_theme_preset.dart';
import 'theme_import_service.dart';

/// Active theme state: preset, optional imported colors, user overrides.
class ThemeController extends ChangeNotifier {
Expand All @@ -17,6 +18,7 @@ class ThemeController extends ChangeNotifier {
QueryaThemePreset _preset = QueryaThemePreset.queryaDark;
Map<String, String> _importedColors = const {};
Map<String, String> _userOverrides = const {};
String? _importedThemeName;
bool _loaded = false;

ThemeMode get themeMode => _themeMode;
Expand All @@ -25,18 +27,27 @@ class ThemeController extends ChangeNotifier {

bool get isLoaded => _loaded;

bool get hasImportedTheme => _importedColors.isNotEmpty;

String? get importedThemeName => _importedThemeName;

/// User `workbench.colorCustomizations` layer (VS Code keys → hex).
Map<String, String> get userColorOverrides =>
Map.unmodifiable(_userOverrides);

/// Imported theme `colors` layer (from file import; empty until wired).
/// Imported theme `colors` layer (from file import).
Map<String, String> get importedColors => Map.unmodifiable(_importedColors);

/// Merged VS Code color keys: imported → user overrides.
Map<String, String> get effectiveVsCodeColors => mergeVsCodeColorLayers([
/// Merged VS Code color keys for the active preset.
Map<String, String> get effectiveVsCodeColors {
if (_preset == QueryaThemePreset.imported) {
return mergeVsCodeColorLayers([
_importedColors,
_userOverrides,
]);
}
return mergeVsCodeColorLayers([_userOverrides]);
}

/// Parsed effective colors for supported VS Code keys only.
Map<String, Color> get effectiveWorkbenchColors {
Expand All @@ -62,35 +73,82 @@ class ThemeController extends ChangeNotifier {

Future<void> load() async {
final mode = await AppSettings.instance.getThemeMode();
final preset = await AppSettings.instance.getThemePreset();
var preset = await AppSettings.instance.getThemePreset();
final overrides = await AppSettings.instance.getThemeColorOverrides();
var imported = await AppSettings.instance.getThemeImportedColors();
_importedThemeName = await AppSettings.instance.getThemeImportName();

if (imported.isEmpty) {
final fromDisk = await ThemeImportService.loadPersistedColors();
if (fromDisk != null && fromDisk.isNotEmpty) {
imported = fromDisk;
await AppSettings.instance.setThemeImportedColors(imported);
}
}

if (preset == QueryaThemePreset.imported && imported.isEmpty) {
preset = QueryaThemePreset.queryaDark;
await AppSettings.instance.setThemePreset(preset);
}

_themeMode = mode;
_preset = preset;
_userOverrides = Map.unmodifiable(overrides);
_importedColors = Map.unmodifiable(imported);
_loaded = true;
notifyListeners();
}

Future<void> setThemeMode(ThemeMode mode) async {
_themeMode = mode;
_preset = mode == ThemeMode.light
? QueryaThemePreset.queryaLight
: QueryaThemePreset.queryaDark;
if (_preset != QueryaThemePreset.imported) {
_preset = mode == ThemeMode.light
? QueryaThemePreset.queryaLight
: QueryaThemePreset.queryaDark;
await AppSettings.instance.setThemePreset(_preset);
}
await AppSettings.instance.setThemeMode(mode);
await AppSettings.instance.setThemePreset(_preset);
notifyListeners();
}

Future<void> setPreset(QueryaThemePreset preset) async {
if (preset == QueryaThemePreset.imported && !hasImportedTheme) {
return;
}
_preset = preset;
_themeMode = preset == QueryaThemePreset.queryaLight
? ThemeMode.light
: ThemeMode.dark;
await AppSettings.instance.setThemePreset(preset);
await AppSettings.instance.setThemeMode(_themeMode);
if (preset == QueryaThemePreset.imported) {
await AppSettings.instance.setThemePreset(preset);
} else {
_themeMode = preset == QueryaThemePreset.queryaLight
? ThemeMode.light
: ThemeMode.dark;
await AppSettings.instance.setThemePreset(preset);
await AppSettings.instance.setThemeMode(_themeMode);
}
notifyListeners();
}

/// Parses a VS Code theme file, persists it, and activates the imported preset.
Future<ThemeImportResult> importThemeFromFile(String path) async {
final result = await ThemeImportService.importFromPath(path);
switch (result) {
case ThemeImportSuccess(:final name, :final isDark, :final colors, :final storedPath):
_importedColors = Map.unmodifiable(colors);
_importedThemeName = name;
_preset = QueryaThemePreset.imported;
_themeMode = isDark ? ThemeMode.dark : ThemeMode.light;
await AppSettings.instance.setThemeImportedColors(colors);
await AppSettings.instance.setThemeImportName(name);
await AppSettings.instance.setThemeImportPath(storedPath);
await AppSettings.instance.setThemePreset(QueryaThemePreset.imported);
await AppSettings.instance.setThemeMode(_themeMode);
notifyListeners();
return result;
case ThemeImportFailure():
return result;
}
}

/// Sets or clears a user override for a VS Code `colors` key.
Future<void> setWorkbenchColor(String vscodeKey, Color? value) async {
final next = Map<String, String>.from(_userOverrides);
Expand All @@ -111,12 +169,29 @@ class ThemeController extends ChangeNotifier {
notifyListeners();
}

/// Clears imported theme file and settings; falls back to Querya Dark.
Future<void> clearImportedTheme() async {
await ThemeImportService.deletePersistedImport();
await AppSettings.instance.clearThemeImport();
_importedColors = const {};
_importedThemeName = null;
if (_preset == QueryaThemePreset.imported) {
_preset = QueryaThemePreset.queryaDark;
_themeMode = ThemeMode.dark;
await AppSettings.instance.setThemePreset(_preset);
await AppSettings.instance.setThemeMode(_themeMode);
}
notifyListeners();
}

Future<void> resetToDefaults() async {
await ThemeImportService.deletePersistedImport();
await AppSettings.instance.clearThemeSettings();
_themeMode = ThemeMode.dark;
_preset = QueryaThemePreset.queryaDark;
_importedColors = const {};
_userOverrides = const {};
_importedThemeName = null;
notifyListeners();
}

Expand Down
Loading
Loading