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
2 changes: 1 addition & 1 deletion docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Living document for planned work. Not a commitment order; adjust as priorities c
## Theme system

- **Done:** runtime themes, VS Code `colors` import, `tokenColors` syntax highlighting — see [theme.md](theme.md).
- **Later:** animated theme transitions ([#57](https://github.com/QueryaHub/Querya-Desktop/issues/57)), advanced editor (LSP / `code_forge` spike).
- **Later:** advanced editor (LSP / `code_forge` spike). Theme transitions: Preferences → **Animate theme changes** (off by default).

## Query history and favorites

Expand Down
15 changes: 14 additions & 1 deletion docs/theme.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,19 @@ See also: [theme-import.md](theme-import.md).

Run: `flutter test test/core/theme/`

## Theme transition animation

Off by default. Enable in **Preferences → Appearance → Animate theme changes** to
turn on `ShadcnApp.enableThemeAnimation`.

Manual QA (with animation enabled):

- [ ] Toggle dark / light / system — no stuck overlay or wrong brightness on dialogs
- [ ] Switch preset (Querya Dark ↔ Light, imported) — sidebars and editor chrome animate smoothly
- [ ] Open connection dialog, settings sheet, SQL history — backgrounds readable during transition
- [ ] Resize main window while toggling theme — no layout jump or transparent holes
- [ ] Import theme while animation on — editor and workbench settle to final colors

## Roadmap (Phase 2+)

| Topic | Status |
Expand All @@ -154,7 +167,7 @@ Run: `flutter test test/core/theme/`
| Preferences UI | Done |
| SQL/JSON syntax highlighting | Done |
| `tokenColors` → highlighter | Done |
| Theme transition animation | [#57](https://github.com/QueryaHub/Querya-Desktop/issues/57) |
| Theme transition animation | Preferences → **Animate theme changes** (default off) |
| `code_forge` / LSP editor | [#52](https://github.com/QueryaHub/Querya-Desktop/issues/52) |

## Related docs
Expand Down
2 changes: 1 addition & 1 deletion lib/app/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class QueryaApp extends StatelessWidget {
darkTheme: themeController.darkShadcnTheme,
themeMode: themeController.themeMode,
debugShowCheckedModeBanner: false,
enableThemeAnimation: false,
enableThemeAnimation: themeController.themeAnimationEnabled,
enableScrollInterception: false,
home: QueryaThemeScope(
data: queryaTheme,
Expand Down
25 changes: 25 additions & 0 deletions lib/core/storage/app_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ abstract final class AppSettingsKeys {
static const themeImportPath = 'theme_import_path';
static const themeImportName = 'theme_import_name';
static const themeImportedColorsJson = 'theme_imported_colors_json';
static const themeAnimationEnabled = 'theme_animation_enabled';
}

/// Bumps [listenable] when any preference is persisted so open screens can reload.
Expand Down Expand Up @@ -349,10 +350,34 @@ class AppSettings {
AppSettingsRevision.bump();
}

/// Smooth color transitions when switching theme (off by default).
Future<bool> getThemeAnimationEnabled() async {
final v = await LocalDb.instance.getAppSetting(
AppSettingsKeys.themeAnimationEnabled,
);
if (v == null || v.isEmpty) return false;
return v == 'true' || v == '1';
}

Future<void> setThemeAnimationEnabled(bool enabled) async {
if (!enabled) {
await LocalDb.instance.deleteAppSetting(
AppSettingsKeys.themeAnimationEnabled,
);
} else {
await LocalDb.instance.setAppSetting(
AppSettingsKeys.themeAnimationEnabled,
'true',
);
}
AppSettingsRevision.bump();
}

Future<void> clearThemeSettings() async {
await LocalDb.instance.deleteAppSetting(AppSettingsKeys.themeMode);
await LocalDb.instance.deleteAppSetting(AppSettingsKeys.themePreset);
await LocalDb.instance.deleteAppSetting(AppSettingsKeys.themeOverridesJson);
await LocalDb.instance.deleteAppSetting(AppSettingsKeys.themeAnimationEnabled);
await deleteThemeImportKeys();
AppSettingsRevision.bump();
}
Expand Down
13 changes: 13 additions & 0 deletions lib/core/theme/theme_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@ class ThemeController extends ChangeNotifier {
Map<String, String> _userOverrides = const {};
String? _importedThemeName;
bool _loaded = false;
bool _themeAnimationEnabled = false;

ThemeMode get themeMode => _themeMode;

/// When true, [QueryaApp] enables ShadcnAnimatedTheme transitions.
bool get themeAnimationEnabled => _themeAnimationEnabled;

QueryaThemePreset get preset => _preset;

bool get isLoaded => _loaded;
Expand Down Expand Up @@ -99,10 +103,18 @@ class ThemeController extends ChangeNotifier {
_preset = preset;
_userOverrides = Map.unmodifiable(overrides);
_importedColors = Map.unmodifiable(imported);
_themeAnimationEnabled =
await AppSettings.instance.getThemeAnimationEnabled();
_loaded = true;
notifyListeners();
}

Future<void> setThemeAnimationEnabled(bool enabled) async {
_themeAnimationEnabled = enabled;
await AppSettings.instance.setThemeAnimationEnabled(enabled);
notifyListeners();
}

Future<void> setThemeMode(ThemeMode mode) async {
_themeMode = mode;
if (_preset != QueryaThemePreset.imported) {
Expand Down Expand Up @@ -205,6 +217,7 @@ class ThemeController extends ChangeNotifier {
_importedTokenColors = const [];
_userOverrides = const {};
_importedThemeName = null;
_themeAnimationEnabled = false;
notifyListeners();
}

Expand Down
19 changes: 19 additions & 0 deletions lib/features/settings/preferences_appearance_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ class _PreferencesAppearanceSectionState
if (mounted) setState(() => _importError = null);
}

Future<void> _setThemeAnimation(bool enabled) async {
await _controller.setThemeAnimationEnabled(enabled);
}

@override
material.Widget build(material.BuildContext context) {
final c = _controller;
Expand Down Expand Up @@ -158,6 +162,21 @@ class _PreferencesAppearanceSectionState
],
),
const material.SizedBox(height: 12),
material.Row(
children: [
const Text('Animate theme changes').small(),
const material.SizedBox(width: 12),
material.Switch(
value: c.themeAnimationEnabled,
onChanged: (v) => unawaited(_setThemeAnimation(v)),
),
],
),
const material.SizedBox(height: 4),
const Text(
'Smooth transitions when switching dark/light or presets. Off by default for stability.',
).muted().xSmall(),
const material.SizedBox(height: 12),
material.Wrap(
spacing: 8,
runSpacing: 8,
Expand Down
14 changes: 14 additions & 0 deletions test/core/storage/app_settings_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,20 @@ void main() {
expect(await AppSettings.instance.getThemeMode(), ThemeMode.dark);
});

test('theme animation defaults off and roundtrip', () async {
expect(await AppSettings.instance.getThemeAnimationEnabled(), isFalse);

await AppSettings.instance.setThemeAnimationEnabled(true);
expect(await AppSettings.instance.getThemeAnimationEnabled(), isTrue);

await AppSettings.instance.setThemeAnimationEnabled(false);
expect(await AppSettings.instance.getThemeAnimationEnabled(), isFalse);

await AppSettings.instance.setThemeAnimationEnabled(true);
await AppSettings.instance.clearThemeSettings();
expect(await AppSettings.instance.getThemeAnimationEnabled(), isFalse);
});

test('theme color overrides json roundtrip', () async {
await AppSettings.instance.setThemeColorOverrides({
'sideBar.background': '#ff0000',
Expand Down
13 changes: 13 additions & 0 deletions test/core/theme/querya_theme_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,18 @@ void main() {
expect(td.brightness, Brightness.dark);
expect(td.colorScheme.primary, QueryaColors.accentCyan);
});

test('ThemeData.lerp at 0.5 matches QueryaTheme.lerp colorScheme', () {
const a = QueryaTheme.darkDefault;
const b = QueryaTheme.lightDefault;
final qaMid = QueryaTheme.lerp(a, b, 0.5);
final tdMid = ThemeData.lerp(
a.toShadcnThemeData(),
b.toShadcnThemeData(),
0.5,
);
expect(tdMid.colorScheme.primary, qaMid.colorScheme.primary);
expect(tdMid.colorScheme.background, qaMid.colorScheme.background);
});
});
}
14 changes: 14 additions & 0 deletions test/core/theme/theme_controller_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,20 @@ void main() {
expect(c.preset, QueryaThemePreset.queryaDark);
});

test('setThemeAnimationEnabled persists and reset clears', () async {
final c = ThemeController.instance;
await c.load();
expect(c.themeAnimationEnabled, isFalse);

await c.setThemeAnimationEnabled(true);
expect(c.themeAnimationEnabled, isTrue);
expect(await AppSettings.instance.getThemeAnimationEnabled(), isTrue);

await c.resetToDefaults();
expect(c.themeAnimationEnabled, isFalse);
expect(await AppSettings.instance.getThemeAnimationEnabled(), isFalse);
});

test('clearColorOverrides does not reset theme mode', () async {
final c = ThemeController.instance;
await c.setThemeMode(ThemeMode.light);
Expand Down
Loading