[client] Basic grid view (no switching)

This commit is contained in:
Abhishek Shroff
2025-08-02 13:53:30 +05:30
parent 7bc7237f39
commit 9c94c1840e
5 changed files with 278 additions and 31 deletions

View File

@@ -22,6 +22,7 @@ import 'package:super_clipboard/super_clipboard.dart';
import 'explorer_controller.dart';
import 'folder_empty_view.dart';
import 'folder_grid_view.dart';
import 'folder_list_view.dart';
class ExplorerView extends StatefulWidget {
@@ -154,7 +155,7 @@ class _ExplorerViewState extends State<ExplorerView> {
return RefreshIndicator(
key: _refreshKey,
onRefresh: context.read<ExplorerController>().refresh,
child: const FolderListView(),
child: const FolderGridView(),
);
}
}

View File

@@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
import 'package:phylum/libphylum/db/db.dart';
import 'package:phylum/libphylum/phylum_account.dart';
import 'package:phylum/ui/explorer/resource_icon_extension.dart';
import 'package:phylum/util/file_size.dart';
import 'package:phylum/util/time.dart';
import 'package:provider/provider.dart';
import 'explorer_controller.dart';
class FolderGridItem extends StatelessWidget {
final Resource resource;
final bool dropTargetActive;
FolderGridItem({required this.resource, required this.dropTargetActive}) : super(key: ValueKey(resource.id));
@override
Widget build(BuildContext context) {
final account = context.read<PhylumAccount>();
final showBorder = context.select<ExplorerState, bool>((state) => state.focusId == resource.id && state.showFocus);
final highlight = context.select<ExplorerState, bool>((state) => state.isSelected(resource.id));
final dim = context.select<ExplorerState, bool>((state) => state.isSelected(resource.id) && state.dragging);
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final border = dropTargetActive
? BorderSide(color: colorScheme.secondary, width: 2.0)
: showBorder
? BorderSide(color: colorScheme.primary, width: 2.0)
: const BorderSide(color: Colors.transparent, width: 2.0);
final color = highlight
? dim
? colorScheme.primaryContainer.withAlpha(192)
: colorScheme.primaryContainer
: dropTargetActive
? colorScheme.secondaryContainer
: null;
return IconTheme(
data: IconThemeData(
color: colorScheme.onSurfaceVariant,
),
child: Card(
color: color,
borderOnForeground: true,
clipBehavior: Clip.antiAlias,
shape: dropTargetActive || showBorder
? RoundedRectangleBorder(side: border, borderRadius: BorderRadius.circular(12.0))
: null,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(child: resource.getIcon(account)),
Container(
decoration: BoxDecoration(color: Theme.of(context).colorScheme.surfaceContainerLow),
padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
resource.name,
maxLines: 1,
softWrap: false,
overflow: TextOverflow.fade,
style: theme.textTheme.bodyLarge!.copyWith(
color: highlight
? dim
? colorScheme.primary.withAlpha(192)
: colorScheme.primary
: colorScheme.onSurface,
),
),
StreamBuilder(
stream: account.db.latestVersion(resource.id).watchSingleOrNull(),
builder: (context, snapshot) {
String subtitle = "";
if (resource.modified != null) {
subtitle = resource.modified!.formatShort();
}
final data = snapshot.data;
if (data != null) {
if (subtitle.isNotEmpty) {
subtitle += " \u2022 ";
}
subtitle += data.size.formatForDisplay();
}
return Text(
subtitle,
maxLines: 1,
softWrap: false,
overflow: TextOverflow.fade,
style: theme.textTheme.bodyMedium!.copyWith(
color: highlight
? dim
? colorScheme.primary.withAlpha(192)
: colorScheme.primary
: colorScheme.onSurfaceVariant,
),
);
}),
],
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,135 @@
import 'package:flutter/material.dart';
import 'package:phylum/ui/app/shortcuts.dart';
import 'package:phylum/libphylum/db/db.dart';
import 'package:phylum/ui/explorer/folder_grid_item.dart';
import 'package:phylum/ui/explorer/resource_drop_and_drop.dart';
import 'package:phylum/ui/explorer/resource_item_gesture_handler.dart';
import 'package:provider/provider.dart';
import 'explorer_controller.dart';
const _rowSpacing = 12.0;
class FolderGridView extends StatefulWidget {
const FolderGridView({super.key});
@override
State<FolderGridView> createState() => _FolderGridViewState();
}
class _FolderGridViewState extends State<FolderGridView> {
double _rowHeight = 0.0;
double _viewportHeight = 0.0;
int _crossAxisCount = 1;
final _scrollController = ScrollController(debugLabel: "ResourceItemGrid");
@override
void initState() {
super.initState();
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void ensureVisible(int index) {
final currentOffset = _scrollController.offset;
final itemStart = (index ~/ _crossAxisCount) * (_rowHeight + _rowSpacing);
if (itemStart < currentOffset) {
_scrollController.jumpTo(itemStart);
} else if (currentOffset <= itemStart + _rowHeight + -_viewportHeight) {
_scrollController.jumpTo(itemStart - _viewportHeight + _rowHeight);
}
}
@override
Widget build(BuildContext context) {
final resources = context.select<ExplorerState, List<Resource>>((state) => state.resources);
return LayoutBuilder(builder: (context, constraints) {
_viewportHeight = constraints.maxHeight;
_crossAxisCount = constraints.maxWidth ~/ 240;
_rowHeight = constraints.maxWidth / _crossAxisCount;
return Actions(
actions: {
FocusUpIntent: CallbackAction<FocusUpIntent>(onInvoke: (i) {
final index = context
.read<ExplorerController>()
.updateSelection((i) => i < _crossAxisCount ? i : i - _crossAxisCount, i.mode, true);
ensureVisible(index);
return null;
}),
FocusDownIntent: CallbackAction<FocusDownIntent>(onInvoke: (i) {
final index = context.read<ExplorerController>().updateSelection(
(i) => i < 0
? 0
: i + _crossAxisCount >= resources.length
? i
: i + _crossAxisCount,
i.mode,
true);
ensureVisible(index);
return null;
}),
FocusLeftIntent: CallbackAction<FocusLeftIntent>(onInvoke: (i) {
final index = context.read<ExplorerController>().updateSelection((i) => i - 1, i.mode, true);
ensureVisible(index);
return null;
}),
FocusRightIntent: CallbackAction<FocusRightIntent>(onInvoke: (i) {
final index = context.read<ExplorerController>().updateSelection((i) => i + 1, i.mode, true);
ensureVisible(index);
return null;
}),
FocusFirstIntent: CallbackAction<FocusFirstIntent>(onInvoke: (i) {
final index = context.read<ExplorerController>().updateSelection((i) => 0, i.mode, true);
ensureVisible(index);
return null;
}),
FocusLastIntent: CallbackAction<FocusLastIntent>(onInvoke: (i) {
final index = context.read<ExplorerController>().updateSelection((i) => 1 << 31, i.mode, true);
ensureVisible(index);
return null;
}),
},
child: Focus(
autofocus: true,
descendantsAreFocusable: false,
child: GridView.builder(
itemCount: resources.length,
controller: _scrollController,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _crossAxisCount,
crossAxisSpacing: 12,
mainAxisSpacing: _rowSpacing,
mainAxisExtent: _rowHeight,
),
itemBuilder: (context, index) {
final resource = resources[index];
return (resource.dir)
? ResourceDragTarget(
resourceId: resource.id,
buildItem: (context, dropTargetActive) => ResourceItemGestureHandler(
index: index,
resource: resource,
buildItem: () => FolderGridItem(
resource: resource,
dropTargetActive: dropTargetActive,
),
),
)
: ResourceItemGestureHandler(
index: index,
resource: resource,
buildItem: () => FolderGridItem(
resource: resource,
dropTargetActive: false,
),
);
}),
),
);
});
}
}

View File

@@ -62,7 +62,7 @@ class _FolderListViewState extends State<FolderListView> {
return null;
}),
FocusLastIntent: CallbackAction<FocusLastIntent>(onInvoke: (i) {
final index = context.read<ExplorerController>().updateSelection((i) => 4294967296, i.mode, true);
final index = context.read<ExplorerController>().updateSelection((i) => 1 << 31, i.mode, true);
ensureVisible(index);
return null;
}),
@@ -71,7 +71,7 @@ class _FolderListViewState extends State<FolderListView> {
autofocus: true,
descendantsAreFocusable: false,
child: LayoutBuilder(builder: (context, constraints) {
_rowHeight ??= calculateRowHeight(context);
_rowHeight ??= ResourceDetailsRow.calculateHeight(context);
_viewportHeight = constraints.maxHeight;
return ListView.builder(
itemCount: resources.length,

View File

@@ -19,34 +19,6 @@ const _iconPaddingH = 16.0;
const _iconSize = 24.0;
const _subtitleIconSize = 14.0;
double calculateRowHeight(BuildContext context) {
final theme = Theme.of(context);
final scaler = MediaQuery.textScalerOf(context);
final titleSize = (TextPainter(
text: TextSpan(text: 'A', style: theme.textTheme.bodyLarge!),
maxLines: 1,
textScaler: scaler,
textDirection: TextDirection.ltr)
..layout())
.size
.height;
final subtitleTextSize = (TextPainter(
text: TextSpan(text: 'A', style: theme.textTheme.bodyMedium!),
maxLines: 1,
textScaler: scaler,
textDirection: TextDirection.ltr)
..layout())
.size
.height;
// final subtitleTextSize = scaler.scale(theme.textTheme.bodyMedium!.fontSize!);
final subtitleSize = subtitleTextSize > _subtitleIconSize ? subtitleTextSize : _subtitleIconSize;
final contentSize = titleSize + subtitleSize;
final iconSize = _iconSize + (_iconPaddingV * 2);
final totalSize = (2 * _rowPaddingV) + (iconSize > contentSize ? iconSize : contentSize);
// print('$totalSize $titleSize $subtitleSize ($subtitleTextSize $_subtitleIconSize)');
return totalSize;
}
class ResourceDetailsRow extends StatelessWidget {
final Resource resource;
final bool dropTargetActive;
@@ -205,4 +177,32 @@ class ResourceDetailsRow extends StatelessWidget {
)),
);
}
static double calculateHeight(BuildContext context) {
final theme = Theme.of(context);
final scaler = MediaQuery.textScalerOf(context);
final titleSize = (TextPainter(
text: TextSpan(text: 'A', style: theme.textTheme.bodyLarge!),
maxLines: 1,
textScaler: scaler,
textDirection: TextDirection.ltr)
..layout())
.size
.height;
final subtitleTextSize = (TextPainter(
text: TextSpan(text: 'A', style: theme.textTheme.bodyMedium!),
maxLines: 1,
textScaler: scaler,
textDirection: TextDirection.ltr)
..layout())
.size
.height;
// final subtitleTextSize = scaler.scale(theme.textTheme.bodyMedium!.fontSize!);
final subtitleSize = subtitleTextSize > _subtitleIconSize ? subtitleTextSize : _subtitleIconSize;
final contentSize = titleSize + subtitleSize;
final iconSize = _iconSize + (_iconPaddingV * 2);
final totalSize = (2 * _rowPaddingV) + (iconSize > contentSize ? iconSize : contentSize);
// print('$totalSize $titleSize $subtitleSize ($subtitleTextSize $_subtitleIconSize)');
return totalSize;
}
}