From 2fc1d521471e8319e22a7d31f3556f2e03512e44 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Fri, 9 Oct 2026 12:58:07 +0300 Subject: [PATCH 1/3] feat(export): light palette by default for chart and ERD SVG, setting for the current theme (#1163) Chart and ERD exports followed the current theme inconsistently: the chart SVG took the workbench colours, the ERD SVG always used light literals. Exports now use one light palette for documents by default. An export-theme setting, off by default, switches both to the colours of the current theme; the Export menus of the chart and the diagram offer the toggle. --- lib/core/storage/app_settings.dart | 17 +++++ lib/features/erd/erd_view.dart | 41 ++++++++++-- lib/features/results/charts/chart_svg.dart | 9 +++ .../results/charts/quick_chart_view.dart | 62 ++++++++++++++----- test/features/erd/erd_test.dart | 10 ++- 5 files changed, 115 insertions(+), 24 deletions(-) diff --git a/lib/core/storage/app_settings.dart b/lib/core/storage/app_settings.dart index 3871178f..8937bf36 100644 --- a/lib/core/storage/app_settings.dart +++ b/lib/core/storage/app_settings.dart @@ -110,6 +110,7 @@ abstract final class AppSettingsKeys { static const sqliteSqlStmtTimeoutSeconds = 'sqlite_sql_stmt_timeout_seconds'; static const sqlResultMaxRows = 'sql_result_max_rows'; static const sqlEditorFontSizePoints = 'sql_editor_font_size_points'; + static const exportCurrentTheme = 'export_current_theme'; static const sqlHistoryMaxEntries = 'sql_history_max_entries'; static const themeMode = 'theme_mode'; static const themePreset = 'theme_preset'; @@ -317,6 +318,22 @@ class AppSettings { ); } + /// Chart and diagram exports. False (default): a light palette for documents. + /// True: the colours of the current theme. + Future getExportCurrentTheme() async { + final value = await LocalDb.instance.getAppSetting( + AppSettingsKeys.exportCurrentTheme, + ); + return value == '1' || value?.toLowerCase() == 'true'; + } + + Future setExportCurrentTheme(bool value) async { + await LocalDb.instance.setAppSetting( + AppSettingsKeys.exportCurrentTheme, + value ? '1' : '0', + ); + } + Future getSidebarVisible() async { final value = await LocalDb.instance.getAppSetting( AppSettingsKeys.sidebarVisible, diff --git a/lib/features/erd/erd_view.dart b/lib/features/erd/erd_view.dart index da183d85..28e22ee8 100644 --- a/lib/features/erd/erd_view.dart +++ b/lib/features/erd/erd_view.dart @@ -20,6 +20,7 @@ import 'package:querya_desktop/features/erd/erd_geometry.dart'; import 'package:querya_desktop/features/erd/erd_layout.dart'; import 'package:querya_desktop/features/erd/erd_model.dart'; import 'package:querya_desktop/features/erd/erd_router.dart'; +import 'package:querya_desktop/core/storage/app_settings.dart'; import 'package:querya_desktop/shared/widgets/app_toast.dart'; import 'package:querya_desktop/shared/widgets/querya_action_menu.dart'; import 'package:querya_desktop/shared/widgets/querya_empty_state.dart'; @@ -46,7 +47,7 @@ Future defaultErdFileSaver(String name, Uint8List bytes) async { /// Interactive entity-relationship diagram of the connected database. /// Entries of the diagram's Export menu. -enum _ExportAction { mermaid, svg, png, copyMermaid } +enum _ExportAction { mermaid, svg, png, copyMermaid, toggleTheme } class ErdView extends material.StatefulWidget { const ErdView({ @@ -523,6 +524,31 @@ class _ErdViewState extends material.State { } } + /// The SVG export: the light palette for documents, unless the setting asks + /// for the current theme. + Future _exportSvg(ErdSchema schema, ErdLayout layout) async { + final currentTheme = await AppSettings.instance.getExportCurrentTheme(); + if (!mounted) return; + final colors = currentTheme ? _svgColors() : const ErdSvgColors(); + await _save( + '$_fileStem.svg', + Uint8List.fromList(utf8.encode(ErdExport.toSvg(schema, layout, + routes: _routes, colors: colors)))); + } + + /// Flips between the light export palette and the current theme. + Future _toggleExportTheme() async { + final now = !await AppSettings.instance.getExportCurrentTheme(); + await AppSettings.instance.setExportCurrentTheme(now); + if (!mounted) return; + showAppToast( + context: context, + message: now + ? 'Exports use the current theme' + : 'Exports use the light palette for documents', + ); + } + /// Colours of the current theme for the SVG export, as the screen draws the /// cards and edges. ErdSvgColors _svgColors() { @@ -554,13 +580,11 @@ class _ErdViewState extends material.State { '$_fileStem.mmd', Uint8List.fromList(utf8.encode(ErdExport.toMermaid(schema)))); case _ExportAction.svg: - _save( - '$_fileStem.svg', - Uint8List.fromList(utf8.encode( - ErdExport.toSvg(schema, layout, - routes: _routes, colors: _svgColors())))); + unawaited(_exportSvg(schema, layout)); case _ExportAction.png: unawaited(_exportPng()); + case _ExportAction.toggleTheme: + unawaited(_toggleExportTheme()); case _ExportAction.copyMermaid: Clipboard.setData(ClipboardData(text: ErdExport.toMermaid(schema))); showAppToast( @@ -963,6 +987,11 @@ class _ErdViewState extends material.State { opacity: ready ? 1 : 0.5, child: QueryaActionMenu<_ExportAction>( items: const [ + QueryaActionMenuItem( + value: _ExportAction.toggleTheme, + label: 'Toggle export theme (light / current)', + icon: material.Icons.palette_outlined, + ), QueryaActionMenuItem( value: _ExportAction.mermaid, label: 'Mermaid (.mmd)', diff --git a/lib/features/results/charts/chart_svg.dart b/lib/features/results/charts/chart_svg.dart index 54d811aa..50bdeedc 100644 --- a/lib/features/results/charts/chart_svg.dart +++ b/lib/features/results/charts/chart_svg.dart @@ -27,6 +27,15 @@ class ChartSvgColors { class ChartSvg { ChartSvg._(); + /// Light palette for documents: white background, dark text, and a series + /// palette that stays readable on white. + static const ChartSvgColors light = ChartSvgColors( + background: '#ffffff', + text: '#334155', + grid: '#e2e8f0', + series: ['#2563eb', '#0d9488', '#d97706', '#7c3aed', '#dc2626'], + ); + static const double width = 960; static const double height = 540; static const double _left = 64; diff --git a/lib/features/results/charts/quick_chart_view.dart b/lib/features/results/charts/quick_chart_view.dart index ef171eef..ef52543d 100644 --- a/lib/features/results/charts/quick_chart_view.dart +++ b/lib/features/results/charts/quick_chart_view.dart @@ -1,3 +1,4 @@ +import 'dart:async' show unawaited; import 'dart:convert' show utf8; import 'dart:math' show min, pi; import 'dart:typed_data'; @@ -7,14 +8,16 @@ import 'package:file_selector/file_selector.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart' as material; import 'package:flutter/rendering.dart'; +import 'package:querya_desktop/core/storage/app_settings.dart'; import 'package:querya_desktop/features/results/charts/chart_data.dart'; import 'package:querya_desktop/features/results/charts/chart_format.dart'; import 'package:querya_desktop/features/results/charts/chart_svg.dart'; +import 'package:querya_desktop/shared/widgets/app_toast.dart'; import 'package:querya_desktop/shared/widgets/querya_action_menu.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; /// Formats of the chart export menu. -enum _ChartExport { png, svg } +enum _ChartExport { png, svg, toggleTheme } /// Saves rendered chart PNG bytes; replaceable in tests. typedef ChartPngSaver = Future Function(Uint8List png); @@ -204,31 +207,49 @@ class _QuickChartViewState extends material.State { Future _exportSvg() async { final valueCol = _valueCol; if (valueCol == null) return; + final currentTheme = await AppSettings.instance.getExportCurrentTheme(); + if (!mounted) return; final palette = context.semanticPalette; final wb = context.workbench; final svg = ChartSvg.build( points: _series(valueCol).points, type: _type, title: _chartTitle(), - colors: ChartSvgColors( - background: ChartSvgColors.hex(wb.surface.toARGB32()), - text: ChartSvgColors.hex(wb.mutedForeground.toARGB32()), - grid: ChartSvgColors.hex(wb.borderSubtle.toARGB32()), - series: [ - for (final c in [ - palette.type1, - palette.type2, - palette.type3, - palette.type4, - palette.type5, - ]) - ChartSvgColors.hex(c.toARGB32()), - ], - ), + colors: currentTheme + ? ChartSvgColors( + background: ChartSvgColors.hex(wb.surface.toARGB32()), + text: ChartSvgColors.hex(wb.mutedForeground.toARGB32()), + grid: ChartSvgColors.hex(wb.borderSubtle.toARGB32()), + series: [ + for (final c in [ + palette.type1, + palette.type2, + palette.type3, + palette.type4, + palette.type5, + ]) + ChartSvgColors.hex(c.toARGB32()), + ], + ) + : ChartSvg.light, ); await (widget.onSaveSvg ?? _defaultSvgSaver)(svg); } + /// Flips between the light export palette and the current theme, and says + /// which one is in use. + Future _toggleExportTheme() async { + final now = !await AppSettings.instance.getExportCurrentTheme(); + await AppSettings.instance.setExportCurrentTheme(now); + if (!mounted) return; + showAppToast( + context: context, + message: now + ? 'Exports use the current theme' + : 'Exports use the light palette for documents', + ); + } + @override material.Widget build(material.BuildContext context) { final wb = context.workbench; @@ -356,9 +377,16 @@ class _QuickChartViewState extends material.State { label: 'SVG', icon: material.Icons.polyline_outlined, ), + QueryaActionMenuItem( + value: _ChartExport.toggleTheme, + label: 'Toggle export theme (light / current)', + icon: material.Icons.palette_outlined, + ), ], onSelected: (format) { - if (format == _ChartExport.png) { + if (format == _ChartExport.toggleTheme) { + unawaited(_toggleExportTheme()); + } else if (format == _ChartExport.png) { _export(); } else { _exportSvg(); diff --git a/test/features/erd/erd_test.dart b/test/features/erd/erd_test.dart index c9aafa8f..32a7b46b 100644 --- a/test/features/erd/erd_test.dart +++ b/test/features/erd/erd_test.dart @@ -588,8 +588,16 @@ void main() { await t.tap(find.byKey(const material.ValueKey('erd_export'))); await t.pump(); await t.tap(find.text('SVG')); - await t.pump(); + // The export reads its setting first, which is real I/O. + for (var i = 0; i < 40 && !saved.containsKey('erd.svg'); i++) { + await t.runAsync( + () => Future.delayed(const Duration(milliseconds: 50))); + await t.pump(); + } expect(utf8.decode(saved['erd.svg']!), contains('')); final card = find.byKey(const material.ValueKey('erd_table_users')); await t.tap(card); From 60108d3c726f0d6bf25252dfb7879982441b471b Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Fri, 9 Oct 2026 13:01:41 +0300 Subject: [PATCH 2/3] fix(export): unreadable export setting falls back to the light palette; drop an unused import (#1163) Reading the setting touches the local database; without one (tests, a damaged store) the export must still run, so it uses the light default. The chart file imported app_toast next to widgets, which already provides it. --- lib/core/storage/app_settings.dart | 13 +++++++++---- lib/features/results/charts/quick_chart_view.dart | 1 - .../results/charts/quick_chart_view_test.dart | 7 ++++++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/lib/core/storage/app_settings.dart b/lib/core/storage/app_settings.dart index 8937bf36..7e2f6300 100644 --- a/lib/core/storage/app_settings.dart +++ b/lib/core/storage/app_settings.dart @@ -321,10 +321,15 @@ class AppSettings { /// Chart and diagram exports. False (default): a light palette for documents. /// True: the colours of the current theme. Future getExportCurrentTheme() async { - final value = await LocalDb.instance.getAppSetting( - AppSettingsKeys.exportCurrentTheme, - ); - return value == '1' || value?.toLowerCase() == 'true'; + try { + final value = await LocalDb.instance.getAppSetting( + AppSettingsKeys.exportCurrentTheme, + ); + return value == '1' || value?.toLowerCase() == 'true'; + } catch (_) { + // No readable store (tests, a damaged database): export the default. + return false; + } } Future setExportCurrentTheme(bool value) async { diff --git a/lib/features/results/charts/quick_chart_view.dart b/lib/features/results/charts/quick_chart_view.dart index ef52543d..7965b9ac 100644 --- a/lib/features/results/charts/quick_chart_view.dart +++ b/lib/features/results/charts/quick_chart_view.dart @@ -12,7 +12,6 @@ import 'package:querya_desktop/core/storage/app_settings.dart'; import 'package:querya_desktop/features/results/charts/chart_data.dart'; import 'package:querya_desktop/features/results/charts/chart_format.dart'; import 'package:querya_desktop/features/results/charts/chart_svg.dart'; -import 'package:querya_desktop/shared/widgets/app_toast.dart'; import 'package:querya_desktop/shared/widgets/querya_action_menu.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; diff --git a/test/features/results/charts/quick_chart_view_test.dart b/test/features/results/charts/quick_chart_view_test.dart index 10636912..6d25af72 100644 --- a/test/features/results/charts/quick_chart_view_test.dart +++ b/test/features/results/charts/quick_chart_view_test.dart @@ -112,7 +112,12 @@ void main() { await t.tap(find.byKey(const material.ValueKey('chart_export'))); await t.pumpAndSettle(); await t.tap(find.text('SVG')); - await t.pump(); + // The export reads its setting first, which is real I/O. + for (var i = 0; i < 40 && saved == null; i++) { + await t.runAsync( + () => Future.delayed(const Duration(milliseconds: 50))); + await t.pump(); + } expect(saved, isNotNull); expect(saved, contains('amount by name')); From 91e26e0ce662210400350d229b091f0cf2d65946 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Fri, 9 Oct 2026 13:08:24 +0300 Subject: [PATCH 3/3] test(charts): wait for the pie SVG export too (#1163) The second SVG export of the test reads the setting as well, so one pump was not enough for it to be saved. --- test/features/results/charts/quick_chart_view_test.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/features/results/charts/quick_chart_view_test.dart b/test/features/results/charts/quick_chart_view_test.dart index 6d25af72..a0abd544 100644 --- a/test/features/results/charts/quick_chart_view_test.dart +++ b/test/features/results/charts/quick_chart_view_test.dart @@ -127,8 +127,13 @@ void main() { await t.pump(); await t.tap(find.byKey(const material.ValueKey('chart_export'))); await t.pumpAndSettle(); + saved = null; await t.tap(find.text('SVG')); - await t.pump(); + for (var i = 0; i < 40 && saved == null; i++) { + await t.runAsync( + () => Future.delayed(const Duration(milliseconds: 50))); + await t.pump(); + } expect('