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 .flutter-plugins-dependencies

Large diffs are not rendered by default.

26 changes: 21 additions & 5 deletions lib/app/app.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'package:querya_desktop/core/layout/ui_scale.dart';
import 'package:querya_desktop/core/layout/ui_scale_controller.dart';
import 'package:querya_desktop/core/theme/querya_material_theme.dart';
import 'package:querya_desktop/core/theme/querya_theme_scope.dart';
import 'package:querya_desktop/core/theme/theme_controller.dart';
Expand All @@ -13,11 +15,14 @@ class QueryaApp extends StatelessWidget {
Widget build(BuildContext context) {
final themeController = ThemeController.instance;

final uiScaleController = UiScaleController.instance;

return ListenableBuilder(
listenable: themeController,
listenable: Listenable.merge([themeController, uiScaleController]),
builder: (context, _) {
final queryaTheme = themeController.activeTheme;
final colorScheme = queryaTheme.colorScheme;
final scale = uiScaleController.scale;
return ShadcnApp(
title: 'Querya',
theme: themeController.lightShadcnTheme,
Expand All @@ -28,10 +33,21 @@ class QueryaApp extends StatelessWidget {
enableThemeAnimation: themeController.themeAnimationEnabled,
enableScrollInterception: false,
// Above navigator so dialogs/overlays (SQL editor, Preferences) see tokens.
builder: (context, child) => QueryaThemeScope(
data: queryaTheme,
child: child ?? const SizedBox.shrink(),
),
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(),
),
),
);
},
home: const AppLifecycleCleanup(
child: MainScreen(),
),
Expand Down
29 changes: 29 additions & 0 deletions lib/core/layout/ui_scale.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'package:flutter/widgets.dart';

/// App-wide UI scale factor (Preferences → Appearance).
class QueryaUiScaleScope extends InheritedWidget {
const QueryaUiScaleScope({
super.key,
required this.scale,
required super.child,
});

final double scale;

static double of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<QueryaUiScaleScope>()
?.scale ??
1.0;
}

@override
bool updateShouldNotify(QueryaUiScaleScope oldWidget) =>
oldWidget.scale != scale;
}

extension QueryaUiScaleContext on BuildContext {
double get uiScale => QueryaUiScaleScope.of(this);

double scaled(double logicalPixels) => logicalPixels * uiScale;
}
43 changes: 43 additions & 0 deletions lib/core/layout/ui_scale_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'package:flutter/foundation.dart';
import 'package:querya_desktop/core/storage/app_settings.dart';

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

double _scale = kDefaultUiScale;
double get scale => _scale;

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

/// Live preview while dragging the scale slider (not persisted).
void setScalePreview(double value, {bool fine = false}) {
final next = _normalize(value, fine: fine);
if (next == _scale) return;
_scale = next;
notifyListeners();
}

/// Persist scale to SQLite (called on slider release).
Future<void> commitScale(double value, {bool fine = false}) async {
await AppSettings.instance.setUiScale(value, fine: fine);
_scale = await AppSettings.instance.getUiScale();
notifyListeners();
}

Future<void> setScale(double value, {bool fine = false}) =>
commitScale(value, fine: fine);

double _normalize(double value, {required bool fine}) {
final clamped = value.clamp(kMinUiScale, kMaxUiScale);
if (fine) {
final steps = ((clamped - kMinUiScale) / kUiScaleStep).round();
return (kMinUiScale + steps * kUiScaleStep).clamp(kMinUiScale, kMaxUiScale);
}
return snapUiScaleToPreset(clamped);
}
}
71 changes: 59 additions & 12 deletions lib/core/layout/window_layout.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:math' as math;

import 'package:flutter/widgets.dart';
import 'package:querya_desktop/core/layout/ui_scale.dart';

