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
62 changes: 37 additions & 25 deletions lib/app/app.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:querya_desktop/core/layout/ui_scale.dart';
import 'package:querya_desktop/core/layout/ui_scale_controller.dart';
import 'package:querya_desktop/core/motion/querya_motion_controller.dart';
import 'package:querya_desktop/core/motion/querya_motion_scope.dart';
import 'package:querya_desktop/core/theme/querya_theme_scope.dart';
import 'package:querya_desktop/core/theme/theme_controller.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
Expand All @@ -14,6 +16,7 @@ class QueryaApp extends StatelessWidget {
Widget build(BuildContext context) {
final themeController = ThemeController.instance;
final uiScaleController = UiScaleController.instance;
final motionController = QueryaMotionController.instance;

return ListenableBuilder(
listenable: themeController,
Expand All @@ -25,34 +28,43 @@ class QueryaApp extends StatelessWidget {
listenable: uiScaleController,
builder: (context, _) {
final scale = uiScaleController.scale;
return ShadcnApp(
title: 'Querya',
theme: themeController.lightShadcnTheme,
darkTheme: themeController.darkShadcnTheme,
themeMode: themeController.themeMode,
materialTheme: themeController.materialThemeFor(colorScheme),
debugShowCheckedModeBanner: false,
enableThemeAnimation: themeController.themeAnimationEnabled,
enableScrollInterception: false,
// Above navigator so dialogs/overlays (SQL editor, Preferences) see tokens.
builder: (context, child) {
final mq = MediaQuery.maybeOf(context);
return QueryaUiScaleScope(
scale: scale,
child: MediaQuery(
data: (mq ?? const MediaQueryData()).copyWith(
textScaler: TextScaler.linear(scale),
),
child: QueryaThemeScope(
data: queryaTheme,
child: child ?? const SizedBox.shrink(),
),
return ListenableBuilder(
listenable: motionController,
builder: (context, _) {
final motionLevel = motionController.level;
return ShadcnApp(
title: 'Querya',
theme: themeController.lightShadcnTheme,
darkTheme: themeController.darkShadcnTheme,
themeMode: themeController.themeMode,
materialTheme: themeController.materialThemeFor(colorScheme),
debugShowCheckedModeBanner: false,
enableThemeAnimation: themeController.themeAnimationEnabled,
enableScrollInterception: false,
// Above navigator so dialogs/overlays (SQL editor, Preferences) see tokens.
builder: (context, child) {
final mq = MediaQuery.maybeOf(context);
return QueryaUiScaleScope(
scale: scale,
child: MediaQuery(
data: (mq ?? const MediaQueryData()).copyWith(
textScaler: TextScaler.linear(scale),
),
child: QueryaThemeScope(
data: queryaTheme,
child: QueryaMotionScope(
level: motionLevel,
child: child ?? const SizedBox.shrink(),
),
),
),
);
},
home: const AppLifecycleCleanup(
child: MainScreen(),
),
);
},
home: const AppLifecycleCleanup(
child: MainScreen(),
),
);
},
);
Expand Down
24 changes: 24 additions & 0 deletions lib/core/motion/querya_motion_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:flutter/foundation.dart';
import 'package:querya_desktop/core/storage/app_settings.dart';
import 'querya_motion_scope.dart';

/// Loads and broadcasts [AppSettings] motion level for the widget tree.
class QueryaMotionController extends ChangeNotifier {
QueryaMotionController._();
static final QueryaMotionController instance = QueryaMotionController._();

QueryaMotionLevel _level = QueryaMotionLevel.full;
QueryaMotionLevel get level => _level;

Future<void> load() async {
_level = await AppSettings.instance.getMotionLevel();
notifyListeners();
}

Future<void> setLevel(QueryaMotionLevel value) async {
if (_level == value) return;
await AppSettings.instance.setMotionLevel(value);
_level = value;
notifyListeners();
}
}
21 changes: 21 additions & 0 deletions lib/core/storage/app_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:convert';

import 'package:shadcn_flutter/shadcn_flutter.dart';

import '../motion/querya_motion_scope.dart';
import '../theme/querya_theme_preset.dart';
import 'local_db.dart';

Expand Down Expand Up @@ -111,6 +112,7 @@ abstract final class AppSettingsKeys {
static const themeSelectedPath = 'theme_selected_path';
static const themeAnimationEnabled = 'theme_animation_enabled';
static const uiScale = 'ui_scale';
static const motionLevel = 'motion_level';
}

/// Bumps [listenable] when any preference is persisted (theme, legacy listeners).
Expand Down Expand Up @@ -524,4 +526,23 @@ class AppSettings {
await deleteThemeImportKeys();
AppSettingsRevision.bump();
}

Future<QueryaMotionLevel> getMotionLevel() async {
final v = await LocalDb.instance.getAppSetting(AppSettingsKeys.motionLevel);
return switch (v) {
'off' => QueryaMotionLevel.off,
'reduced' => QueryaMotionLevel.reduced,
_ => QueryaMotionLevel.full,
};
}

Future<void> setMotionLevel(QueryaMotionLevel level) async {
final stored = switch (level) {
QueryaMotionLevel.off => 'off',
QueryaMotionLevel.reduced => 'reduced',
QueryaMotionLevel.full => 'full',
};
await LocalDb.instance.setAppSetting(AppSettingsKeys.motionLevel, stored);
AppSettingsRevision.bump();
}
}
36 changes: 36 additions & 0 deletions lib/features/settings/preferences_appearance_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import 'dart:async' show unawaited;

import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart' as material;
import 'package:querya_desktop/core/motion/querya_motion_controller.dart';
import 'package:querya_desktop/core/motion/querya_motion_scope.dart';
import 'package:querya_desktop/core/platform/open_directory.dart';
import 'package:querya_desktop/core/theme/theme_controller.dart';
import 'package:querya_desktop/core/theme/theme_import_service.dart';
Expand Down Expand Up @@ -257,6 +259,40 @@ class _PreferencesAppearanceSectionState
'Smooth transitions when switching dark/light or presets. Off by default for stability.',
),
const material.SizedBox(height: 12),
PreferencesFieldRow(
label: 'Motion',
control: material.ListenableBuilder(
listenable: QueryaMotionController.instance,
builder: (context, _) {
final controller = QueryaMotionController.instance;
return PreferencesDropdownMenu<QueryaMotionLevel>(
value: controller.level,
onSelected: (v) {
if (v != null) unawaited(controller.setLevel(v));
},
entries: const [
material.DropdownMenuEntry(
value: QueryaMotionLevel.full,
label: 'Full',
),
material.DropdownMenuEntry(
value: QueryaMotionLevel.reduced,
label: 'Reduced',
),
material.DropdownMenuEntry(
value: QueryaMotionLevel.off,
label: 'Off',
),
],
);
},
),
),
const material.SizedBox(height: 4),
const PreferencesHint(
'Full enables all animations. Reduced cuts durations in half. Off disables all transitions.',
),
const material.SizedBox(height: 12),
material.Wrap(
spacing: 8,
runSpacing: 8,
Expand Down
2 changes: 2 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'app/app.dart';
import 'core/editor/syntax_highlight_service.dart';
import 'core/layout/ui_scale_controller.dart';
import 'core/motion/display_refresh_service.dart';
import 'core/motion/querya_motion_controller.dart';
import 'core/storage/local_db.dart';
import 'core/theme/theme_controller.dart';

Expand All @@ -15,6 +16,7 @@ void main() async {
await SyntaxHighlightService.ensureInitialized();
await ThemeController.instance.load();
await UiScaleController.instance.load();
await QueryaMotionController.instance.load();
runApp(const QueryaApp());
doWhenWindowReady(() {
final win = appWindow;
Expand Down
4 changes: 4 additions & 0 deletions linux/flutter/generated_plugin_registrant.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <irondash_engine_context/irondash_engine_context_plugin.h>
#include <refresh_rate/refresh_rate_plugin.h>
#include <super_native_extensions/super_native_extensions_plugin.h>

void fl_register_plugins(FlPluginRegistry* registry) {
Expand All @@ -25,6 +26,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin");
irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar);
g_autoptr(FlPluginRegistrar) refresh_rate_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "RefreshRatePlugin");
refresh_rate_plugin_register_with_registrar(refresh_rate_registrar);
g_autoptr(FlPluginRegistrar) super_native_extensions_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "SuperNativeExtensionsPlugin");
super_native_extensions_plugin_register_with_registrar(super_native_extensions_registrar);
Expand Down
1 change: 1 addition & 0 deletions linux/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
flutter_secure_storage_linux
irondash_engine_context
refresh_rate
super_native_extensions
)

Expand Down
2 changes: 2 additions & 0 deletions macos/Flutter/GeneratedPluginRegistrant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import device_info_plus
import file_selector_macos
import flutter_secure_storage_macos
import irondash_engine_context
import refresh_rate
import sqflite_darwin
import super_native_extensions

Expand All @@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin"))
RefreshRatePlugin.register(with: registry.registrar(forPlugin: "RefreshRatePlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin"))
}
3 changes: 3 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ dependency_overrides:
# Patched ToastLayer (fixes InheritedNotifier crash on resize / hot reload).
shadcn_flutter:
path: third_party/shadcn_flutter
# Linux -Werror: fix uninitialized rates[0] in refresh_rate 1.0.2.
refresh_rate:
path: third_party/refresh_rate

flutter:
uses-material-design: true
Expand Down
75 changes: 75 additions & 0 deletions test/core/motion/querya_motion_controller_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import 'dart:io';

import 'package:flutter_test/flutter_test.dart';
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
import 'package:querya_desktop/core/motion/querya_motion_controller.dart';
import 'package:querya_desktop/core/motion/querya_motion_scope.dart';
import 'package:querya_desktop/core/storage/local_db.dart';

class _FakePathProvider extends PathProviderPlatform {
_FakePathProvider(this._root);
final String _root;

@override
Future<String?> getApplicationSupportPath() async => _root;

@override
Future<String?> getTemporaryPath() async => _root;

@override
Future<String?> getApplicationDocumentsPath() async => _root;
}

void main() {
TestWidgetsFlutterBinding.ensureInitialized();

late Directory tempDir;

setUpAll(() async {
tempDir = await Directory.systemTemp.createTemp('querya_motion_controller_test_');
PathProviderPlatform.instance = _FakePathProvider(tempDir.path);
await LocalDb.initFfi();
});

tearDownAll(() async {
await LocalDb.instance.close();
if (await tempDir.exists()) {
await tempDir.delete(recursive: true);
}
});

test('QueryaMotionController default/off/reduced logic', () async {
final controller = QueryaMotionController.instance;

// Initially loads default (full)
await controller.load();
expect(controller.level, QueryaMotionLevel.full);

var notifyCount = 0;
void listener() => notifyCount++;
controller.addListener(listener);

try {
// Sets and persists reduced
await controller.setLevel(QueryaMotionLevel.reduced);
expect(controller.level, QueryaMotionLevel.reduced);
expect(notifyCount, 1);

// Sets and persists off
await controller.setLevel(QueryaMotionLevel.off);
expect(controller.level, QueryaMotionLevel.off);
expect(notifyCount, 2);

// Same level does not notify
await controller.setLevel(QueryaMotionLevel.off);
expect(notifyCount, 2);

// Load from DB restores level
final anotherController = QueryaMotionController.instance;
await anotherController.load();
expect(anotherController.level, QueryaMotionLevel.off);
} finally {
controller.removeListener(listener);
}
});
}
Loading
Loading