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
22 changes: 22 additions & 0 deletions lib/core/storage/app_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -317,6 +318,27 @@ class AppSettings {
);
}

/// Chart and diagram exports. False (default): a light palette for documents.
/// True: the colours of the current theme.
Future<bool> getExportCurrentTheme() async {
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<void> setExportCurrentTheme(bool value) async {
await LocalDb.instance.setAppSetting(
AppSettingsKeys.exportCurrentTheme,
value ? '1' : '0',
);
}

Future<bool> getSidebarVisible() async {
final value = await LocalDb.instance.getAppSetting(
AppSettingsKeys.sidebarVisible,
Expand Down
41 changes: 35 additions & 6 deletions lib/features/erd/erd_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -46,7 +47,7 @@ Future<void> 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({
Expand Down Expand Up @@ -523,6 +524,31 @@ class _ErdViewState extends material.State<ErdView> {
}
}

/// The SVG export: the light palette for documents, unless the setting asks
/// for the current theme.
Future<void> _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<void> _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() {
Expand Down Expand Up @@ -554,13 +580,11 @@ class _ErdViewState extends material.State<ErdView> {
'$_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(
Expand Down Expand Up @@ -963,6 +987,11 @@ class _ErdViewState extends material.State<ErdView> {
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)',
Expand Down
9 changes: 9 additions & 0 deletions lib/features/results/charts/chart_svg.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
61 changes: 44 additions & 17 deletions lib/features/results/charts/quick_chart_view.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:async' show unawaited;
import 'dart:convert' show utf8;
import 'dart:math' show min, pi;
import 'dart:typed_data';
Expand All @@ -7,14 +8,15 @@ 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/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<void> Function(Uint8List png);
Expand Down Expand Up @@ -204,31 +206,49 @@ class _QuickChartViewState extends material.State<QuickChartView> {
Future<void> _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<void> _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;
Expand Down Expand Up @@ -356,9 +376,16 @@ class _QuickChartViewState extends material.State<QuickChartView> {
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();
Expand Down
10 changes: 9 additions & 1 deletion test/features/erd/erd_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>.delayed(const Duration(milliseconds: 50)));
await t.pump();
}
expect(utf8.decode(saved['erd.svg']!), contains('<svg'));
// The dark test theme still exports the light palette by default.
expect(utf8.decode(saved['erd.svg']!),
contains('<rect width="100%" height="100%" fill="#ffffff"/>'));

final card = find.byKey(const material.ValueKey('erd_table_users'));
await t.tap(card);
Expand Down
14 changes: 12 additions & 2 deletions test/features/results/charts/quick_chart_view_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>.delayed(const Duration(milliseconds: 50)));
await t.pump();
}
expect(saved, isNotNull);
expect(saved, contains('<svg'));
expect(saved, contains('<title>amount by name</title>'));
Expand All @@ -122,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<void>.delayed(const Duration(milliseconds: 50)));
await t.pump();
}
expect('<path '.allMatches(saved!).length, rows.length);
});

Expand Down
Loading