/// Breakpoints and sizes derived from window / overlay size (desktop adaptive UI).
abstract class WindowLayout {
Expand All @@ -18,6 +19,36 @@ abstract class WindowLayout {
return (screenHeight * 0.04).clamp(12.0, 40.0);
}

/// Scaled [BoxConstraints] for modal dialogs (respects [QueryaUiScaleScope]).
static BoxConstraints dialogConstraints(
BuildContext context, {
double? maxWidth,
double? minWidth,
double? maxHeight,
double? minHeight,
}) {
return BoxConstraints(
maxWidth: maxWidth != null ? context.scaled(maxWidth) : double.infinity,
minWidth: minWidth != null ? context.scaled(minWidth) : 0,
maxHeight: maxHeight != null ? context.scaled(maxHeight) : double.infinity,
minHeight: minHeight != null ? context.scaled(minHeight) : 0,
);
}

/// Fits a base dialog dimension into the viewport, then applies UI scale.
static double scaledDialogExtent(
BuildContext context, {
required double screenExtent,
required double insetTotal,
required double baseMax,
required double baseMin,
double viewportFactor = 1.0,
}) {
final available = math.max(0.0, screenExtent - insetTotal);
final base = math.min(baseMax, math.max(baseMin, available * viewportFactor));
return math.min(context.scaled(base), available);
}

/// Use for [Dialog.insetPadding] / modal margins on small windows.
static EdgeInsets dialogSymmetricInsets(BuildContext context) {
final mq = MediaQuery.sizeOf(context);
Expand All @@ -28,15 +59,30 @@ abstract class WindowLayout {
}

/// "Select database" and similar pickers.
static double newConnectionDialogMaxWidth(double screenWidth) {
final inset = dialogHorizontalInset(screenWidth) * 2;
return math.min(740, math.max(280.0, screenWidth - inset));
static double newConnectionDialogMaxWidth(BuildContext context) {
final mq = MediaQuery.sizeOf(context);
final inset = dialogHorizontalInset(mq.width) * 2;
return scaledDialogExtent(
context,
screenExtent: mq.width,
insetTotal: inset,
baseMax: 740,
baseMin: 280,
viewportFactor: 1.0,
);
}

static double newConnectionDialogHeight(double screenHeight) {
final inset = dialogVerticalInset(screenHeight) * 2;
final h = screenHeight - inset;
return math.min(580, math.max(320.0, h * 0.78));
static double newConnectionDialogHeight(BuildContext context) {
final mq = MediaQuery.sizeOf(context);
final inset = dialogVerticalInset(mq.height) * 2;
return scaledDialogExtent(
context,
screenExtent: mq.height,
insetTotal: inset,
baseMax: 580,
baseMin: 320,
viewportFactor: 0.78,
);
}

static double newConnectionSidebarWidth(double dialogWidth) {
Expand All @@ -53,12 +99,13 @@ abstract class WindowLayout {
return 1;
}

static double dbTypeCardHeight(int crossAxisCount) {
return switch (crossAxisCount) {
4 => 144,
2 => 138,
_ => 132,
static double dbTypeCardHeight(BuildContext context, int crossAxisCount) {
final base = switch (crossAxisCount) {
4 => 144.0,
2 => 138.0,
_ => 132.0,
};
return context.scaled(base);
}

/// Empty workspace hero content max width (stays within viewport minus padding).
Expand Down
69 changes: 69 additions & 0 deletions lib/core/storage/app_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,56 @@ const List<int> kSqlResultMaxRowsPresets = [
/// Default monospace size in the SQL editor (logical pixels).
const double kDefaultSqlEditorFontSize = 13;

/// Default interface scale (1.0 = 100%).
const double kDefaultUiScale = 1.0;

/// Minimum interface scale (Telegram Desktop supports 75%).
const double kMinUiScale = 0.75;

/// Maximum interface scale (Telegram goes to 300%; 200% is enough for Querya).
const double kMaxUiScale = 2.0;

/// Slider step — 1% increments when Shift is held (fine control).
const double kUiScaleStep = 0.01;

/// Fixed tick marks on the interface scale slider (75% … 200%).
const List<double> kUiScalePresets = [
0.75,
0.85,
0.9,
1.0,
1.1,
1.25,
1.5,
1.75,
2.0,
];

int nearestUiScalePresetIndex(double scale) {
var best = 0;
var bestDist = double.infinity;
for (var i = 0; i < kUiScalePresets.length; i++) {
final dist = (kUiScalePresets[i] - scale).abs();
if (dist < bestDist) {
bestDist = dist;
best = i;
}
}
return best;
}

double snapUiScaleToPreset(double scale) =>
kUiScalePresets[nearestUiScalePresetIndex(scale)];

double _normalizeUiScaleContinuous(double value) {
final clamped = value.clamp(kMinUiScale, kMaxUiScale);
final steps = ((clamped - kMinUiScale) / kUiScaleStep).round();
return (kMinUiScale + steps * kUiScaleStep).clamp(kMinUiScale, kMaxUiScale);
}

double _normalizeUiScale(double value, {bool fine = false}) =>
fine ? _normalizeUiScaleContinuous(value) : snapUiScaleToPreset(value);

/// Default cap on stored SQL history entries per connection + database.
const int kDefaultSqlHistoryMaxEntries = 100;

Expand Down Expand Up @@ -57,6 +107,7 @@ abstract final class AppSettingsKeys {
static const themeImportName = 'theme_import_name';
static const themeImportedColorsJson = 'theme_imported_colors_json';
static const themeAnimationEnabled = 'theme_animation_enabled';
static const uiScale = 'ui_scale';
}

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

/// Global interface scale for typography and compact controls.
Future<double> getUiScale() async {
final v = await LocalDb.instance.getAppSetting(AppSettingsKeys.uiScale);
if (v == null || v.isEmpty) return kDefaultUiScale;
final n = double.tryParse(v);
if (n == null) return kDefaultUiScale;
return _normalizeUiScaleContinuous(n);
}

Future<void> setUiScale(double scale, {bool fine = false}) async {
final normalized = _normalizeUiScale(scale, fine: fine);
await LocalDb.instance.setAppSetting(
AppSettingsKeys.uiScale,
normalized.toStringAsFixed(2),
);
AppSettingsRevision.bump();
}

/// Max SQL history rows kept per connection + database (oldest trimmed).
Future<int> getSqlHistoryMaxEntries() async {
final v = await LocalDb.instance.getAppSetting(
Expand Down
6 changes: 5 additions & 1 deletion lib/features/connections/driver_manager_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ class _DriverManagerDialogContent extends material.StatelessWidget {
final theme = Theme.of(context).colorScheme;
final radius = Theme.of(context).radiusXxl;
return material.Container(
constraints: const material.BoxConstraints(maxWidth: 520, minWidth: 400),
constraints: WindowLayout.dialogConstraints(
context,
maxWidth: 520,
minWidth: 400,
),
decoration: material.BoxDecoration(
color: theme.popover,
borderRadius: material.BorderRadius.circular(radius),
Expand Down
9 changes: 4 additions & 5 deletions lib/features/connections/new_connection_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,8 @@ class _NewConnectionDialogContentState extends material.State<_NewConnectionDial
material.Widget build(material.BuildContext context) {
final theme = Theme.of(context).colorScheme;
final radius = Theme.of(context).radiusXxl;
final mq = MediaQuery.sizeOf(context);
final dialogMaxW = WindowLayout.newConnectionDialogMaxWidth(mq.width);
final dialogH = WindowLayout.newConnectionDialogHeight(mq.height);
final dialogMaxW = WindowLayout.newConnectionDialogMaxWidth(context);
final dialogH = WindowLayout.newConnectionDialogHeight(context);
final headerPadH = dialogMaxW < 420 ? 16.0 : 24.0;
final stackFilters = dialogMaxW < 520;

Expand Down Expand Up @@ -185,8 +184,8 @@ class _NewConnectionDialogContentState extends material.State<_NewConnectionDial
final innerW = math.max(0.0, constraints.maxWidth - gridPad * 2);
final crossAxisCount =
WindowLayout.dbTypeGridCrossAxisCount(innerW);
final cardHeight =
WindowLayout.dbTypeCardHeight(crossAxisCount);
final cardHeight =
WindowLayout.dbTypeCardHeight(context, crossAxisCount);
final cardWidth = crossAxisCount > 0
? (innerW - spacing * (crossAxisCount - 1)) / crossAxisCount
: innerW;
Expand Down
6 changes: 5 additions & 1 deletion lib/features/connections/new_folder_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ class _NewFolderDialogContentState extends material.State<_NewFolderDialogConten
final theme = Theme.of(context).colorScheme;
final radius = Theme.of(context).radiusXxl;
return material.Container(
constraints: const material.BoxConstraints(maxWidth: 440, minWidth: 360),
constraints: WindowLayout.dialogConstraints(
context,
maxWidth: 440,
minWidth: 360,
),
decoration: material.BoxDecoration(
color: theme.popover,
borderRadius: material.BorderRadius.circular(radius),
Expand Down
3 changes: 2 additions & 1 deletion lib/features/main_screen/sql_query_history_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ class _SqlQueryHistoryDialogContentState
final scheme = Theme.of(context).colorScheme;
final radius = Theme.of(context).radiusXxl;
return material.Container(
constraints: const material.BoxConstraints(
constraints: WindowLayout.dialogConstraints(
context,
maxWidth: 520,
minWidth: 320,
maxHeight: 440,
Expand Down
2 changes: 1 addition & 1 deletion lib/features/mongodb/mongo_database_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class _CreateMongoDBDialogContentState extends material.State<_CreateMongoDBDial
final radius = Theme.of(context).radiusXxl;

return material.Container(
constraints: const material.BoxConstraints(maxWidth: 500),
constraints: WindowLayout.dialogConstraints(context, maxWidth: 500),
decoration: material.BoxDecoration(
color: theme.popover,
borderRadius: material.BorderRadius.circular(radius),
Expand Down
6 changes: 5 additions & 1 deletion lib/features/mongodb/mongodb_connection_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,11 @@ class _MongoConnectionFormContentState extends material.State<_MongoConnectionFo
final radius = Theme.of(context).radiusXxl;

return material.Container(
constraints: const material.BoxConstraints(maxWidth: 600, maxHeight: 700),
constraints: WindowLayout.dialogConstraints(
context,
maxWidth: 600,
maxHeight: 700,
),
decoration: material.BoxDecoration(
color: theme.popover,
borderRadius: material.BorderRadius.circular(radius),
Expand Down
Loading
Loading