diff --git a/lib/core/sdui/sdui_form_builder.dart b/lib/core/sdui/sdui_form_builder.dart index 39632c96..d218b95d 100644 --- a/lib/core/sdui/sdui_form_builder.dart +++ b/lib/core/sdui/sdui_form_builder.dart @@ -129,9 +129,8 @@ class SduiFormBuilderState extends material.State { Future _pickFile(SduiFormField field) async { final picker = widget.filePicker; - final path = picker != null - ? await picker(field) - : (await openFile())?.path; + final path = + picker != null ? await picker(field) : (await openFile())?.path; if (path == null || !mounted) return; _textControllers[field.id]?.text = path; _notifyChanged(); @@ -199,28 +198,52 @@ class SduiFormBuilderState extends material.State { ), ); case SduiFieldType.select: + final options = field.options; + final current = _selectValues[field.id] ?? + (options.isNotEmpty ? options.first.value : ''); return material.Column( crossAxisAlignment: material.CrossAxisAlignment.stretch, children: [ Text(field.label).small().semiBold(), const Gap(4), - material.DropdownButtonFormField( - initialValue: _selectValues[field.id], - items: [ - for (final opt in field.options) - material.DropdownMenuItem( - value: opt.value, - child: material.Text(opt.label), - ), - ], - onChanged: (v) { - setState(() => _selectValues[field.id] = v); - _notifyChanged(); - }, + material.FormField( + initialValue: current, validator: field.required - ? (v) => - (v == null || v.isEmpty) ? '${field.label} is required' : null + ? (v) => (v == null || v.isEmpty) + ? '${field.label} is required' + : null : null, + builder: (state) { + final value = state.value ?? current; + return material.Column( + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + QueryaDropdown( + value: value.isEmpty && options.isNotEmpty + ? options.first.value + : value, + expandToParent: true, + items: [ + for (final opt in options) + QueryaDropdownItem( + value: opt.value, + label: opt.label, + ), + ], + onSelected: (v) { + final next = v ?? value; + setState(() => _selectValues[field.id] = next); + state.didChange(next); + _notifyChanged(); + }, + ), + if (state.hasError) ...[ + const Gap(4), + Text(state.errorText!).xSmall().muted(), + ], + ], + ); + }, ), ], ); diff --git a/lib/core/sdui/sdui_form_schema.dart b/lib/core/sdui/sdui_form_schema.dart index ed6e14ed..21a3cfe1 100644 --- a/lib/core/sdui/sdui_form_schema.dart +++ b/lib/core/sdui/sdui_form_schema.dart @@ -60,7 +60,8 @@ class SduiFormField { if (item is Map) { options.add(SduiSelectOption.fromJson(item)); } else if (item is Map) { - options.add(SduiSelectOption.fromJson(Map.from(item))); + options + .add(SduiSelectOption.fromJson(Map.from(item))); } else if (item != null) { options.add(SduiSelectOption(value: '$item', label: '$item')); } diff --git a/lib/core/sdui/sdui_tree_builder.dart b/lib/core/sdui/sdui_tree_builder.dart index 8dce2670..fafc2455 100644 --- a/lib/core/sdui/sdui_tree_builder.dart +++ b/lib/core/sdui/sdui_tree_builder.dart @@ -169,18 +169,19 @@ class SduiTreeBuilderState extends material.State { physics: widget.maxHeight == null ? const material.NeverScrollableScrollPhysics() : const material.ClampingScrollPhysics(), - itemExtent: _rowExtent, + itemExtent: rows.any((r) => r.isError) ? null : _rowExtent, itemCount: rows.length, itemBuilder: (context, index) { final row = rows[index]; if (row.isError) { - return material.Padding( + return TreeLoadError( + title: 'Could not expand', + message: row.error!, + detailFontSize: 10, padding: material.EdgeInsets.only( left: 36.0 + row.depth * QueryaTreeTokens.indent, - ), - child: material.Align( - alignment: material.Alignment.centerLeft, - child: Text(row.error!).muted().xSmall(), + top: 2, + bottom: 2, ), ); } @@ -213,7 +214,7 @@ class SduiTreeBuilderState extends material.State { final rowLeft = 8.0 + depth * QueryaTreeTokens.indent + (canExpand ? 0 : 4.0); - return material.InkWell( + final row = material.InkWell( onTap: () { if (isBrowsable) { widget.onNodeSelected?.call(node); @@ -284,6 +285,12 @@ class SduiTreeBuilderState extends material.State { ), ), ); + if (!canExpand) return row; + return material.Semantics( + button: true, + expanded: isExpanded, + child: row, + ); } String _resolveNodeKind(SduiTreeNode node) { diff --git a/lib/core/sdui/sdui_tree_schema.dart b/lib/core/sdui/sdui_tree_schema.dart index d530654a..843cdefa 100644 --- a/lib/core/sdui/sdui_tree_schema.dart +++ b/lib/core/sdui/sdui_tree_schema.dart @@ -93,4 +93,5 @@ class SduiTreeSchema { } /// Loads children for an expandable node (`fetchTreeChildren` RPC). -typedef SduiFetchTreeChildren = Future> Function(String nodeId); +typedef SduiFetchTreeChildren = Future> Function( + String nodeId); diff --git a/lib/features/connections/connections_panel.dart b/lib/features/connections/connections_panel.dart index 4c3cef41..1730bc14 100644 --- a/lib/features/connections/connections_panel.dart +++ b/lib/features/connections/connections_panel.dart @@ -38,6 +38,7 @@ import 'package:flutter/material.dart' as material Expanded, CircularProgressIndicator, Material, + Semantics, StatelessWidget, Colors, Tooltip, @@ -349,11 +350,17 @@ class ConnectionsPanelState extends State { _expandedConnections.remove(id); }); if (conn.type == 'postgresql') { - PostgresService.instance.interrupt(conn, database: conn.databaseName ?? 'postgres', mode: PgSessionMode.readOnly); - PostgresService.instance.interrupt(conn, database: conn.databaseName ?? 'postgres', mode: PgSessionMode.readWrite); + PostgresService.instance.interrupt(conn, + database: conn.databaseName ?? 'postgres', + mode: PgSessionMode.readOnly); + PostgresService.instance.interrupt(conn, + database: conn.databaseName ?? 'postgres', + mode: PgSessionMode.readWrite); } else if (conn.type == 'mysql') { - MysqlService.instance.interrupt(conn, database: conn.databaseName ?? '', mode: MysqlSessionMode.readOnly); - MysqlService.instance.interrupt(conn, database: conn.databaseName ?? '', mode: MysqlSessionMode.readWrite); + MysqlService.instance.interrupt(conn, + database: conn.databaseName ?? '', mode: MysqlSessionMode.readOnly); + MysqlService.instance.interrupt(conn, + database: conn.databaseName ?? '', mode: MysqlSessionMode.readWrite); } else if (conn.type == 'sqlite') { SqliteService.instance.interrupt(conn, mode: SqliteSessionMode.readOnly); SqliteService.instance.interrupt(conn, mode: SqliteSessionMode.readWrite); diff --git a/lib/features/connections/connections_panel_extension.dart b/lib/features/connections/connections_panel_extension.dart index 88902940..036a9dc0 100644 --- a/lib/features/connections/connections_panel_extension.dart +++ b/lib/features/connections/connections_panel_extension.dart @@ -91,8 +91,8 @@ class _ExtensionConnectionTileState extends State<_ExtensionConnectionTile> { _error = null; }); try { - final schema = - await ExtensionDriverSession.instance.getSchemaTree(widget.connection); + final schema = await ExtensionDriverSession.instance + .getSchemaTree(widget.connection); if (!mounted) return; setState(() { _schema = schema; @@ -174,19 +174,25 @@ class _ExtensionConnectionTileState extends State<_ExtensionConnectionTile> { children: [ material.MouseRegion( cursor: material.SystemMouseCursors.click, - child: material.InkWell( - onTap: _toggle, - borderRadius: material.BorderRadius.circular(4), - child: material.Padding( - padding: const material.EdgeInsets.all(2), - child: material.AnimatedRotation( - turns: widget.isExpanded ? 0.25 : 0, - duration: context.motionDuration(QueryaMotion.treeExpand), - curve: context.motionCurve(QueryaMotion.treeExpandCurve), - child: material.Icon( - QueryaIcons.expandClosed, - size: QueryaIconSizes.sidebarExpand, - color: theme.colorScheme.mutedForeground, + child: material.Semantics( + button: true, + expanded: widget.isExpanded, + child: material.InkWell( + onTap: _toggle, + borderRadius: material.BorderRadius.circular(4), + child: material.Padding( + padding: const material.EdgeInsets.all(2), + child: material.AnimatedRotation( + turns: widget.isExpanded ? 0.25 : 0, + duration: + context.motionDuration(QueryaMotion.treeExpand), + curve: + context.motionCurve(QueryaMotion.treeExpandCurve), + child: material.Icon( + QueryaIcons.expandClosed, + size: QueryaIconSizes.sidebarExpand, + color: theme.colorScheme.mutedForeground, + ), ), ), ), @@ -288,6 +294,7 @@ class _ExtensionConnectionTileState extends State<_ExtensionConnectionTile> { ) else if (_error != null) TreeLoadError( + title: 'Could not load extension tree', message: _error!, padding: const material.EdgeInsets.only( left: 28, diff --git a/lib/features/connections/connections_panel_mongo.dart b/lib/features/connections/connections_panel_mongo.dart index 68e991b4..d745ccf8 100644 --- a/lib/features/connections/connections_panel_mongo.dart +++ b/lib/features/connections/connections_panel_mongo.dart @@ -191,19 +191,25 @@ class _MongoConnectionTileState extends State<_MongoConnectionTile> { // Expand/collapse arrow material.MouseRegion( cursor: material.SystemMouseCursors.click, - child: material.InkWell( - onTap: _toggle, - borderRadius: material.BorderRadius.circular(4), - child: material.Padding( - padding: const material.EdgeInsets.all(2), - child: material.AnimatedRotation( - turns: _expanded ? 0.25 : 0, - duration: context.motionDuration(QueryaMotion.treeExpand), - curve: context.motionCurve(QueryaMotion.treeExpandCurve), - child: material.Icon( - QueryaIcons.expandClosed, - size: QueryaIconSizes.sidebarExpand, - color: theme.colorScheme.mutedForeground, + child: material.Semantics( + button: true, + expanded: _expanded, + child: material.InkWell( + onTap: _toggle, + borderRadius: material.BorderRadius.circular(4), + child: material.Padding( + padding: const material.EdgeInsets.all(2), + child: material.AnimatedRotation( + turns: _expanded ? 0.25 : 0, + duration: + context.motionDuration(QueryaMotion.treeExpand), + curve: + context.motionCurve(QueryaMotion.treeExpandCurve), + child: material.Icon( + QueryaIcons.expandClosed, + size: QueryaIconSizes.sidebarExpand, + color: theme.colorScheme.mutedForeground, + ), ), ), ), @@ -285,7 +291,6 @@ class _MongoConnectionTileState extends State<_MongoConnectionTile> { TreeLoadError( title: 'Could not load databases', message: _error!, - showTitleRow: true, detailFontSize: 10, padding: const material.EdgeInsets.only( left: 28, diff --git a/lib/features/connections/connections_panel_mysql.dart b/lib/features/connections/connections_panel_mysql.dart index 37a0edb3..a6f08dc1 100644 --- a/lib/features/connections/connections_panel_mysql.dart +++ b/lib/features/connections/connections_panel_mysql.dart @@ -156,19 +156,25 @@ class _MysqlConnectionTileState extends State<_MysqlConnectionTile> { children: [ material.MouseRegion( cursor: material.SystemMouseCursors.click, - child: material.InkWell( - onTap: _toggle, - borderRadius: material.BorderRadius.circular(4), - child: material.Padding( - padding: const material.EdgeInsets.all(2), - child: material.AnimatedRotation( - turns: _expanded ? 0.25 : 0, - duration: context.motionDuration(QueryaMotion.treeExpand), - curve: context.motionCurve(QueryaMotion.treeExpandCurve), - child: material.Icon( - QueryaIcons.expandClosed, - size: QueryaIconSizes.sidebarExpand, - color: theme.colorScheme.mutedForeground, + child: material.Semantics( + button: true, + expanded: _expanded, + child: material.InkWell( + onTap: _toggle, + borderRadius: material.BorderRadius.circular(4), + child: material.Padding( + padding: const material.EdgeInsets.all(2), + child: material.AnimatedRotation( + turns: _expanded ? 0.25 : 0, + duration: + context.motionDuration(QueryaMotion.treeExpand), + curve: + context.motionCurve(QueryaMotion.treeExpandCurve), + child: material.Icon( + QueryaIcons.expandClosed, + size: QueryaIconSizes.sidebarExpand, + color: theme.colorScheme.mutedForeground, + ), ), ), ), @@ -246,6 +252,7 @@ class _MysqlConnectionTileState extends State<_MysqlConnectionTile> { ), if (_error != null) TreeLoadError( + title: 'Could not load databases', message: _error!, padding: const material.EdgeInsets.only( left: 28, @@ -457,6 +464,7 @@ class _MysqlDatabaseNodeState extends State<_MysqlDatabaseNode> { color: theme.colorScheme.foreground, ), verticalPadding: 4, + expanded: _expanded, onTap: _toggle, connection: widget.connection, onContextRefresh: _loadTables, @@ -490,6 +498,7 @@ class _MysqlDatabaseNodeState extends State<_MysqlDatabaseNode> { ) else if (_error != null) TreeLoadError( + title: 'Could not load tables', message: _error!, onRetry: _loadTables, ), @@ -648,6 +657,7 @@ class _MysqlObjectGroupState extends State<_MysqlObjectGroup> { fontSize: 11, color: theme.colorScheme.mutedForeground, ), + expanded: _expanded, onTap: () => setState(() => _expanded = !_expanded), connection: widget.connection, onContextRefresh: widget.onRefresh, diff --git a/lib/features/connections/connections_panel_pg_tree.dart b/lib/features/connections/connections_panel_pg_tree.dart index f0afecd2..1579556f 100644 --- a/lib/features/connections/connections_panel_pg_tree.dart +++ b/lib/features/connections/connections_panel_pg_tree.dart @@ -65,6 +65,7 @@ class _PgTreeRow extends material.StatelessWidget { this.iconColor, this.trailing, this.onTap, + this.expanded, this.verticalPadding = 3, required this.textStyle, this.connection, @@ -85,6 +86,9 @@ class _PgTreeRow extends material.StatelessWidget { final material.Color? iconColor; final material.Widget? trailing; final void Function()? onTap; + + /// When non-null, row is an expand control ([Semantics.button] + expanded). + final bool? expanded; final double verticalPadding; final material.TextStyle textStyle; final ConnectionRow? connection; @@ -141,8 +145,16 @@ class _PgTreeRow extends material.StatelessWidget { ), ), ); - if (connection == null) return row; - return ContextMenu( + if (connection == null) { + return expanded == null + ? row + : material.Semantics( + button: true, + expanded: expanded, + child: row, + ); + } + final menu = ContextMenu( items: [ if (onContextRefresh != null) MenuButton( @@ -194,6 +206,12 @@ class _PgTreeRow extends material.StatelessWidget { ], child: row, ); + if (expanded == null) return menu; + return material.Semantics( + button: true, + expanded: expanded, + child: menu, + ); } } @@ -229,6 +247,7 @@ class _PgDatabasesNodeState extends State<_PgDatabasesNode> { color: theme.colorScheme.foreground, ), verticalPadding: 4, + expanded: _expanded, onTap: () => setState(() => _expanded = !_expanded), connection: widget.connection, onContextRefresh: widget.onRefreshDatabases, @@ -356,6 +375,7 @@ class _PgDatabaseNodeState extends State<_PgDatabaseNode> { color: theme.colorScheme.foreground, ), verticalPadding: 4, + expanded: _expanded, onTap: _toggle, connection: widget.connection, onContextRefresh: _loadSchemas, @@ -406,6 +426,7 @@ class _PgDatabaseNodeState extends State<_PgDatabaseNode> { ) else if (_error != null) TreeLoadError( + title: 'Could not load schemas', message: _error!, onRetry: _loadSchemas, ), @@ -550,6 +571,7 @@ class _PgSchemasNodeState extends State<_PgSchemasNode> { fontSize: 11, color: theme.colorScheme.mutedForeground, ), + expanded: _expanded, onTap: () => setState(() => _expanded = !_expanded), connection: widget.connection, onContextRefresh: widget.onRefreshSchemas, @@ -706,6 +728,7 @@ class _PgSchemaNodeState extends State<_PgSchemaNode> { fontSize: 12, color: theme.colorScheme.foreground, ), + expanded: _expanded, onTap: _toggle, connection: widget.connection, onContextRefresh: _loadObjects, @@ -736,6 +759,7 @@ class _PgSchemaNodeState extends State<_PgSchemaNode> { ) else if (_error != null) TreeLoadError( + title: 'Could not load objects', message: _error!, onRetry: _loadObjects, ), @@ -1024,6 +1048,7 @@ class _PgObjectGroupState extends State<_PgObjectGroup> { fontSize: 11, color: theme.colorScheme.mutedForeground, ), + expanded: _expanded, onTap: () => setState(() => _expanded = !_expanded), connection: widget.connection, onContextRefresh: widget.onRefresh, diff --git a/lib/features/connections/connections_panel_postgres_connection.dart b/lib/features/connections/connections_panel_postgres_connection.dart index f592e3c6..c6ecc336 100644 --- a/lib/features/connections/connections_panel_postgres_connection.dart +++ b/lib/features/connections/connections_panel_postgres_connection.dart @@ -151,19 +151,25 @@ class _PostgresConnectionTileState extends State<_PostgresConnectionTile> { children: [ material.MouseRegion( cursor: material.SystemMouseCursors.click, - child: material.InkWell( - onTap: _toggle, - borderRadius: material.BorderRadius.circular(4), - child: material.Padding( - padding: const material.EdgeInsets.all(2), - child: material.AnimatedRotation( - turns: _expanded ? 0.25 : 0, - duration: context.motionDuration(QueryaMotion.treeExpand), - curve: context.motionCurve(QueryaMotion.treeExpandCurve), - child: material.Icon( - QueryaIcons.expandClosed, - size: QueryaIconSizes.sidebarExpand, - color: theme.colorScheme.mutedForeground, + child: material.Semantics( + button: true, + expanded: _expanded, + child: material.InkWell( + onTap: _toggle, + borderRadius: material.BorderRadius.circular(4), + child: material.Padding( + padding: const material.EdgeInsets.all(2), + child: material.AnimatedRotation( + turns: _expanded ? 0.25 : 0, + duration: + context.motionDuration(QueryaMotion.treeExpand), + curve: + context.motionCurve(QueryaMotion.treeExpandCurve), + child: material.Icon( + QueryaIcons.expandClosed, + size: QueryaIconSizes.sidebarExpand, + color: theme.colorScheme.mutedForeground, + ), ), ), ), @@ -241,6 +247,7 @@ class _PostgresConnectionTileState extends State<_PostgresConnectionTile> { ), if (_error != null) TreeLoadError( + title: 'Could not load databases', message: _error!, padding: const material.EdgeInsets.only( left: 28, diff --git a/lib/features/connections/connections_panel_redis.dart b/lib/features/connections/connections_panel_redis.dart index cdb0aa4e..fb0f650b 100644 --- a/lib/features/connections/connections_panel_redis.dart +++ b/lib/features/connections/connections_panel_redis.dart @@ -168,19 +168,25 @@ class _RedisConnectionTileState extends State<_RedisConnectionTile> { // Expand/collapse arrow material.MouseRegion( cursor: material.SystemMouseCursors.click, - child: material.InkWell( - onTap: _toggle, - borderRadius: material.BorderRadius.circular(4), - child: material.Padding( - padding: const material.EdgeInsets.all(2), - child: material.AnimatedRotation( - turns: _expanded ? 0.25 : 0, - duration: context.motionDuration(QueryaMotion.treeExpand), - curve: context.motionCurve(QueryaMotion.treeExpandCurve), - child: material.Icon( - QueryaIcons.expandClosed, - size: QueryaIconSizes.sidebarExpand, - color: theme.colorScheme.mutedForeground, + child: material.Semantics( + button: true, + expanded: _expanded, + child: material.InkWell( + onTap: _toggle, + borderRadius: material.BorderRadius.circular(4), + child: material.Padding( + padding: const material.EdgeInsets.all(2), + child: material.AnimatedRotation( + turns: _expanded ? 0.25 : 0, + duration: + context.motionDuration(QueryaMotion.treeExpand), + curve: + context.motionCurve(QueryaMotion.treeExpandCurve), + child: material.Icon( + QueryaIcons.expandClosed, + size: QueryaIconSizes.sidebarExpand, + color: theme.colorScheme.mutedForeground, + ), ), ), ), @@ -260,6 +266,7 @@ class _RedisConnectionTileState extends State<_RedisConnectionTile> { ), if (_error != null) TreeLoadError( + title: 'Could not load Redis info', message: _error!, padding: const material.EdgeInsets.only( left: 28, diff --git a/lib/features/connections/connections_panel_sidebar.dart b/lib/features/connections/connections_panel_sidebar.dart index 98229949..8d981b0b 100644 --- a/lib/features/connections/connections_panel_sidebar.dart +++ b/lib/features/connections/connections_panel_sidebar.dart @@ -247,41 +247,47 @@ class _FolderTileState extends State<_FolderTile> { children: [ material.MouseRegion( cursor: material.SystemMouseCursors.click, - child: material.InkWell( - onTap: _toggle, - borderRadius: material.BorderRadius.circular(6), - child: material.Padding( - padding: const material.EdgeInsets.symmetric( - horizontal: 8, vertical: 6), - child: material.Row( - children: [ - material.AnimatedRotation( - turns: _expanded ? 0.25 : 0, - duration: context.motionDuration(QueryaMotion.treeExpand), - curve: context.motionCurve(QueryaMotion.treeExpandCurve), - child: material.Icon( - QueryaIcons.expandClosed, - size: QueryaIconSizes.sidebarExpand, - color: theme.colorScheme.mutedForeground, + child: material.Semantics( + button: true, + expanded: _expanded, + child: material.InkWell( + onTap: _toggle, + borderRadius: material.BorderRadius.circular(6), + child: material.Padding( + padding: const material.EdgeInsets.symmetric( + horizontal: 8, vertical: 6), + child: material.Row( + children: [ + material.AnimatedRotation( + turns: _expanded ? 0.25 : 0, + duration: + context.motionDuration(QueryaMotion.treeExpand), + curve: + context.motionCurve(QueryaMotion.treeExpandCurve), + child: material.Icon( + QueryaIcons.expandClosed, + size: QueryaIconSizes.sidebarExpand, + color: theme.colorScheme.mutedForeground, + ), ), - ), - const Gap(2), - material.Icon(QueryaIcons.folder, - size: QueryaIconSizes.sidebarConnectionIcon, - color: theme.colorScheme.primary), - const Gap(8), - material.Expanded( - child: material.Text( - widget.name, - overflow: material.TextOverflow.ellipsis, - maxLines: 1, - style: material.TextStyle( - fontSize: 13, - color: theme.colorScheme.foreground, + const Gap(2), + material.Icon(QueryaIcons.folder, + size: QueryaIconSizes.sidebarConnectionIcon, + color: theme.colorScheme.primary), + const Gap(8), + material.Expanded( + child: material.Text( + widget.name, + overflow: material.TextOverflow.ellipsis, + maxLines: 1, + style: material.TextStyle( + fontSize: 13, + color: theme.colorScheme.foreground, + ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/features/connections/connections_panel_sqlite.dart b/lib/features/connections/connections_panel_sqlite.dart index 0563f513..1b5dcb01 100644 --- a/lib/features/connections/connections_panel_sqlite.dart +++ b/lib/features/connections/connections_panel_sqlite.dart @@ -163,19 +163,25 @@ class _SqliteConnectionTileState extends State<_SqliteConnectionTile> { children: [ material.MouseRegion( cursor: material.SystemMouseCursors.click, - child: material.InkWell( - onTap: _toggle, - borderRadius: material.BorderRadius.circular(4), - child: material.Padding( - padding: const material.EdgeInsets.all(2), - child: material.AnimatedRotation( - turns: _expanded ? 0.25 : 0, - duration: context.motionDuration(QueryaMotion.treeExpand), - curve: context.motionCurve(QueryaMotion.treeExpandCurve), - child: material.Icon( - QueryaIcons.expandClosed, - size: QueryaIconSizes.sidebarExpand, - color: theme.colorScheme.mutedForeground, + child: material.Semantics( + button: true, + expanded: _expanded, + child: material.InkWell( + onTap: _toggle, + borderRadius: material.BorderRadius.circular(4), + child: material.Padding( + padding: const material.EdgeInsets.all(2), + child: material.AnimatedRotation( + turns: _expanded ? 0.25 : 0, + duration: + context.motionDuration(QueryaMotion.treeExpand), + curve: + context.motionCurve(QueryaMotion.treeExpandCurve), + child: material.Icon( + QueryaIcons.expandClosed, + size: QueryaIconSizes.sidebarExpand, + color: theme.colorScheme.mutedForeground, + ), ), ), ), @@ -253,6 +259,7 @@ class _SqliteConnectionTileState extends State<_SqliteConnectionTile> { ), if (_error != null) TreeLoadError( + title: 'Could not load objects', message: _error!, padding: const material.EdgeInsets.only( left: 28, @@ -378,6 +385,7 @@ class _SqliteObjectGroupState extends State<_SqliteObjectGroup> { fontSize: 11, color: theme.colorScheme.mutedForeground, ), + expanded: _expanded, onTap: () => setState(() => _expanded = !_expanded), connection: widget.connection, onContextRefresh: widget.onRefresh, diff --git a/lib/features/connections/driver_manager_dialog.dart b/lib/features/connections/driver_manager_dialog.dart index 38a34464..a32fd6db 100644 --- a/lib/features/connections/driver_manager_dialog.dart +++ b/lib/features/connections/driver_manager_dialog.dart @@ -75,85 +75,77 @@ class _DriverManagerDialogContent extends material.StatelessWidget { @override material.Widget build(material.BuildContext context) { final theme = Theme.of(context).colorScheme; - final radius = Theme.of(context).radiusXxl; final drivers = _buildDriverList(); - return material.Container( + return QueryaDialogCard( constraints: WindowLayout.dialogConstraints( context, maxWidth: 520, minWidth: 400, ), - decoration: material.BoxDecoration( - color: theme.popover, - borderRadius: material.BorderRadius.circular(radius), - border: material.Border.all(color: theme.muted), - ), - child: material.ClipRRect( - borderRadius: material.BorderRadius.circular(radius), - child: material.Column( - mainAxisSize: material.MainAxisSize.min, - crossAxisAlignment: material.CrossAxisAlignment.stretch, - children: [ - material.Padding( - padding: const material.EdgeInsets.fromLTRB(24, 24, 24, 8), - child: material.Column( - crossAxisAlignment: material.CrossAxisAlignment.start, - children: [ - const Text('Driver Manager').large().semiBold(), - const material.SizedBox(height: 6), - const Text( - 'Built-in Dart drivers and installed sandboxed extension drivers. ' - 'Add a server under Connection → New Database Connection.', - ).muted().small(), - ], - ), - ), - material.Padding( - padding: const material.EdgeInsets.symmetric( - horizontal: 24, vertical: 12), - child: material.Container( - decoration: material.BoxDecoration( - color: theme.muted.withValues(alpha: 0.15), - borderRadius: material.BorderRadius.circular(10), - border: material.Border.all( - color: theme.border.withValues(alpha: 0.3)), - ), - child: material.ListView.separated( - shrinkWrap: true, - padding: const material.EdgeInsets.symmetric(vertical: 8), - itemCount: drivers.length, - separatorBuilder: (_, __) => material.Divider( - height: 1, - color: theme.border.withValues(alpha: 0.3), - ), - itemBuilder: (context, index) { - final info = drivers[index]; - return _DriverRow(info: info, theme: theme); - }, - ), - ), + borderColor: theme.muted, + child: material.Column( + mainAxisSize: material.MainAxisSize.min, + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + material.Padding( + padding: const material.EdgeInsets.fromLTRB(24, 24, 24, 8), + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + children: [ + const Text('Driver Manager').large().semiBold(), + const material.SizedBox(height: 6), + const Text( + 'Built-in Dart drivers and installed sandboxed extension drivers. ' + 'Add a server under Connection → New Database Connection.', + ).muted().small(), + ], ), - material.Container( - padding: const material.EdgeInsets.symmetric( - horizontal: 24, vertical: 16), + ), + material.Padding( + padding: const material.EdgeInsets.symmetric( + horizontal: 24, vertical: 12), + child: material.Container( decoration: material.BoxDecoration( - border: material.Border( - top: material.BorderSide( - color: theme.border.withValues(alpha: 0.3)), + color: theme.muted.withValues(alpha: 0.15), + borderRadius: material.BorderRadius.circular(10), + border: material.Border.all( + color: theme.border.withValues(alpha: 0.3)), + ), + child: material.ListView.separated( + shrinkWrap: true, + padding: const material.EdgeInsets.symmetric(vertical: 8), + itemCount: drivers.length, + separatorBuilder: (_, __) => material.Divider( + height: 1, + color: theme.border.withValues(alpha: 0.3), ), + itemBuilder: (context, index) { + final info = drivers[index]; + return _DriverRow(info: info, theme: theme); + }, ), - child: material.Row( - mainAxisAlignment: material.MainAxisAlignment.end, - children: [ - PrimaryButton( - onPressed: () => material.Navigator.of(context).pop(), - child: const Text('Close'), - ), - ], + ), + ), + material.Container( + padding: const material.EdgeInsets.symmetric( + horizontal: 24, vertical: 16), + decoration: material.BoxDecoration( + border: material.Border( + top: material.BorderSide( + color: theme.border.withValues(alpha: 0.3)), ), ), - ], - ), + child: material.Row( + mainAxisAlignment: material.MainAxisAlignment.end, + children: [ + PrimaryButton( + onPressed: () => material.Navigator.of(context).pop(), + child: const Text('Close'), + ), + ], + ), + ), + ], ), ); } diff --git a/lib/features/connections/new_connection_dialog.dart b/lib/features/connections/new_connection_dialog.dart index dc26a23f..b64f2470 100644 --- a/lib/features/connections/new_connection_dialog.dart +++ b/lib/features/connections/new_connection_dialog.dart @@ -105,26 +105,20 @@ class _NewConnectionDialogContentState @override material.Widget build(material.BuildContext context) { final theme = Theme.of(context).colorScheme; - final radius = Theme.of(context).radiusXxl; final dialogMaxW = WindowLayout.newConnectionDialogMaxWidth(context); final dialogH = WindowLayout.newConnectionDialogHeight(context); final headerPadH = dialogMaxW < 420 ? 16.0 : 24.0; final stackFilters = dialogMaxW < 520; - return material.Container( + return material.SizedBox( width: dialogMaxW, - constraints: material.BoxConstraints( - maxWidth: dialogMaxW, - maxHeight: dialogH, - minHeight: math.min(320.0, dialogH), - ), - decoration: material.BoxDecoration( - color: theme.popover, - borderRadius: material.BorderRadius.circular(radius), - border: material.Border.all(color: theme.muted), - ), - child: material.ClipRRect( - borderRadius: material.BorderRadius.circular(radius), + child: QueryaDialogCard( + constraints: material.BoxConstraints( + maxWidth: dialogMaxW, + maxHeight: dialogH, + minHeight: math.min(320.0, dialogH), + ), + borderColor: theme.muted, child: material.SizedBox( height: dialogH, child: material.Column( @@ -403,8 +397,7 @@ class _DbTypeCard extends material.StatelessWidget { final highlight = t.muted.withValues(alpha: 0.4); return QueryaHoverSurface( borderRadius: material.BorderRadius.circular(10), - padding: - const material.EdgeInsets.symmetric(vertical: 10, horizontal: 8), + padding: const material.EdgeInsets.symmetric(vertical: 10, horizontal: 8), idleColor: selected ? highlight : t.muted.withValues(alpha: 0.12), hoveredColor: highlight, border: material.Border.all( diff --git a/lib/features/connections/new_connection_url_dialog.dart b/lib/features/connections/new_connection_url_dialog.dart index 6c73fe42..b4c67678 100644 --- a/lib/features/connections/new_connection_url_dialog.dart +++ b/lib/features/connections/new_connection_url_dialog.dart @@ -6,7 +6,8 @@ import 'package:querya_desktop/shared/widgets/widgets.dart'; /// Shows a dialog to create a new database connection from a URI. /// Returns the ConnectionRow or null if cancelled. -Future showNewConnectionUrlDialog(material.BuildContext context) { +Future showNewConnectionUrlDialog( + material.BuildContext context) { return showAppDialog( context: context, builder: (context) => material.Dialog( @@ -48,108 +49,101 @@ class _NewConnectionUrlDialogContentState @override material.Widget build(material.BuildContext context) { final theme = Theme.of(context).colorScheme; - final radius = Theme.of(context).radiusXxl; - return material.Container( + return QueryaDialogCard( constraints: WindowLayout.dialogConstraints( context, maxWidth: 580, minWidth: 420, ), - decoration: material.BoxDecoration( - color: theme.popover, - borderRadius: material.BorderRadius.circular(radius), - border: material.Border.all(color: theme.muted), - ), - child: material.ClipRRect( - borderRadius: material.BorderRadius.circular(radius), - child: material.Column( - mainAxisSize: material.MainAxisSize.min, - crossAxisAlignment: material.CrossAxisAlignment.stretch, - children: [ - material.Padding( - padding: const material.EdgeInsets.fromLTRB(24, 24, 24, 8), - child: material.Column( - crossAxisAlignment: material.CrossAxisAlignment.start, - children: [ - const Text('New connection from URL').large().semiBold(), - const material.SizedBox(height: 6), - const Text( - 'Create a connection by pasting a database URI (e.g. postgresql://user:pass@host:5432/db).', - ).muted().small(), - const material.SizedBox(height: 16), - material.Container( - decoration: material.BoxDecoration( - color: theme.muted.withValues(alpha: 0.2), - borderRadius: material.BorderRadius.circular(8), - border: material.Border.all( + borderColor: theme.muted, + child: material.Column( + mainAxisSize: material.MainAxisSize.min, + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + material.Padding( + padding: const material.EdgeInsets.fromLTRB(24, 24, 24, 8), + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + children: [ + const Text('New connection from URL').large().semiBold(), + const material.SizedBox(height: 6), + const Text( + 'Create a connection by pasting a database URI (e.g. postgresql://user:pass@host:5432/db).', + ).muted().small(), + const material.SizedBox(height: 16), + material.Container( + decoration: material.BoxDecoration( + color: theme.muted.withValues(alpha: 0.2), + borderRadius: material.BorderRadius.circular(8), + border: material.Border.all( + color: _validationError != null + ? theme.destructive.withValues(alpha: 0.8) + : theme.border.withValues(alpha: 0.4), + ), + ), + padding: const material.EdgeInsets.symmetric( + horizontal: 12, vertical: 4), + child: material.Row( + children: [ + material.Icon( + material.Icons.link_rounded, + size: 20, color: _validationError != null - ? theme.destructive.withValues(alpha: 0.8) - : theme.border.withValues(alpha: 0.4), + ? theme.destructive + : theme.mutedForeground, ), - ), - padding: const material.EdgeInsets.symmetric( - horizontal: 12, vertical: 4), - child: material.Row( - children: [ - material.Icon( - material.Icons.link_rounded, - size: 20, - color: _validationError != null - ? theme.destructive - : theme.mutedForeground, + const material.SizedBox(width: 10), + material.Expanded( + child: TextField( + controller: _urlController, + placeholder: + const Text('database://user:pass@host:port/db'), + onSubmitted: (_) => _validateAndSubmit(), + onChanged: (_) { + if (_validationError != null) { + setState(() => _validationError = null); + } + }, ), - const material.SizedBox(width: 10), - material.Expanded( - child: TextField( - controller: _urlController, - placeholder: const Text('database://user:pass@host:port/db'), - onSubmitted: (_) => _validateAndSubmit(), - onChanged: (_) { - if (_validationError != null) { - setState(() => _validationError = null); - } - }, - ), - ), - ], - ), + ), + ], ), - if (_validationError != null) ...[ - const material.SizedBox(height: 8), - Text( - _validationError!, - style: material.TextStyle(color: theme.destructive), - ).small(), - ], + ), + if (_validationError != null) ...[ + const material.SizedBox(height: 8), + Text( + _validationError!, + style: material.TextStyle(color: theme.destructive), + ).small(), ], + ], + ), + ), + material.Container( + padding: const material.EdgeInsets.symmetric( + horizontal: 24, vertical: 16), + decoration: material.BoxDecoration( + border: material.Border( + top: material.BorderSide( + color: theme.border.withValues(alpha: 0.3)), ), ), - material.Container( - padding: const material.EdgeInsets.symmetric( - horizontal: 24, vertical: 16), - decoration: material.BoxDecoration( - border: material.Border( - top: material.BorderSide( - color: theme.border.withValues(alpha: 0.3)), + child: material.Row( + mainAxisAlignment: material.MainAxisAlignment.end, + children: [ + GhostButton( + onPressed: () => material.Navigator.of(context).pop(), + child: const Text('Cancel'), ), - ), - child: material.Row( - mainAxisAlignment: material.MainAxisAlignment.end, - children: [ - GhostButton( - onPressed: () => material.Navigator.of(context).pop(), - child: const Text('Cancel'), - ), - const material.SizedBox(width: 12), - PrimaryButton( - onPressed: _validateAndSubmit, - child: const Text('Create'), - ), - ], - ), + const material.SizedBox(width: 12), + PrimaryButton( + onPressed: _validateAndSubmit, + child: const Text('Create'), + ), + ], ), - ], - ), + ), + ], ), ); } diff --git a/lib/features/connections/new_folder_dialog.dart b/lib/features/connections/new_folder_dialog.dart index f4c6a851..37c37539 100644 --- a/lib/features/connections/new_folder_dialog.dart +++ b/lib/features/connections/new_folder_dialog.dart @@ -37,93 +37,85 @@ class _NewFolderDialogContentState @override material.Widget build(material.BuildContext context) { final theme = Theme.of(context).colorScheme; - final radius = Theme.of(context).radiusXxl; - return material.Container( + return QueryaDialogCard( constraints: WindowLayout.dialogConstraints( context, maxWidth: 440, minWidth: 360, ), - decoration: material.BoxDecoration( - color: theme.popover, - borderRadius: material.BorderRadius.circular(radius), - border: material.Border.all(color: theme.muted), - ), - child: material.ClipRRect( - borderRadius: material.BorderRadius.circular(radius), - child: material.Column( - mainAxisSize: material.MainAxisSize.min, - crossAxisAlignment: material.CrossAxisAlignment.stretch, - children: [ - material.Padding( - padding: const material.EdgeInsets.fromLTRB(24, 24, 24, 8), - child: material.Column( - crossAxisAlignment: material.CrossAxisAlignment.start, - children: [ - const Text('New folder').large().semiBold(), - const material.SizedBox(height: 6), - const Text( - 'Enter a name for the new folder in the browser tree.', - ).muted().small(), - const material.SizedBox(height: 16), - material.Container( - decoration: material.BoxDecoration( - color: theme.muted.withValues(alpha: 0.2), - borderRadius: material.BorderRadius.circular(8), - border: material.Border.all( - color: theme.border.withValues(alpha: 0.4)), - ), - padding: const material.EdgeInsets.symmetric( - horizontal: 12, vertical: 4), - child: material.Row( - children: [ - material.Icon( - material.Icons.folder_rounded, - size: 20, - color: theme.mutedForeground, - ), - const material.SizedBox(width: 10), - material.Expanded( - child: TextField( - controller: _nameController, - placeholder: const Text('Folder name'), - onChanged: (_) => setState(() {}), - ), + borderColor: theme.muted, + child: material.Column( + mainAxisSize: material.MainAxisSize.min, + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + material.Padding( + padding: const material.EdgeInsets.fromLTRB(24, 24, 24, 8), + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + children: [ + const Text('New folder').large().semiBold(), + const material.SizedBox(height: 6), + const Text( + 'Enter a name for the new folder in the browser tree.', + ).muted().small(), + const material.SizedBox(height: 16), + material.Container( + decoration: material.BoxDecoration( + color: theme.muted.withValues(alpha: 0.2), + borderRadius: material.BorderRadius.circular(8), + border: material.Border.all( + color: theme.border.withValues(alpha: 0.4)), + ), + padding: const material.EdgeInsets.symmetric( + horizontal: 12, vertical: 4), + child: material.Row( + children: [ + material.Icon( + material.Icons.folder_rounded, + size: 20, + color: theme.mutedForeground, + ), + const material.SizedBox(width: 10), + material.Expanded( + child: TextField( + controller: _nameController, + placeholder: const Text('Folder name'), + onChanged: (_) => setState(() {}), ), - ], - ), + ), + ], ), - ], + ), + ], + ), + ), + material.Container( + padding: const material.EdgeInsets.symmetric( + horizontal: 24, vertical: 16), + decoration: material.BoxDecoration( + border: material.Border( + top: material.BorderSide( + color: theme.border.withValues(alpha: 0.3)), ), ), - material.Container( - padding: const material.EdgeInsets.symmetric( - horizontal: 24, vertical: 16), - decoration: material.BoxDecoration( - border: material.Border( - top: material.BorderSide( - color: theme.border.withValues(alpha: 0.3)), + child: material.Row( + mainAxisAlignment: material.MainAxisAlignment.end, + children: [ + GhostButton( + onPressed: () => material.Navigator.of(context).pop(), + child: const Text('Cancel'), ), - ), - child: material.Row( - mainAxisAlignment: material.MainAxisAlignment.end, - children: [ - GhostButton( - onPressed: () => material.Navigator.of(context).pop(), - child: const Text('Cancel'), - ), - const material.SizedBox(width: 12), - PrimaryButton( - onPressed: _name.isEmpty - ? null - : () => material.Navigator.of(context).pop(_name), - child: const Text('Create'), - ), - ], - ), + const material.SizedBox(width: 12), + PrimaryButton( + onPressed: _name.isEmpty + ? null + : () => material.Navigator.of(context).pop(_name), + child: const Text('Create'), + ), + ], ), - ], - ), + ), + ], ), ); } diff --git a/lib/features/extensions/presentation/pages/extension_manager_dialog.dart b/lib/features/extensions/presentation/pages/extension_manager_dialog.dart index d87455d1..a2f0a275 100644 --- a/lib/features/extensions/presentation/pages/extension_manager_dialog.dart +++ b/lib/features/extensions/presentation/pages/extension_manager_dialog.dart @@ -158,86 +158,77 @@ class _ExtensionManagerContentState @override material.Widget build(material.BuildContext context) { final theme = Theme.of(context).colorScheme; - final radius = Theme.of(context).radiusXxl; final onPopover = theme.popoverForeground; return material.DefaultTextStyle( style: material.TextStyle(color: onPopover), child: material.IconTheme( data: material.IconThemeData(color: onPopover), - child: material.Container( + child: QueryaDialogCard( constraints: WindowLayout.dialogConstraints( context, maxWidth: 800, minWidth: 600, maxHeight: 700, ), - decoration: material.BoxDecoration( - color: theme.popover, - borderRadius: material.BorderRadius.circular(radius), - border: material.Border.all(color: theme.border), - ), - child: material.ClipRRect( - borderRadius: material.BorderRadius.circular(radius), - child: material.Column( - crossAxisAlignment: material.CrossAxisAlignment.stretch, - children: [ - material.Padding( - padding: const material.EdgeInsets.fromLTRB(24, 24, 24, 8), - child: material.Row( - mainAxisAlignment: material.MainAxisAlignment.spaceBetween, - crossAxisAlignment: material.CrossAxisAlignment.center, - children: [ - material.Expanded( - child: material.Column( - crossAxisAlignment: material.CrossAxisAlignment.start, - children: [ - const Text('Extensions') - .large() - .semiBold() - .foreground(), - const material.SizedBox(height: 6), - const Text( - 'Manage local and marketplace extensions') - .muted() - .small(), - ], - ), - ), - const material.SizedBox(width: 16), - PrimaryButton( - onPressed: () => material.Navigator.of(context).pop(), - child: const Text('Close'), + borderColor: theme.border, + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + material.Padding( + padding: const material.EdgeInsets.fromLTRB(24, 24, 24, 8), + child: material.Row( + mainAxisAlignment: material.MainAxisAlignment.spaceBetween, + crossAxisAlignment: material.CrossAxisAlignment.center, + children: [ + material.Expanded( + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + children: [ + const Text('Extensions') + .large() + .semiBold() + .foreground(), + const material.SizedBox(height: 6), + const Text('Manage local and marketplace extensions') + .muted() + .small(), + ], ), - ], - ), + ), + const material.SizedBox(width: 16), + PrimaryButton( + onPressed: () => material.Navigator.of(context).pop(), + child: const Text('Close'), + ), + ], ), - material.Padding( - padding: const material.EdgeInsets.symmetric( - horizontal: 24.0, vertical: 8.0), - child: QueryaTabStrip( - labels: [ - 'Installed (${_installed.length})', - 'Marketplace', - 'Updates', - ], - selectedIndex: _tabIndex, - onSelected: (index) => setState(() => _tabIndex = index), - ), + ), + material.Padding( + padding: const material.EdgeInsets.symmetric( + horizontal: 24.0, vertical: 8.0), + child: QueryaTabStrip( + labels: [ + 'Installed (${_installed.length})', + 'Marketplace', + 'Updates', + ], + selectedIndex: _tabIndex, + onSelected: (index) => setState(() => _tabIndex = index), ), - material.Divider(height: 1, color: theme.border), - material.Expanded( - child: QueryaCrossFadeStack( - index: _tabIndex, - children: [ - _buildInstalledTab(), - _buildMarketplaceTab(), - _buildUpdatesTab(), - ], - ), + ), + material.Divider(height: 1, color: theme.border), + material.Expanded( + child: QueryaCrossFadeStack( + index: _tabIndex, + children: [ + _buildInstalledTab(), + _buildMarketplaceTab(), + _buildUpdatesTab(), + ], ), - ], - ), + ), + ], ), ), ), diff --git a/lib/features/help/about_dialog.dart b/lib/features/help/about_dialog.dart index 11eef0a7..8735e213 100644 --- a/lib/features/help/about_dialog.dart +++ b/lib/features/help/about_dialog.dart @@ -31,84 +31,76 @@ class _AboutDialogContentState extends material.State<_AboutDialogContent> { @override material.Widget build(material.BuildContext context) { final theme = Theme.of(context).colorScheme; - final radius = Theme.of(context).radiusXxl; final wb = context.workbench; - return material.Container( + return QueryaDialogCard( constraints: WindowLayout.dialogConstraints( context, maxWidth: 420, minWidth: 320, ), - decoration: material.BoxDecoration( - color: theme.popover, - borderRadius: material.BorderRadius.circular(radius), - border: material.Border.all(color: theme.muted), - ), - child: material.ClipRRect( - borderRadius: material.BorderRadius.circular(radius), - child: material.Column( - mainAxisSize: material.MainAxisSize.min, - children: [ - material.Padding( - padding: const material.EdgeInsets.fromLTRB(24, 28, 24, 8), - child: material.Column( - children: [ - material.Icon( - material.Icons.search_rounded, - size: 48, - color: wb.accent, - ), - const material.SizedBox(height: 16), - const Text('Querya').large().semiBold(), - const material.SizedBox(height: 8), - FutureBuilder( - future: _packageInfo, - builder: (context, snapshot) { - final version = snapshot.data?.version ?? '…'; - return Text('Version $version').muted().small(); - }, - ), - const material.SizedBox(height: 16), - const Text( - 'A lightweight desktop SQL/NoSQL client.', - ).muted().small(), - const material.SizedBox(height: 12), - const Text( - 'Licensed under the MIT License.', - ).small(), - const material.SizedBox(height: 16), - GhostButton( - onPressed: () => launchRepositoryUrl(), - child: const Text('View repository'), - ), - ], - ), + borderColor: theme.muted, + child: material.Column( + mainAxisSize: material.MainAxisSize.min, + children: [ + material.Padding( + padding: const material.EdgeInsets.fromLTRB(24, 28, 24, 8), + child: material.Column( + children: [ + material.Icon( + material.Icons.search_rounded, + size: 48, + color: wb.accent, + ), + const material.SizedBox(height: 16), + const Text('Querya').large().semiBold(), + const material.SizedBox(height: 8), + FutureBuilder( + future: _packageInfo, + builder: (context, snapshot) { + final version = snapshot.data?.version ?? '…'; + return Text('Version $version').muted().small(); + }, + ), + const material.SizedBox(height: 16), + const Text( + 'A lightweight desktop SQL/NoSQL client.', + ).muted().small(), + const material.SizedBox(height: 12), + const Text( + 'Licensed under the MIT License.', + ).small(), + const material.SizedBox(height: 16), + GhostButton( + onPressed: () => launchRepositoryUrl(), + child: const Text('View repository'), + ), + ], ), - material.Container( - padding: const material.EdgeInsets.symmetric( - horizontal: 24, - vertical: 16, - ), - decoration: material.BoxDecoration( - border: material.Border( - top: material.BorderSide( - color: theme.border.withValues(alpha: 0.3), - ), + ), + material.Container( + padding: const material.EdgeInsets.symmetric( + horizontal: 24, + vertical: 16, + ), + decoration: material.BoxDecoration( + border: material.Border( + top: material.BorderSide( + color: theme.border.withValues(alpha: 0.3), ), ), - child: material.Row( - mainAxisAlignment: material.MainAxisAlignment.end, - children: [ - PrimaryButton( - onPressed: () => material.Navigator.of(context).pop(), - child: const Text('Close'), - ), - ], - ), ), - ], - ), + child: material.Row( + mainAxisAlignment: material.MainAxisAlignment.end, + children: [ + PrimaryButton( + onPressed: () => material.Navigator.of(context).pop(), + child: const Text('Close'), + ), + ], + ), + ), + ], ), ); } diff --git a/lib/features/mongodb/mongo_database_dialog.dart b/lib/features/mongodb/mongo_database_dialog.dart index af10e3d9..2bb39f8e 100644 --- a/lib/features/mongodb/mongo_database_dialog.dart +++ b/lib/features/mongodb/mongo_database_dialog.dart @@ -51,77 +51,69 @@ class _CreateMongoDBDialogContentState @override material.Widget build(material.BuildContext context) { final theme = Theme.of(context).colorScheme; - final radius = Theme.of(context).radiusXxl; - return material.Container( + return QueryaDialogCard( constraints: WindowLayout.dialogConstraints(context, maxWidth: 500), - decoration: material.BoxDecoration( - color: theme.popover, - borderRadius: material.BorderRadius.circular(radius), - border: material.Border.all(color: theme.muted), - ), - child: material.ClipRRect( - borderRadius: material.BorderRadius.circular(radius), - child: material.Column( - mainAxisSize: material.MainAxisSize.min, - crossAxisAlignment: material.CrossAxisAlignment.stretch, - children: [ - material.Padding( - padding: const material.EdgeInsets.fromLTRB(24, 24, 24, 16), - child: material.Column( - crossAxisAlignment: material.CrossAxisAlignment.stretch, - children: [ - material.Row( - children: [ - material.Icon(material.Icons.storage_rounded, - size: 24, color: theme.primary), - const Gap(12), - const Text('Create Database').large().semiBold(), - ], - ), - const Gap(8), - const Text('Enter the name for the new MongoDB database.') - .muted() - .small(), - ], - ), + borderColor: theme.muted, + child: material.Column( + mainAxisSize: material.MainAxisSize.min, + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + material.Padding( + padding: const material.EdgeInsets.fromLTRB(24, 24, 24, 16), + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + material.Row( + children: [ + material.Icon(material.Icons.storage_rounded, + size: 24, color: theme.primary), + const Gap(12), + const Text('Create Database').large().semiBold(), + ], + ), + const Gap(8), + const Text('Enter the name for the new MongoDB database.') + .muted() + .small(), + ], ), - const material.Divider(height: 1), - material.Padding( - padding: const material.EdgeInsets.all(24), - child: material.Column( - crossAxisAlignment: material.CrossAxisAlignment.stretch, - children: [ - const Text('Database Name').small().semiBold(), - const Gap(8), - TextField( - controller: _nameController, - placeholder: const Text('mydb'), - ), - ], - ), + ), + const material.Divider(height: 1), + material.Padding( + padding: const material.EdgeInsets.all(24), + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + const Text('Database Name').small().semiBold(), + const Gap(8), + TextField( + controller: _nameController, + placeholder: const Text('mydb'), + ), + ], ), - const material.Divider(height: 1), - material.Container( - padding: const material.EdgeInsets.symmetric( - horizontal: 24, vertical: 16), - child: material.Row( - mainAxisAlignment: material.MainAxisAlignment.end, - children: [ - GhostButton( - onPressed: () => material.Navigator.of(context).pop(), - child: const Text('Cancel'), - ), - const Gap(12), - PrimaryButton( - onPressed: _formValid ? _save : null, - child: const Text('Create'), - ), - ], - ), + ), + const material.Divider(height: 1), + material.Container( + padding: const material.EdgeInsets.symmetric( + horizontal: 24, vertical: 16), + child: material.Row( + mainAxisAlignment: material.MainAxisAlignment.end, + children: [ + GhostButton( + onPressed: () => material.Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + const Gap(12), + PrimaryButton( + onPressed: _formValid ? _save : null, + child: const Text('Create'), + ), + ], ), - ], - ), + ), + ], ), ); } diff --git a/lib/features/mysql/mysql_sql_editor_dialog.dart b/lib/features/mysql/mysql_sql_editor_dialog.dart index e1c09ef8..d18b62bb 100644 --- a/lib/features/mysql/mysql_sql_editor_dialog.dart +++ b/lib/features/mysql/mysql_sql_editor_dialog.dart @@ -73,102 +73,93 @@ class _MysqlSqlEditorDialogState extends material.State<_MysqlSqlEditorDialog> { @override material.Widget build(material.BuildContext context) { final theme = Theme.of(context).colorScheme; - final radius = Theme.of(context).radiusXxl; return material.Dialog( backgroundColor: material.Colors.transparent, insetPadding: WindowLayout.dialogSymmetricInsets(context), - child: material.Container( + child: QueryaDialogCard( constraints: WindowLayout.dialogConstraints( context, maxWidth: 720, minWidth: 480, maxHeight: 520, ), - decoration: material.BoxDecoration( - color: theme.popover, - borderRadius: material.BorderRadius.circular(radius), - border: material.Border.all(color: theme.muted), - ), - child: material.ClipRRect( - borderRadius: material.BorderRadius.circular(radius), - child: material.Column( - mainAxisSize: material.MainAxisSize.min, - crossAxisAlignment: material.CrossAxisAlignment.stretch, - children: [ - material.Padding( - padding: const material.EdgeInsets.fromLTRB(24, 20, 24, 8), - child: material.Column( - crossAxisAlignment: material.CrossAxisAlignment.start, - children: [ - const Text('SQL query').large().semiBold(), - const material.SizedBox(height: 6), - const Text( - 'Table browse uses SELECT with LIMIT/OFFSET. ' - 'Edit or write your own SELECT. Reset restores the browse query. ' - 'Run reloads the grid; unchanged data looks the same.', - ).muted().xSmall(), - ], - ), + borderColor: theme.muted, + child: material.Column( + mainAxisSize: material.MainAxisSize.min, + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + material.Padding( + padding: const material.EdgeInsets.fromLTRB(24, 20, 24, 8), + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + children: [ + const Text('SQL query').large().semiBold(), + const material.SizedBox(height: 6), + const Text( + 'Table browse uses SELECT with LIMIT/OFFSET. ' + 'Edit or write your own SELECT. Reset restores the browse query. ' + 'Run reloads the grid; unchanged data looks the same.', + ).muted().xSmall(), + ], ), - material.Padding( - padding: const material.EdgeInsets.symmetric(horizontal: 24), - child: material.SizedBox( - height: 280, - child: material.Container( - decoration: - SqlEditorChrome.inlineFieldDecorationFromContext( - context, - ), - child: QueryaCodeEditor( - controller: _controller, - language: QueryaCodeLanguage.sql, - fontSize: 12, - variant: QueryaCodeEditorVariant.material, - textAlignVertical: material.TextAlignVertical.top, - hintText: 'SELECT …', - contentPadding: const material.EdgeInsets.all(12), - ), + ), + material.Padding( + padding: const material.EdgeInsets.symmetric(horizontal: 24), + child: material.SizedBox( + height: 280, + child: material.Container( + decoration: SqlEditorChrome.inlineFieldDecorationFromContext( + context, ), - ), - ), - if (_error != null) - material.Padding( - padding: const material.EdgeInsets.fromLTRB(24, 8, 24, 0), - child: material.Text( - _error!, - style: material.TextStyle( - color: theme.destructive, fontSize: 12), + child: QueryaCodeEditor( + controller: _controller, + language: QueryaCodeLanguage.sql, + fontSize: 12, + variant: QueryaCodeEditorVariant.material, + textAlignVertical: material.TextAlignVertical.top, + hintText: 'SELECT …', + contentPadding: const material.EdgeInsets.all(12), ), ), + ), + ), + if (_error != null) material.Padding( - padding: const material.EdgeInsets.all(20), - child: material.Row( - mainAxisAlignment: material.MainAxisAlignment.end, - children: [ - OutlineButton( - onPressed: () => material.Navigator.of(context).pop(), - child: const Text('Cancel'), - ), - const Gap(8), - OutlineButton( - onPressed: () { - setState(() { - _error = null; - _controller.text = widget.browseSql; - }); - }, - child: const Text('Reset'), - ), - const Gap(8), - PrimaryButton( - onPressed: _submit, - child: const Text('Run'), - ), - ], + padding: const material.EdgeInsets.fromLTRB(24, 8, 24, 0), + child: material.Text( + _error!, + style: material.TextStyle( + color: theme.destructive, fontSize: 12), ), ), - ], - ), + material.Padding( + padding: const material.EdgeInsets.all(20), + child: material.Row( + mainAxisAlignment: material.MainAxisAlignment.end, + children: [ + OutlineButton( + onPressed: () => material.Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + const Gap(8), + OutlineButton( + onPressed: () { + setState(() { + _error = null; + _controller.text = widget.browseSql; + }); + }, + child: const Text('Reset'), + ), + const Gap(8), + PrimaryButton( + onPressed: _submit, + child: const Text('Run'), + ), + ], + ), + ), + ], ), ), ); diff --git a/lib/features/postgresql/postgres_sql_editor_dialog.dart b/lib/features/postgresql/postgres_sql_editor_dialog.dart index 1f28fefb..49177e71 100644 --- a/lib/features/postgresql/postgres_sql_editor_dialog.dart +++ b/lib/features/postgresql/postgres_sql_editor_dialog.dart @@ -101,102 +101,93 @@ class _PostgresSqlEditorDialogState @override material.Widget build(material.BuildContext context) { final theme = Theme.of(context).colorScheme; - final radius = Theme.of(context).radiusXxl; return material.Dialog( backgroundColor: material.Colors.transparent, insetPadding: WindowLayout.dialogSymmetricInsets(context), - child: material.Container( + child: QueryaDialogCard( constraints: WindowLayout.dialogConstraints( context, maxWidth: 720, minWidth: 480, maxHeight: 520, ), - decoration: material.BoxDecoration( - color: theme.popover, - borderRadius: material.BorderRadius.circular(radius), - border: material.Border.all(color: theme.muted), - ), - child: material.ClipRRect( - borderRadius: material.BorderRadius.circular(radius), - child: material.Column( - mainAxisSize: material.MainAxisSize.min, - crossAxisAlignment: material.CrossAxisAlignment.stretch, - children: [ - material.Padding( - padding: const material.EdgeInsets.fromLTRB(24, 20, 24, 8), - child: material.Column( - crossAxisAlignment: material.CrossAxisAlignment.start, - children: [ - const Text('SQL query').large().semiBold(), - const material.SizedBox(height: 6), - const Text( - 'Table browse uses SELECT with LIMIT/OFFSET. ' - 'Edit it or write your own SELECT. Reset restores the table browse query. ' - 'Run reloads the grid from the database; if rows are unchanged, the view will look the same.', - ).muted().xSmall(), - ], - ), + borderColor: theme.muted, + child: material.Column( + mainAxisSize: material.MainAxisSize.min, + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + material.Padding( + padding: const material.EdgeInsets.fromLTRB(24, 20, 24, 8), + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + children: [ + const Text('SQL query').large().semiBold(), + const material.SizedBox(height: 6), + const Text( + 'Table browse uses SELECT with LIMIT/OFFSET. ' + 'Edit it or write your own SELECT. Reset restores the table browse query. ' + 'Run reloads the grid from the database; if rows are unchanged, the view will look the same.', + ).muted().xSmall(), + ], ), - material.Padding( - padding: const material.EdgeInsets.symmetric(horizontal: 24), - child: material.SizedBox( - height: 280, - child: material.Container( - decoration: - SqlEditorChrome.inlineFieldDecorationFromContext( - context, - ), - child: QueryaCodeEditor( - controller: _controller, - language: QueryaCodeLanguage.sql, - fontSize: 12, - variant: QueryaCodeEditorVariant.material, - textAlignVertical: material.TextAlignVertical.top, - hintText: 'SELECT …', - contentPadding: const material.EdgeInsets.all(12), - ), + ), + material.Padding( + padding: const material.EdgeInsets.symmetric(horizontal: 24), + child: material.SizedBox( + height: 280, + child: material.Container( + decoration: SqlEditorChrome.inlineFieldDecorationFromContext( + context, ), - ), - ), - if (_error != null) - material.Padding( - padding: const material.EdgeInsets.fromLTRB(24, 8, 24, 0), - child: material.Text( - _error!, - style: material.TextStyle( - color: theme.destructive, fontSize: 12), + child: QueryaCodeEditor( + controller: _controller, + language: QueryaCodeLanguage.sql, + fontSize: 12, + variant: QueryaCodeEditorVariant.material, + textAlignVertical: material.TextAlignVertical.top, + hintText: 'SELECT …', + contentPadding: const material.EdgeInsets.all(12), ), ), + ), + ), + if (_error != null) material.Padding( - padding: const material.EdgeInsets.all(20), - child: material.Row( - mainAxisAlignment: material.MainAxisAlignment.end, - children: [ - OutlineButton( - onPressed: () => material.Navigator.of(context).pop(), - child: const Text('Cancel'), - ), - const Gap(8), - OutlineButton( - onPressed: () { - setState(() { - _error = null; - _controller.text = widget.browseSql; - }); - }, - child: const Text('Reset'), - ), - const Gap(8), - PrimaryButton( - onPressed: _submit, - child: const Text('Run'), - ), - ], + padding: const material.EdgeInsets.fromLTRB(24, 8, 24, 0), + child: material.Text( + _error!, + style: material.TextStyle( + color: theme.destructive, fontSize: 12), ), ), - ], - ), + material.Padding( + padding: const material.EdgeInsets.all(20), + child: material.Row( + mainAxisAlignment: material.MainAxisAlignment.end, + children: [ + OutlineButton( + onPressed: () => material.Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + const Gap(8), + OutlineButton( + onPressed: () { + setState(() { + _error = null; + _controller.text = widget.browseSql; + }); + }, + child: const Text('Reset'), + ), + const Gap(8), + PrimaryButton( + onPressed: _submit, + child: const Text('Run'), + ), + ], + ), + ), + ], ), ), ); diff --git a/lib/features/postgresql/postgres_table_privileges_dialog.dart b/lib/features/postgresql/postgres_table_privileges_dialog.dart index c5234604..fc3a2f10 100644 --- a/lib/features/postgresql/postgres_table_privileges_dialog.dart +++ b/lib/features/postgresql/postgres_table_privileges_dialog.dart @@ -77,7 +77,6 @@ class _PrivilegesDialogBodyState extends material.State<_PrivilegesDialogBody> { @override material.Widget build(material.BuildContext context) { final theme = Theme.of(context).colorScheme; - final radius = Theme.of(context).radiusXxl; final mq = material.MediaQuery.sizeOf(context); final hInset = WindowLayout.dialogVerticalInset(mq.height) * 2; final wInset = WindowLayout.dialogHorizontalInset(mq.width) * 2; @@ -103,66 +102,59 @@ class _PrivilegesDialogBodyState extends material.State<_PrivilegesDialogBody> { child: material.SizedBox( width: dialogWidth, height: dialogHeight, - child: material.DecoratedBox( - decoration: material.BoxDecoration( - color: theme.popover, - borderRadius: material.BorderRadius.circular(radius), - border: material.Border.all(color: theme.muted), - ), - child: material.ClipRRect( - borderRadius: material.BorderRadius.circular(radius), - child: material.Column( - crossAxisAlignment: material.CrossAxisAlignment.stretch, - children: [ - material.Padding( - padding: const material.EdgeInsets.fromLTRB(20, 16, 20, 8), - child: material.Column( - crossAxisAlignment: material.CrossAxisAlignment.start, - mainAxisSize: material.MainAxisSize.min, - children: [ - const Text('Table privileges').large().semiBold(), - const material.SizedBox(height: 4), - material.Text( - '${widget.schema}.${widget.tableName}', - style: material.TextStyle( - fontFamily: 'monospace', - fontSize: 12, - color: theme.mutedForeground, - ), - maxLines: 2, - overflow: material.TextOverflow.ellipsis, + child: QueryaDialogCard( + borderColor: theme.muted, + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + material.Padding( + padding: const material.EdgeInsets.fromLTRB(20, 16, 20, 8), + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + mainAxisSize: material.MainAxisSize.min, + children: [ + const Text('Table privileges').large().semiBold(), + const material.SizedBox(height: 4), + material.Text( + '${widget.schema}.${widget.tableName}', + style: material.TextStyle( + fontFamily: 'monospace', + fontSize: 12, + color: theme.mutedForeground, ), - const material.SizedBox(height: 4), - const Text( - 'From information_schema.role_table_grants (read-only).', - ).muted().xSmall(), - ], - ), + maxLines: 2, + overflow: material.TextOverflow.ellipsis, + ), + const material.SizedBox(height: 4), + const Text( + 'From information_schema.role_table_grants (read-only).', + ).muted().xSmall(), + ], ), - const material.Divider(height: 1), - material.Expanded(child: _buildListArea(theme)), - const material.Divider(height: 1), - material.Padding( - padding: const material.EdgeInsets.all(12), - child: material.Row( - mainAxisAlignment: material.MainAxisAlignment.end, - children: [ + ), + const material.Divider(height: 1), + material.Expanded(child: _buildListArea(theme)), + const material.Divider(height: 1), + material.Padding( + padding: const material.EdgeInsets.all(12), + child: material.Row( + mainAxisAlignment: material.MainAxisAlignment.end, + children: [ + OutlineButton( + onPressed: () => material.Navigator.of(context).pop(), + child: const Text('Close'), + ), + if (!_loading && _error == null) ...[ + const Gap(8), OutlineButton( - onPressed: () => material.Navigator.of(context).pop(), - child: const Text('Close'), + onPressed: _load, + child: const Text('Reload'), ), - if (!_loading && _error == null) ...[ - const Gap(8), - OutlineButton( - onPressed: _load, - child: const Text('Reload'), - ), - ], ], - ), + ], ), - ], - ), + ), + ], ), ), ), diff --git a/lib/features/settings/preferences_dialog.dart b/lib/features/settings/preferences_dialog.dart index 7302877d..6d2554c6 100644 --- a/lib/features/settings/preferences_dialog.dart +++ b/lib/features/settings/preferences_dialog.dart @@ -96,215 +96,205 @@ class _PreferencesDialogContentState @override material.Widget build(material.BuildContext context) { final theme = Theme.of(context).colorScheme; - final radius = Theme.of(context).radiusXxl; final onPopover = theme.popoverForeground; return material.DefaultTextStyle( style: material.TextStyle(color: onPopover), child: material.IconTheme( data: material.IconThemeData(color: onPopover), - child: material.Container( + child: QueryaDialogCard( constraints: WindowLayout.dialogConstraints( context, maxWidth: WindowLayout.preferencesDialogMaxWidth, minWidth: WindowLayout.preferencesDialogMinWidth, maxHeight: WindowLayout.preferencesDialogMaxHeight, ), - decoration: material.BoxDecoration( - color: theme.popover, - borderRadius: material.BorderRadius.circular(radius), - border: material.Border.all(color: theme.border), - ), - child: material.ClipRRect( - borderRadius: material.BorderRadius.circular(radius), - child: material.Column( - crossAxisAlignment: material.CrossAxisAlignment.stretch, - children: [ - material.Padding( - padding: const material.EdgeInsets.fromLTRB(24, 24, 24, 8), - child: material.Column( - crossAxisAlignment: material.CrossAxisAlignment.start, - children: [ - const Text('Preferences').large().semiBold().foreground(), - const material.SizedBox(height: 6), - const PreferencesHint( - 'Changes apply immediately. SQL timeouts are global for all connections of that type.', - ), - ], - ), + borderColor: theme.border, + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + material.Padding( + padding: const material.EdgeInsets.fromLTRB(24, 24, 24, 8), + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + children: [ + const Text('Preferences').large().semiBold().foreground(), + const material.SizedBox(height: 6), + const PreferencesHint( + 'Changes apply immediately. SQL timeouts are global for all connections of that type.', + ), + ], ), - material.Expanded( - child: material.SingleChildScrollView( - padding: const material.EdgeInsets.symmetric( - horizontal: 24, vertical: 8), - child: _loading - ? const material.Center( - child: material.Padding( - padding: material.EdgeInsets.all(24), - child: material.CircularProgressIndicator(), + ), + material.Expanded( + child: material.SingleChildScrollView( + padding: const material.EdgeInsets.symmetric( + horizontal: 24, vertical: 8), + child: _loading + ? const material.Center( + child: material.Padding( + padding: material.EdgeInsets.all(24), + child: material.CircularProgressIndicator(), + ), + ) + : material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + children: [ + const Text('General') + .semiBold() + .small() + .foreground(), + const material.SizedBox(height: 8), + PreferencesCheckboxRow( + value: _checkUpdatesOnStartup, + title: const Text( + 'Automatically check for updates on startup', + ).small(), + subtitle: const Text( + 'Queries GitHub Releases silently when Querya starts.', + ).muted().xSmall(), + onChanged: (v) { + unawaited(_setCheckUpdatesOnStartup(v)); + }, ), - ) - : material.Column( - crossAxisAlignment: - material.CrossAxisAlignment.start, - children: [ - const Text('General') - .semiBold() - .small() - .foreground(), - const material.SizedBox(height: 8), - PreferencesCheckboxRow( - value: _checkUpdatesOnStartup, - title: const Text( - 'Automatically check for updates on startup', - ).small(), - subtitle: const Text( - 'Queries GitHub Releases silently when Querya starts.', - ).muted().xSmall(), - onChanged: (v) { - unawaited(_setCheckUpdatesOnStartup(v)); - }, - ), - const material.SizedBox(height: 24), - const PreferencesAppearanceSection(), - const material.SizedBox(height: 24), - const PreferencesExtensionsSection(), - const material.SizedBox(height: 24), - const Text('SQL — PostgreSQL') - .semiBold() - .small() - .foreground(), - const material.SizedBox(height: 8), - PreferencesFieldRow( - label: 'Statement timeout', - control: SqlStatementTimeoutDropdown( - value: _pgTimeout, - expandToParent: true, - onChanged: (v) => unawaited(_setPg(v)), - ), - ), - const material.SizedBox(height: 24), - const Text('SQL — MySQL / MariaDB') - .semiBold() - .small() - .foreground(), - const material.SizedBox(height: 8), - PreferencesFieldRow( - label: 'Statement timeout', - control: SqlStatementTimeoutDropdown( - value: _mysqlTimeout, - expandToParent: true, - onChanged: (v) => unawaited(_setMysql(v)), - ), + const material.SizedBox(height: 24), + const PreferencesAppearanceSection(), + const material.SizedBox(height: 24), + const PreferencesExtensionsSection(), + const material.SizedBox(height: 24), + const Text('SQL — PostgreSQL') + .semiBold() + .small() + .foreground(), + const material.SizedBox(height: 8), + PreferencesFieldRow( + label: 'Statement timeout', + control: SqlStatementTimeoutDropdown( + value: _pgTimeout, + expandToParent: true, + onChanged: (v) => unawaited(_setPg(v)), ), - const material.SizedBox(height: 24), - const Text('SQL editor') - .semiBold() - .small() - .foreground(), - const material.SizedBox(height: 8), - PreferencesFieldRow( - label: 'Max rows in results', - control: PreferencesDropdownMenu( - value: _maxRows, - onSelected: (v) { - if (v != null) unawaited(_setMaxRows(v)); - }, - entries: [ - for (final n in kSqlResultMaxRowsPresets) - material.DropdownMenuEntry( - value: n, - label: '$n', - ), - ], - ), - ), - const material.SizedBox(height: 12), - PreferencesFieldRow( - label: 'Query history limit', - hint: - 'Per connection and database; oldest queries are dropped.', - control: PreferencesDropdownMenu( - value: _historyMax, - onSelected: (v) { - if (v != null) { - unawaited(_setHistoryMax(v)); - } - }, - entries: [ - for (final n - in kSqlHistoryMaxEntriesPresets) - material.DropdownMenuEntry( - value: n, - label: '$n entries', - ), - ], - ), + ), + const material.SizedBox(height: 24), + const Text('SQL — MySQL / MariaDB') + .semiBold() + .small() + .foreground(), + const material.SizedBox(height: 8), + PreferencesFieldRow( + label: 'Statement timeout', + control: SqlStatementTimeoutDropdown( + value: _mysqlTimeout, + expandToParent: true, + onChanged: (v) => unawaited(_setMysql(v)), ), - const material.SizedBox(height: 12), - PreferencesFieldRow( - label: 'Font size', - control: PreferencesDropdownMenu( - value: _fontSize, - onSelected: (v) { - if (v != null) unawaited(_setFont(v)); - }, - entries: const [ - material.DropdownMenuEntry( - value: 11.0, - label: '11 pt', - ), - material.DropdownMenuEntry( - value: 12.0, - label: '12 pt', - ), - material.DropdownMenuEntry( - value: 13.0, - label: '13 pt', - ), - material.DropdownMenuEntry( - value: 14.0, - label: '14 pt', - ), + ), + const material.SizedBox(height: 24), + const Text('SQL editor') + .semiBold() + .small() + .foreground(), + const material.SizedBox(height: 8), + PreferencesFieldRow( + label: 'Max rows in results', + control: PreferencesDropdownMenu( + value: _maxRows, + onSelected: (v) { + if (v != null) unawaited(_setMaxRows(v)); + }, + entries: [ + for (final n in kSqlResultMaxRowsPresets) material.DropdownMenuEntry( - value: 16.0, - label: '16 pt', + value: n, + label: '$n', ), + ], + ), + ), + const material.SizedBox(height: 12), + PreferencesFieldRow( + label: 'Query history limit', + hint: + 'Per connection and database; oldest queries are dropped.', + control: PreferencesDropdownMenu( + value: _historyMax, + onSelected: (v) { + if (v != null) { + unawaited(_setHistoryMax(v)); + } + }, + entries: [ + for (final n in kSqlHistoryMaxEntriesPresets) material.DropdownMenuEntry( - value: 18.0, - label: '18 pt', + value: n, + label: '$n entries', ), - ], - ), + ], ), - const material.SizedBox(height: 16), - const PreferencesHint( - 'Preferences are stored locally in SQLite (non-secret keys only).', + ), + const material.SizedBox(height: 12), + PreferencesFieldRow( + label: 'Font size', + control: PreferencesDropdownMenu( + value: _fontSize, + onSelected: (v) { + if (v != null) unawaited(_setFont(v)); + }, + entries: const [ + material.DropdownMenuEntry( + value: 11.0, + label: '11 pt', + ), + material.DropdownMenuEntry( + value: 12.0, + label: '12 pt', + ), + material.DropdownMenuEntry( + value: 13.0, + label: '13 pt', + ), + material.DropdownMenuEntry( + value: 14.0, + label: '14 pt', + ), + material.DropdownMenuEntry( + value: 16.0, + label: '16 pt', + ), + material.DropdownMenuEntry( + value: 18.0, + label: '18 pt', + ), + ], ), - ], - ), + ), + const material.SizedBox(height: 16), + const PreferencesHint( + 'Preferences are stored locally in SQLite (non-secret keys only).', + ), + ], + ), + ), + ), + material.Container( + padding: const material.EdgeInsets.symmetric( + horizontal: 24, vertical: 16), + decoration: material.BoxDecoration( + border: material.Border( + top: material.BorderSide( + color: theme.border.withValues(alpha: 0.3)), ), ), - material.Container( - padding: const material.EdgeInsets.symmetric( - horizontal: 24, vertical: 16), - decoration: material.BoxDecoration( - border: material.Border( - top: material.BorderSide( - color: theme.border.withValues(alpha: 0.3)), + child: material.Row( + mainAxisAlignment: material.MainAxisAlignment.end, + children: [ + PrimaryButton( + onPressed: () => material.Navigator.of(context).pop(), + child: const Text('Close'), ), - ), - child: material.Row( - mainAxisAlignment: material.MainAxisAlignment.end, - children: [ - PrimaryButton( - onPressed: () => material.Navigator.of(context).pop(), - child: const Text('Close'), - ), - ], - ), + ], ), - ], - ), + ), + ], ), ), ), diff --git a/lib/features/updater/update_dialog.dart b/lib/features/updater/update_dialog.dart index c9582d94..6600fb91 100644 --- a/lib/features/updater/update_dialog.dart +++ b/lib/features/updater/update_dialog.dart @@ -228,89 +228,81 @@ class _UpdateDialogContentState extends material.State<_UpdateDialogContent> { @override material.Widget build(material.BuildContext context) { final theme = Theme.of(context).colorScheme; - final radius = Theme.of(context).radiusXxl; final wb = context.workbench; - return material.Container( + return QueryaDialogCard( constraints: WindowLayout.dialogConstraints( context, maxWidth: 520, minWidth: 360, maxHeight: 640, ), - decoration: material.BoxDecoration( - color: theme.popover, - borderRadius: material.BorderRadius.circular(radius), - border: material.Border.all(color: theme.muted), - ), - child: material.ClipRRect( - borderRadius: material.BorderRadius.circular(radius), - child: material.Column( - mainAxisSize: material.MainAxisSize.min, - crossAxisAlignment: material.CrossAxisAlignment.stretch, - children: [ - material.Padding( - padding: const material.EdgeInsets.fromLTRB(24, 24, 24, 8), - child: material.Column( - crossAxisAlignment: material.CrossAxisAlignment.start, - children: [ - material.Row( - children: [ - material.Icon( - material.Icons.system_update_alt_rounded, - color: wb.accent, - ), - const material.SizedBox(width: 10), - const Text('Software Update').large().semiBold(), - ], - ), - const material.SizedBox(height: 8), - Text(_subtitle()).muted().small(), - ], - ), - ), - material.Flexible( - child: material.SingleChildScrollView( - padding: const material.EdgeInsets.symmetric( - horizontal: 24, - vertical: 8, - ), - child: material.AnimatedSwitcher( - duration: context.motionDuration(QueryaMotion.standard), - switchInCurve: context.motionCurve(QueryaMotion.enter), - switchOutCurve: context.motionCurve(QueryaMotion.exit), - layoutBuilder: (currentChild, previousChildren) { - return material.Stack( - alignment: material.Alignment.topCenter, - children: [ - ...previousChildren, - if (currentChild != null) currentChild, - ], - ); - }, - child: material.KeyedSubtree( - key: material.ValueKey(_phase), - child: _body(context), - ), + borderColor: theme.muted, + child: material.Column( + mainAxisSize: material.MainAxisSize.min, + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + material.Padding( + padding: const material.EdgeInsets.fromLTRB(24, 24, 24, 8), + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + children: [ + material.Row( + children: [ + material.Icon( + material.Icons.system_update_alt_rounded, + color: wb.accent, + ), + const material.SizedBox(width: 10), + const Text('Software Update').large().semiBold(), + ], ), - ), + const material.SizedBox(height: 8), + Text(_subtitle()).muted().small(), + ], ), - material.Container( + ), + material.Flexible( + child: material.SingleChildScrollView( padding: const material.EdgeInsets.symmetric( horizontal: 24, - vertical: 16, + vertical: 8, + ), + child: material.AnimatedSwitcher( + duration: context.motionDuration(QueryaMotion.standard), + switchInCurve: context.motionCurve(QueryaMotion.enter), + switchOutCurve: context.motionCurve(QueryaMotion.exit), + layoutBuilder: (currentChild, previousChildren) { + return material.Stack( + alignment: material.Alignment.topCenter, + children: [ + ...previousChildren, + if (currentChild != null) currentChild, + ], + ); + }, + child: material.KeyedSubtree( + key: material.ValueKey(_phase), + child: _body(context), + ), ), - decoration: material.BoxDecoration( - border: material.Border( - top: material.BorderSide( - color: theme.border.withValues(alpha: 0.3), - ), + ), + ), + material.Container( + padding: const material.EdgeInsets.symmetric( + horizontal: 24, + vertical: 16, + ), + decoration: material.BoxDecoration( + border: material.Border( + top: material.BorderSide( + color: theme.border.withValues(alpha: 0.3), ), ), - child: _actions(context), ), - ], - ), + child: _actions(context), + ), + ], ), ); } diff --git a/lib/shared/widgets/querya_dialog_card.dart b/lib/shared/widgets/querya_dialog_card.dart new file mode 100644 index 00000000..29f71fd8 --- /dev/null +++ b/lib/shared/widgets/querya_dialog_card.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart' as material; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// Dialog shell: [Material] popover fill (ListTile ink host) under the same +/// [Container] constraints as the pre-migration shell. +/// +/// Re-applies the ambient [DefaultTextStyle] / [IconTheme] after [Material], +/// which would otherwise inject [ThemeData.textTheme] and bloat dense dialog +/// chrome (Extension Manager overflow). +class QueryaDialogCard extends material.StatelessWidget { + const QueryaDialogCard({ + super.key, + required this.child, + this.constraints, + this.borderColor, + }); + + final material.Widget child; + final material.BoxConstraints? constraints; + final material.Color? borderColor; + + @override + material.Widget build(material.BuildContext context) { + final theme = Theme.of(context).colorScheme; + final radius = Theme.of(context).radiusXxl; + final borderRadius = material.BorderRadius.circular(radius); + final textStyle = material.DefaultTextStyle.of(context).style; + final iconTheme = material.IconTheme.of(context); + + final card = material.Material( + color: theme.popover, + elevation: 0, + shape: material.RoundedRectangleBorder( + borderRadius: borderRadius, + side: material.BorderSide(color: borderColor ?? theme.border), + ), + clipBehavior: material.Clip.antiAlias, + child: material.DefaultTextStyle( + style: textStyle, + child: material.IconTheme( + data: iconTheme, + child: child, + ), + ), + ); + + if (constraints == null) return card; + + return material.Container( + constraints: constraints, + child: card, + ); + } +} diff --git a/lib/shared/widgets/tree_load_error.dart b/lib/shared/widgets/tree_load_error.dart index 7bc6afdb..aabaee49 100644 --- a/lib/shared/widgets/tree_load_error.dart +++ b/lib/shared/widgets/tree_load_error.dart @@ -4,10 +4,13 @@ import 'package:querya_desktop/core/ui/querya_icons.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; /// Inline error block for connection tree lazy-load failures. +/// +/// Always shows the error icon + title row by default (Mongo dialect); +/// pass [showTitleRow]: false only for ultra-compact one-liners. class TreeLoadError extends material.StatelessWidget { const TreeLoadError({ super.key, - this.title, + this.title = 'Could not load', required this.message, this.onRetry, this.retryLabel = 'Retry', @@ -17,10 +20,10 @@ class TreeLoadError extends material.StatelessWidget { bottom: 8, ), this.detailFontSize = 11, - this.showTitleRow = false, + this.showTitleRow = true, }); - final String? title; + final String title; final String message; final VoidCallback? onRetry; final String retryLabel; @@ -40,7 +43,7 @@ class TreeLoadError extends material.StatelessWidget { crossAxisAlignment: material.CrossAxisAlignment.start, mainAxisSize: material.MainAxisSize.min, children: [ - if (showTitleRow && title != null) + if (showTitleRow) material.Row( crossAxisAlignment: material.CrossAxisAlignment.start, children: [ @@ -52,7 +55,7 @@ class TreeLoadError extends material.StatelessWidget { const Gap(6), material.Expanded( child: material.Text( - title!, + title, maxLines: 2, overflow: material.TextOverflow.ellipsis, style: material.TextStyle( @@ -63,7 +66,7 @@ class TreeLoadError extends material.StatelessWidget { ), ], ), - if (showTitleRow && title != null) const Gap(6), + if (showTitleRow) const Gap(6), material.SelectableText( message, style: material.TextStyle( diff --git a/lib/shared/widgets/widgets.dart b/lib/shared/widgets/widgets.dart index 4c9f5bbe..49ae9541 100644 --- a/lib/shared/widgets/widgets.dart +++ b/lib/shared/widgets/widgets.dart @@ -11,6 +11,7 @@ library; export 'app_dialog.dart'; export 'app_toast.dart'; export 'export_menu_button.dart'; +export 'querya_dialog_card.dart'; export 'querya_tab_strip.dart'; export 'querya_dropdown.dart' show diff --git a/test/shared/widgets/tree_load_error_test.dart b/test/shared/widgets/tree_load_error_test.dart index 3aa281f8..4e79bf53 100644 --- a/test/shared/widgets/tree_load_error_test.dart +++ b/test/shared/widgets/tree_load_error_test.dart @@ -17,8 +17,10 @@ void main() { ), ); + expect(find.text('Could not load'), findsOneWidget); expect(find.text('connection refused'), findsOneWidget); expect(find.text('Retry'), findsOneWidget); + expect(find.byIcon(material.Icons.error_outline_rounded), findsOneWidget); await tester.tap(find.text('Retry')); await tester.pump(); @@ -26,18 +28,32 @@ void main() { expect(retried, isTrue); }); - testWidgets('TreeLoadError title row uses error icon', (tester) async { + testWidgets('TreeLoadError title row uses custom title', (tester) async { await tester.pumpWidget( queryaThemeTestShell( child: const TreeLoadError( - title: 'Could not load', + title: 'Could not load databases', message: 'timeout', - showTitleRow: true, ), ), ); - expect(find.text('Could not load'), findsOneWidget); + expect(find.text('Could not load databases'), findsOneWidget); expect(find.byIcon(material.Icons.error_outline_rounded), findsOneWidget); }); + + testWidgets('TreeLoadError can hide title row', (tester) async { + await tester.pumpWidget( + queryaThemeTestShell( + child: const TreeLoadError( + message: 'timeout', + showTitleRow: false, + ), + ), + ); + + expect(find.text('Could not load'), findsNothing); + expect(find.byIcon(material.Icons.error_outline_rounded), findsNothing); + expect(find.text('timeout'), findsOneWidget); + }); }