diff --git a/lib/core/sdui/sdui_tree_builder.dart b/lib/core/sdui/sdui_tree_builder.dart index 69d06c04..7ab1795c 100644 --- a/lib/core/sdui/sdui_tree_builder.dart +++ b/lib/core/sdui/sdui_tree_builder.dart @@ -50,7 +50,7 @@ class SduiTreeBuilderState extends material.State { final Set _expanded = {}; final Map _expandErrors = {}; - static const double _rowExtent = 36; + static const double _rowExtent = 28; @override void initState() { @@ -180,17 +180,27 @@ class SduiTreeBuilderState extends material.State { } material.Widget _buildNodeRow(SduiTreeNode node, {required int depth}) { + final theme = material.Theme.of(context); + final muted = theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.85); + final primary = theme.colorScheme.primary; final canExpand = node.expandable || node.hasChildren; final isExpanded = _expanded.contains(node.id); final isLoading = _loading.contains(node.id); final nodeKind = _resolveNodeKind(node); final isBrowsable = nodeKind == 'table' || nodeKind == 'view'; + final iconSize = canExpand + ? QueryaIconSizes.treeGroup + : QueryaIconSizes.treeLeaf; + final iconColor = isBrowsable + ? primary.withValues(alpha: 0.5) + : muted; + final rowLeft = 8.0 + depth * 16.0 + (canExpand ? 0 : 4.0); return material.InkWell( onTap: isBrowsable ? () => widget.onNodeSelected?.call(node) : null, child: material.Padding( padding: material.EdgeInsets.only( - left: 8.0 + depth * 16.0, + left: rowLeft, right: 8, ), child: material.Row( @@ -201,7 +211,7 @@ class SduiTreeBuilderState extends material.State { height: 28, child: material.IconButton( padding: material.EdgeInsets.zero, - iconSize: 18, + iconSize: QueryaIconSizes.treeExpand, onPressed: () { if (isExpanded) { _onCollapse(node); @@ -209,10 +219,14 @@ class SduiTreeBuilderState extends material.State { _onExpand(node); } }, - icon: material.Icon( - isExpanded - ? material.Icons.expand_more - : material.Icons.chevron_right, + icon: material.AnimatedRotation( + turns: isExpanded ? 0.25 : 0, + duration: const Duration(milliseconds: 160), + curve: material.Curves.easeOutCubic, + child: const material.Icon( + QueryaIcons.expandClosed, + size: QueryaIconSizes.treeExpand, + ), ), ), ) @@ -230,7 +244,8 @@ class SduiTreeBuilderState extends material.State { node.icon, expandable: node.expandable, ), - size: QueryaIconSizes.sduiNode, + size: iconSize, + color: iconColor, ), const Gap(8), material.Expanded( @@ -239,7 +254,8 @@ class SduiTreeBuilderState extends material.State { overflow: material.TextOverflow.ellipsis, maxLines: 1, style: material.TextStyle( - fontSize: 12, + fontSize: 11, + color: isBrowsable ? theme.colorScheme.onSurface : muted, fontWeight: isBrowsable ? material.FontWeight.w600 : null, ), ), diff --git a/lib/core/ui/querya_icons.dart b/lib/core/ui/querya_icons.dart index f1d4daf7..297dcf4e 100644 --- a/lib/core/ui/querya_icons.dart +++ b/lib/core/ui/querya_icons.dart @@ -65,10 +65,10 @@ abstract final class QueryaIcons { case 'database': return database; case 'table': - return tableGroup; + return expandable ? tableGroup : tableLeaf; case 'view': case 'eye': - return viewGroup; + return expandable ? viewGroup : viewLeaf; case 'folder': case 'folder-table': return folder; diff --git a/lib/features/connections/connections_panel_extension.dart b/lib/features/connections/connections_panel_extension.dart index 19036f2e..28743426 100644 --- a/lib/features/connections/connections_panel_extension.dart +++ b/lib/features/connections/connections_panel_extension.dart @@ -184,7 +184,7 @@ class _ExtensionConnectionTileState extends State<_ExtensionConnectionTile> { duration: context.motionDuration(QueryaMotion.fast), curve: context.motionCurve(QueryaMotion.standardCurve), child: material.Icon( - material.Icons.chevron_right_rounded, + QueryaIcons.expandClosed, size: 16, color: theme.colorScheme.mutedForeground, ), diff --git a/lib/features/connections/connections_panel_mysql.dart b/lib/features/connections/connections_panel_mysql.dart index 98fe6e5c..eaf0e582 100644 --- a/lib/features/connections/connections_panel_mysql.dart +++ b/lib/features/connections/connections_panel_mysql.dart @@ -657,7 +657,7 @@ class _MysqlObjectGroupState extends State<_MysqlObjectGroup> { context: context, itemCount: widget.items.length, itemExtent: kConnectionTreeRowExtent, - padding: const material.EdgeInsets.only(left: 22), + padding: const material.EdgeInsets.only(left: 26), itemBuilder: (context, index) { final item = widget.items[index]; return _PgTreeRow( diff --git a/lib/features/connections/connections_panel_pg_tree.dart b/lib/features/connections/connections_panel_pg_tree.dart index a24d20d6..4281af75 100644 --- a/lib/features/connections/connections_panel_pg_tree.dart +++ b/lib/features/connections/connections_panel_pg_tree.dart @@ -1035,7 +1035,7 @@ class _PgObjectGroupState extends State<_PgObjectGroup> { context: context, itemCount: widget.items.length, itemExtent: kConnectionTreeRowExtent, - padding: const material.EdgeInsets.only(left: 22), + padding: const material.EdgeInsets.only(left: 26), itemBuilder: (context, index) { final item = widget.items[index]; return _PgTreeRow( diff --git a/lib/features/connections/connections_panel_redis.dart b/lib/features/connections/connections_panel_redis.dart index c23254e6..2e01abfa 100644 --- a/lib/features/connections/connections_panel_redis.dart +++ b/lib/features/connections/connections_panel_redis.dart @@ -258,22 +258,24 @@ class _RedisConnectionTileState extends State<_RedisConnectionTile> { ), ), if (_error != null) - material.Padding( + TreeLoadError( + message: _error!, padding: const material.EdgeInsets.only( - left: 28, top: 4, bottom: 4), - child: material.Text( - 'Error', - overflow: material.TextOverflow.ellipsis, - maxLines: 1, - style: material.TextStyle( - fontSize: 11, color: theme.colorScheme.destructive), + left: 28, + top: 4, + bottom: 4, ), + onRetry: _loadDatabases, ), - for (final db in _databases) - _RedisDatabaseNode( - index: db.index, - keys: db.keys, - onTap: () => widget.onDatabaseTap?.call(db.index), + if (_databases.isNotEmpty) + _RedisDatabasesNode( + connection: widget.connection, + databases: _databases, + onRefreshDatabases: () { + setState(() => _databases = []); + _loadDatabases(); + }, + onDatabaseTap: widget.onDatabaseTap, ), ], ), @@ -285,6 +287,60 @@ class _RedisConnectionTileState extends State<_RedisConnectionTile> { } } +class _RedisDatabasesNode extends material.StatelessWidget { + const _RedisDatabasesNode({ + required this.connection, + required this.databases, + required this.onRefreshDatabases, + this.onDatabaseTap, + }); + + final ConnectionRow connection; + final List<({int index, int keys})> databases; + final VoidCallback onRefreshDatabases; + final void Function(int database)? onDatabaseTap; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return material.Padding( + padding: const material.EdgeInsets.only(left: 20), + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + mainAxisSize: material.MainAxisSize.min, + children: [ + _PgTreeRow( + label: 'Databases (${databases.length})', + icon: QueryaIcons.databasesFolder, + iconSize: QueryaIconSizes.treeConnection, + iconColor: theme.colorScheme.primary.withValues(alpha: 0.7), + textStyle: material.TextStyle( + fontSize: 12, + color: theme.colorScheme.foreground, + ), + verticalPadding: 4, + onTap: null, + connection: connection, + onContextRefresh: onRefreshDatabases, + ), + lazyConnectionTreeList( + context: context, + itemCount: databases.length, + itemBuilder: (context, index) { + final db = databases[index]; + return _RedisDatabaseNode( + index: db.index, + keys: db.keys, + onTap: () => onDatabaseTap?.call(db.index), + ); + }, + ), + ], + ), + ); + } +} + class _RedisDatabaseNode extends StatelessWidget { const _RedisDatabaseNode({ required this.index, @@ -300,49 +356,31 @@ class _RedisDatabaseNode extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); return material.Padding( - padding: const material.EdgeInsets.only(left: 24), - child: material.MouseRegion( - cursor: material.SystemMouseCursors.click, - child: material.InkWell( - onTap: onTap, - borderRadius: material.BorderRadius.circular(6), - child: material.Padding( - padding: - const material.EdgeInsets.symmetric(horizontal: 8, vertical: 5), - child: material.Row( - children: [ - material.Icon( - QueryaIcons.databasesFolder, - size: 14, - color: keys > 0 - ? theme.colorScheme.primary.withValues(alpha: 0.7) - : theme.colorScheme.mutedForeground - .withValues(alpha: 0.5), + padding: const material.EdgeInsets.only(left: 16), + child: _PgTreeRow( + label: 'db$index', + icon: QueryaIcons.database, + iconSize: QueryaIconSizes.treeConnection, + iconColor: keys > 0 + ? theme.colorScheme.primary.withValues(alpha: 0.7) + : theme.colorScheme.mutedForeground.withValues(alpha: 0.5), + trailing: keys > 0 + ? material.Text( + '$keys', + style: material.TextStyle( + fontSize: 10, + color: theme.colorScheme.mutedForeground, ), - const Gap(8), - material.Expanded( - child: material.Text( - 'db$index', - overflow: material.TextOverflow.ellipsis, - maxLines: 1, - style: material.TextStyle( - fontSize: 12, - color: keys > 0 - ? theme.colorScheme.foreground - : theme.colorScheme.mutedForeground, - ), - ), - ), - if (keys > 0) - material.Text( - '$keys', - style: material.TextStyle( - fontSize: 10, color: theme.colorScheme.mutedForeground), - ), - ], - ), - ), + ) + : null, + textStyle: material.TextStyle( + fontSize: 12, + color: keys > 0 + ? theme.colorScheme.foreground + : theme.colorScheme.mutedForeground, ), + verticalPadding: 3, + onTap: onTap, ), ); } diff --git a/lib/features/connections/connections_panel_sqlite.dart b/lib/features/connections/connections_panel_sqlite.dart index 2efdee59..1456068c 100644 --- a/lib/features/connections/connections_panel_sqlite.dart +++ b/lib/features/connections/connections_panel_sqlite.dart @@ -366,7 +366,7 @@ class _SqliteObjectGroupState extends State<_SqliteObjectGroup> { curve: context.motionCurve(QueryaMotion.standardCurve), child: material.Icon( QueryaIcons.expandClosed, - size: 13, + size: QueryaIconSizes.treeExpand, color: theme.colorScheme.mutedForeground, ), ), @@ -387,7 +387,7 @@ class _SqliteObjectGroupState extends State<_SqliteObjectGroup> { context: context, itemCount: widget.items.length, itemExtent: kConnectionTreeRowExtent, - padding: const material.EdgeInsets.only(left: 22), + padding: const material.EdgeInsets.only(left: 26), itemBuilder: (context, index) { final item = widget.items[index]; return _PgTreeRow( diff --git a/test/core/sdui/sdui_builders_test.dart b/test/core/sdui/sdui_builders_test.dart index 3181f68e..dd432534 100644 --- a/test/core/sdui/sdui_builders_test.dart +++ b/test/core/sdui/sdui_builders_test.dart @@ -4,6 +4,7 @@ import 'package:querya_desktop/core/sdui/sdui_form_builder.dart'; import 'package:querya_desktop/core/sdui/sdui_form_schema.dart'; import 'package:querya_desktop/core/sdui/sdui_tree_builder.dart'; import 'package:querya_desktop/core/sdui/sdui_tree_schema.dart'; +import 'package:querya_desktop/core/ui/querya_icons.dart'; import '../../support/querya_theme_test_shell.dart'; @@ -206,7 +207,7 @@ void main() { expect(find.text('Databases'), findsOneWidget); expect(find.text('analytics'), findsNothing); - await tester.tap(find.byIcon(material.Icons.chevron_right)); + await tester.tap(find.byIcon(QueryaIcons.expandClosed)); await tester.pumpAndSettle(); expect(fetches, 1); diff --git a/test/core/ui/querya_icons_test.dart b/test/core/ui/querya_icons_test.dart index bdeb3b58..7f93df88 100644 --- a/test/core/ui/querya_icons_test.dart +++ b/test/core/ui/querya_icons_test.dart @@ -53,9 +53,21 @@ void main() { QueryaIcons.database, ); expect( - QueryaIcons.sduiNodeIcon('table', expandable: false), + QueryaIcons.sduiNodeIcon('table', expandable: true), QueryaIcons.tableGroup, ); + expect( + QueryaIcons.sduiNodeIcon('table', expandable: false), + QueryaIcons.tableLeaf, + ); + expect( + QueryaIcons.sduiNodeIcon('view', expandable: true), + QueryaIcons.viewGroup, + ); + expect( + QueryaIcons.sduiNodeIcon('view', expandable: false), + QueryaIcons.viewLeaf, + ); expect( QueryaIcons.sduiNodeIcon(null, expandable: true), QueryaIcons.folder,