mirror of
https://codeberg.org/shroff/phylum.git
synced 2026-01-06 03:31:02 -06:00
[client] Basic grid view (no switching)
This commit is contained in:
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
111
client/lib/ui/explorer/folder_grid_item.dart
Normal file
111
client/lib/ui/explorer/folder_grid_item.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
135
client/lib/ui/explorer/folder_grid_view.dart
Normal file
135
client/lib/ui/explorer/folder_grid_view.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user