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
188 changes: 181 additions & 7 deletions lib/core/theme/theme_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@ import 'parser/vscode_colors_merge.dart';
import 'parser/vscode_theme_manifest.dart';
import 'querya_theme.dart';
import 'querya_theme_preset.dart';
import 'theme_definition.dart';
import 'theme_import_service.dart';
import 'theme_load_result.dart';
import 'theme_registry_service.dart';

/// Active theme state: preset, optional imported colors, user overrides.
class ThemeController extends ChangeNotifier {
ThemeController._();
ThemeRegistryService _registryService;

ThemeController._({ThemeRegistryService? registryService})
: _registryService = registryService ?? ThemeRegistryService();

static final ThemeController instance = ThemeController._();

Expand All @@ -27,6 +33,13 @@ class ThemeController extends ChangeNotifier {
bool _loaded = false;
bool _themeAnimationEnabled = false;

List<ThemeDefinition> _availableThemes = const [];
String? _selectedThemeId;
String? _selectedThemePath;
String? _selectedThemeLoadError;
QueryaTheme? _registryTheme;
bool _registrySelectionFailed = false;

QueryaTheme? _cachedLightTheme;
QueryaTheme? _cachedDarkTheme;
QueryaTheme? _cachedActiveTheme;
Expand All @@ -48,6 +61,14 @@ class ThemeController extends ChangeNotifier {

String? get importedThemeName => _importedThemeName;

List<ThemeDefinition> get availableThemes => List.unmodifiable(_availableThemes);

String? get selectedThemeId => _selectedThemeId;

String? get selectedThemePath => _selectedThemePath;

String? get selectedThemeLoadError => _selectedThemeLoadError;

/// User `workbench.colorCustomizations` layer (VS Code keys → hex).
Map<String, String> get userColorOverrides =>
Map.unmodifiable(_userOverrides);
Expand Down Expand Up @@ -81,15 +102,13 @@ class ThemeController extends ChangeNotifier {

/// Workbench + editor tokens for the current preset/mode and overrides.
QueryaTheme get activeTheme =>
_cachedActiveTheme ??= _themeForBrightness(_effectiveBrightness());
_resolvedThemeForBrightness(_effectiveBrightness());

ThemeData get lightShadcnTheme => _cachedLightShadcnTheme ??=
(_cachedLightTheme ??= _themeForBrightness(Brightness.light))
.toShadcnThemeData();
_resolvedThemeForBrightness(Brightness.light).toShadcnThemeData();

ThemeData get darkShadcnTheme => _cachedDarkShadcnTheme ??=
(_cachedDarkTheme ??= _themeForBrightness(Brightness.dark))
.toShadcnThemeData();
_resolvedThemeForBrightness(Brightness.dark).toShadcnThemeData();

/// Cached Material theme for dialogs/dropdowns (avoids rebuild churn).
material.ThemeData materialThemeFor(ColorScheme scheme) {
Expand All @@ -101,6 +120,14 @@ class ThemeController extends ChangeNotifier {
return _cachedMaterialTheme = materialThemeFromQuerya(scheme);
}

@visibleForTesting
void setRegistryServiceForTest(ThemeRegistryService service) {
_registryService = service;
}

@visibleForTesting
ThemeRegistryService get registryServiceForTest => _registryService;

void _invalidateThemeCache() {
_cachedLightTheme = null;
_cachedDarkTheme = null;
Expand Down Expand Up @@ -143,10 +170,66 @@ class ThemeController extends ChangeNotifier {
_importedColors = Map.unmodifiable(imported);
_themeAnimationEnabled =
await AppSettings.instance.getThemeAnimationEnabled();

_availableThemes = await _registryService.loadThemeDefinitions();
await _restoreSelectedRegistryTheme();

_loaded = true;
_notifyThemeChanged();
}

Future<void> loadAvailableThemes() async {
_availableThemes = await _registryService.loadThemeDefinitions();
notifyListeners();
}

Future<void> setThemeById(String id) async {
final definition = _definitionById(id);
if (definition == null) {
_selectedThemeLoadError = 'Theme "$id" not found.';
notifyListeners();
return;
}

final result = await _registryService.loadTheme(definition);
switch (result) {
case ThemeLoadSuccess(:final theme, :final definition):
_registryTheme = theme;
_registrySelectionFailed = false;
_selectedThemeId = definition.id;
_selectedThemePath = definition.path;
_selectedThemeLoadError = null;
_themeMode = theme.brightness == Brightness.light
? ThemeMode.light
: ThemeMode.dark;
await AppSettings.instance.setSelectedThemeId(definition.id);
await AppSettings.instance.setSelectedThemeSource(definition.source.name);
await AppSettings.instance.setSelectedThemePath(definition.path);
await AppSettings.instance.setThemeMode(_themeMode);
_notifyThemeChanged();
case ThemeLoadFailure(:final message):
_selectedThemeLoadError = message;
notifyListeners();
}
}

Future<ThemeLoadResult> previewThemeById(String id) async {
final definition = _definitionById(id);
if (definition == null) {
return ThemeLoadFailure(
definition: ThemeDefinition(
id: id,
name: id,
source: ThemeSource.builtin,
format: ThemeFormat.queryaCustom,
isDark: true,
),
message: 'Theme "$id" not found.',
);
}
return _registryService.loadTheme(definition);
}

Future<void> setThemeAnimationEnabled(bool enabled) async {
_themeAnimationEnabled = enabled;
await AppSettings.instance.setThemeAnimationEnabled(enabled);
Expand All @@ -155,7 +238,7 @@ class ThemeController extends ChangeNotifier {

Future<void> setThemeMode(ThemeMode mode) async {
_themeMode = mode;
if (_preset != QueryaThemePreset.imported) {
if (_registryTheme == null && _preset != QueryaThemePreset.imported) {
_preset = mode == ThemeMode.light
? QueryaThemePreset.queryaLight
: QueryaThemePreset.queryaDark;
Expand All @@ -169,6 +252,7 @@ class ThemeController extends ChangeNotifier {
if (preset == QueryaThemePreset.imported && !hasImportedTheme) {
return;
}
await _clearRegistrySelection();
_preset = preset;
if (preset == QueryaThemePreset.imported) {
await AppSettings.instance.setThemePreset(preset);
Expand All @@ -193,6 +277,7 @@ class ThemeController extends ChangeNotifier {
:final tokenColors,
:final storedPath,
):
await _clearRegistrySelection();
_importedColors = Map.unmodifiable(colors);
_importedTokenColors = List.unmodifiable(tokenColors);
_importedThemeName = name;
Expand All @@ -203,6 +288,7 @@ class ThemeController extends ChangeNotifier {
await AppSettings.instance.setThemeImportPath(storedPath);
await AppSettings.instance.setThemePreset(QueryaThemePreset.imported);
await AppSettings.instance.setThemeMode(_themeMode);
_availableThemes = await _registryService.loadThemeDefinitions();
_notifyThemeChanged();
return result;
case ThemeImportFailure():
Expand Down Expand Up @@ -238,11 +324,13 @@ class ThemeController extends ChangeNotifier {
_importedTokenColors = const [];
_importedThemeName = null;
if (_preset == QueryaThemePreset.imported) {
await _clearRegistrySelection();
_preset = QueryaThemePreset.queryaDark;
_themeMode = ThemeMode.dark;
await AppSettings.instance.setThemePreset(_preset);
await AppSettings.instance.setThemeMode(_themeMode);
}
_availableThemes = await _registryService.loadThemeDefinitions();
_notifyThemeChanged();
}

Expand All @@ -256,9 +344,95 @@ class ThemeController extends ChangeNotifier {
_userOverrides = const {};
_importedThemeName = null;
_themeAnimationEnabled = false;
_availableThemes = const [];
_selectedThemeId = null;
_selectedThemePath = null;
_selectedThemeLoadError = null;
_registryTheme = null;
_registrySelectionFailed = false;
_notifyThemeChanged();
}

Future<void> _restoreSelectedRegistryTheme() async {
_selectedThemeId = await AppSettings.instance.getSelectedThemeId();
_selectedThemePath = await AppSettings.instance.getSelectedThemePath();
final selectedSource = await AppSettings.instance.getSelectedThemeSource();
_selectedThemeLoadError = null;
_registryTheme = null;
_registrySelectionFailed = false;

if (_selectedThemeId == null) return;

final definition = _definitionById(
_selectedThemeId!,
source: selectedSource,
path: _selectedThemePath,
);
if (definition == null) {
_registrySelectionFailed = true;
_selectedThemeLoadError =
'Selected theme "${_selectedThemeId!}" is not available.';
return;
}

final result = await _registryService.loadTheme(definition);
switch (result) {
case ThemeLoadSuccess(:final theme, :final definition):
_registryTheme = theme;
_selectedThemeId = definition.id;
_selectedThemePath = definition.path;
_themeMode = theme.brightness == Brightness.light
? ThemeMode.light
: ThemeMode.dark;
case ThemeLoadFailure(:final message):
_registrySelectionFailed = true;
_selectedThemeLoadError = message;
}
}

Future<void> _clearRegistrySelection() async {
_registryTheme = null;
_registrySelectionFailed = false;
_selectedThemeId = null;
_selectedThemePath = null;
_selectedThemeLoadError = null;
await AppSettings.instance.clearSelectedThemeRegistry();
}

ThemeDefinition? _definitionById(
String id, {
String? source,
String? path,
}) {
final matches = _availableThemes.where((definition) => definition.id == id);
if (matches.isEmpty) return null;

if (source != null && source.isNotEmpty) {
final bySource =
matches.where((definition) => definition.source.name == source);
if (bySource.isNotEmpty) return bySource.first;
}
if (path != null && path.isNotEmpty) {
final byPath = matches.where((definition) => definition.path == path);
if (byPath.isNotEmpty) return byPath.first;
}
return matches.first;
}

QueryaTheme _resolvedThemeForBrightness(Brightness brightness) {
if (_registryTheme != null) return _registryTheme!;
if (_registrySelectionFailed && _selectedThemeId != null) {
return QueryaTheme.darkDefault;
}
if (brightness == Brightness.light) {
return _cachedLightTheme ??= _themeForBrightness(Brightness.light);
}
if (brightness == Brightness.dark) {
return _cachedDarkTheme ??= _themeForBrightness(Brightness.dark);
}
return _cachedActiveTheme ??= _themeForBrightness(brightness);
}

Brightness _effectiveBrightness() {
if (_themeMode == ThemeMode.system) {
final b = WidgetsBinding.instance.platformDispatcher.platformBrightness;
Expand Down
Loading
Loading