Fix sheet peek for async content, and rework several screens' details
showNooSheet's peek detection previously measured content once, right
after the first frame - Details' Versions/Activity tabs fetch their
data asynchronously, so that one-shot measurement only ever saw the
initial loading-spinner height, never the eventual (often long) list,
and the sheet still snapped to full height once data arrived. Replaced
it with a ScrollMetricsNotification-based check that keeps content
capped at the peek height from frame one and upgrades to a draggable
sheet whenever it actually overflows that cap, however and whenever
that happens - fixed for good this time, with a regression test that
mirrors the async-load shape.
Also, in the same pass:
- Upload/Move/Copy's bottom bar meta line now always reads as a count
("Uploading 3 items"), never a filename for the single-item case,
and is centered rather than left-aligned - matches the approved
design and removes a bespoke marquee/chip implementation that only
Upload had.
- Upload/Move/Copy now show the same FilesControlsRow (sort, filter,
List/Grid) Files itself uses, including a folder-grid rendering
path for when List/Grid is set to grid.
- Files' filter chip reads "Filters" instead of "All types" when idle.
- Trash's overflow button - whose only action is "Delete forever" -
is now a direct delete icon (mobile) / "Delete" danger pill
(desktop) instead of a generic "..." menu that hid a single action
behind an extra tap.
- The media viewer's floating chrome (top bar, bottom action bar,
video transport row) now follows the app's light/dark theme instead
of a fixed dark panel - a dark panel read as out of place over a
light-theme stage. Panel tint and icon/text colors come from the
theme's own surface/fg1 tokens (at a higher blur opacity to stay
legible over arbitrary photo/video brightness), and the edge scrim
gradients fade toward the panel's own tint instead of a fixed black.
Mirrored the same change into the design canvas's Main.dc.html/
MainAndroid.dc.html mockups (now with a theme prop) so they match.
This commit is contained in:
@@ -379,9 +379,9 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
|
||||
// The stage follows the app's own theme (`bg`) rather than being pinned
|
||||
// to black - a black stage in light mode read as jarringly out of place
|
||||
// next to the rest of the light-themed app. The floating top/action
|
||||
// bars stay a fixed dark frosted panel regardless (see
|
||||
// FrostedGlassContainer's doc comment) since they need to read over
|
||||
// arbitrary photo/video brightness, not over this background.
|
||||
// bars now follow the theme too (FrostedGlassContainer's own `surface`
|
||||
// default), at a higher blur opacity to stay legible over arbitrary
|
||||
// photo/video brightness underneath.
|
||||
final stageColor = colors.bg;
|
||||
|
||||
return Scaffold(
|
||||
@@ -413,6 +413,10 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
|
||||
// gradient fade, not `FrostedGlassContainer`'s blur - just
|
||||
// enough to keep the back button/title legible over bright
|
||||
// media without another blurred layer stacked on the bar itself.
|
||||
// Fades toward `surface` (the same tint the panel itself uses),
|
||||
// not a fixed black - a black scrim would fight the panel's own
|
||||
// theme-matched icons/text (dark in light theme) instead of
|
||||
// extending its contrast strategy past the panel's hard edge.
|
||||
if (_isSwipeable)
|
||||
IgnorePointer(
|
||||
child: AnimatedOpacity(
|
||||
@@ -420,11 +424,14 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
|
||||
opacity: _controlsVisible ? 1 : 0,
|
||||
child: Container(
|
||||
height: 150,
|
||||
decoration: const BoxDecoration(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0x8C000000), Colors.transparent],
|
||||
colors: [
|
||||
colors.surface.withValues(alpha: 0.7),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -449,14 +456,10 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
|
||||
vertical: 6,
|
||||
),
|
||||
child: IconTheme.merge(
|
||||
// Fixed white, not `colors.fg1`: this bar always
|
||||
// sits on the dark translucent panel over the media
|
||||
// stage (see `FrostedGlassContainer`'s doc comment),
|
||||
// regardless of the app's light/dark theme.
|
||||
data: const IconThemeData(
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
// `fg1`, matching FrostedGlassContainer's own
|
||||
// theme-following panel underneath - both used to
|
||||
// be a fixed white-on-black regardless of theme.
|
||||
data: IconThemeData(color: colors.fg1, size: 24),
|
||||
child: Row(
|
||||
children: [
|
||||
NooTopBarButton(
|
||||
@@ -469,7 +472,7 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
|
||||
child: MarqueeTitle(
|
||||
text: _currentItem.name,
|
||||
style: NooText.label.copyWith(
|
||||
color: Colors.white,
|
||||
color: colors.fg1,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -491,11 +494,14 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
|
||||
opacity: _controlsVisible ? 1 : 0,
|
||||
child: Container(
|
||||
height: 260,
|
||||
decoration: const BoxDecoration(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [Color(0x99000000), Colors.transparent],
|
||||
colors: [
|
||||
colors.surface.withValues(alpha: 0.75),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -7,12 +7,14 @@ import '../providers/files_controller.dart';
|
||||
import '../providers/item_operations.dart';
|
||||
import '../theme/design_tokens.dart';
|
||||
import '../widgets/breadcrumbs.dart';
|
||||
import '../widgets/files_controls_row.dart';
|
||||
import '../widgets/more_tabs_button.dart';
|
||||
import '../widgets/move_copy_conflict_sheet.dart';
|
||||
import '../widgets/noo/core/noo_button.dart';
|
||||
import '../widgets/noo/files/noo_file_kind.dart';
|
||||
import '../widgets/noo/files/noo_file_row.dart';
|
||||
import '../widgets/noo/files/noo_file_table.dart';
|
||||
import '../widgets/noo/media/noo_grid_card.dart';
|
||||
import '../widgets/noo/nav/noo_top_bar.dart';
|
||||
import '../widgets/noo/nav/noo_toolbar.dart';
|
||||
import '../widgets/noo/noo_layout.dart';
|
||||
@@ -86,9 +88,7 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
|
||||
Future<void> _fetch(String path) async {
|
||||
setState(() => _isLoading = true);
|
||||
final raw = await context.read<FilesController>().fetchFolderListing(
|
||||
path,
|
||||
);
|
||||
final raw = await context.read<FilesController>().fetchFolderListing(path);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_rawItems = raw;
|
||||
@@ -212,6 +212,19 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGridCard(NextcloudItem item) {
|
||||
final colors = context.nooColors;
|
||||
return NooGridCard(
|
||||
name: item.name,
|
||||
meta: 'Folder',
|
||||
placeholderColor: colors.accentSoft,
|
||||
icon: LucideIcons.folder,
|
||||
iconColor: colors.accentText,
|
||||
thumbnailHeight: NooLayout.isDesktop(context) ? 118 : 104,
|
||||
onTap: () => _navigateToFolder(item.path),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = context.nooColors;
|
||||
@@ -231,16 +244,34 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
final slivers = <Widget>[
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(gutter, NooSpace.md, gutter, NooSpace.sm),
|
||||
child: hasBreadcrumbs
|
||||
? SizedBox(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
gutter,
|
||||
NooSpace.md,
|
||||
gutter,
|
||||
NooSpace.sm,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 44,
|
||||
child: FilesControlsRow(
|
||||
folderPath: _currentPath,
|
||||
showStorageScope: false,
|
||||
),
|
||||
),
|
||||
if (hasBreadcrumbs) ...[
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
height: 32,
|
||||
child: Breadcrumbs(
|
||||
pathStack: _pathStack,
|
||||
onTap: _navigateToPathIndex,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isLoading)
|
||||
@@ -251,6 +282,22 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
icon: LucideIcons.folder,
|
||||
message: 'No folders here',
|
||||
)
|
||||
else if (files.isGridView)
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.symmetric(horizontal: gutter),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: desktop ? 5 : 2,
|
||||
childAspectRatio: desktop ? 1.05 : 0.92,
|
||||
crossAxisSpacing: desktop ? 16 : 10,
|
||||
mainAxisSpacing: desktop ? 16 : 10,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => _buildGridCard(folders[index]),
|
||||
childCount: folders.length,
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (desktop)
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.symmetric(horizontal: gutter),
|
||||
@@ -266,7 +313,8 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
padding: EdgeInsets.symmetric(horizontal: gutter),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => _buildRow(folders[index], index, folders.length),
|
||||
(context, index) =>
|
||||
_buildRow(folders[index], index, folders.length),
|
||||
childCount: folders.length,
|
||||
),
|
||||
),
|
||||
@@ -312,7 +360,10 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
color: colors.accent,
|
||||
backgroundColor: colors.surface,
|
||||
onRefresh: () => _fetch(_currentPath),
|
||||
child: CustomScrollView(controller: _scrollController, slivers: slivers),
|
||||
child: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: slivers,
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: DecoratedBox(
|
||||
@@ -331,13 +382,14 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
invalidDestination
|
||||
? "Can't ${widget.copy ? 'copy' : 'move'} a folder into "
|
||||
"itself or one of its own subfolders"
|
||||
: '$verb $itemCount item${itemCount == 1 ? '' : 's'}',
|
||||
textAlign: TextAlign.center,
|
||||
style: NooText.meta.copyWith(
|
||||
color: invalidDestination ? colors.danger : colors.fg3,
|
||||
),
|
||||
@@ -350,7 +402,9 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
|
||||
disabled: invalidDestination || _isSubmitting,
|
||||
icon: _isSubmitting
|
||||
? null
|
||||
: (widget.copy ? LucideIcons.copy : LucideIcons.folderInput),
|
||||
: (widget.copy
|
||||
? LucideIcons.copy
|
||||
: LucideIcons.folderInput),
|
||||
onTap: invalidDestination || _isSubmitting
|
||||
? null
|
||||
: () => _confirm(ops),
|
||||
|
||||
@@ -10,11 +10,12 @@ import '../services/share_intent_service.dart';
|
||||
import '../services/upload_service.dart';
|
||||
import '../theme/design_tokens.dart';
|
||||
import '../widgets/breadcrumbs.dart';
|
||||
import '../widgets/marquee_title.dart';
|
||||
import '../widgets/files_controls_row.dart';
|
||||
import '../widgets/noo/core/noo_button.dart';
|
||||
import '../widgets/noo/files/noo_file_kind.dart';
|
||||
import '../widgets/noo/files/noo_file_row.dart';
|
||||
import '../widgets/noo/files/noo_file_table.dart';
|
||||
import '../widgets/noo/media/noo_grid_card.dart';
|
||||
import '../widgets/noo/nav/noo_top_bar.dart';
|
||||
import '../widgets/noo/nav/noo_toolbar.dart';
|
||||
import '../widgets/noo/noo_layout.dart';
|
||||
@@ -51,7 +52,9 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
// Shared files have no relationship to wherever the user was last
|
||||
// browsing, so start the destination picker fresh at the root.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) context.read<FilesController>().navigateToAbsoluteFolder('/');
|
||||
if (mounted) {
|
||||
context.read<FilesController>().navigateToAbsoluteFolder('/');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -94,7 +97,12 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
|
||||
// Only folders are valid upload destinations, so the listing - unlike
|
||||
// Files' own - never shows plain files at all.
|
||||
Widget _buildRow(BuildContext context, NextcloudItem item, int index, int count) {
|
||||
Widget _buildRow(
|
||||
BuildContext context,
|
||||
NextcloudItem item,
|
||||
int index,
|
||||
int count,
|
||||
) {
|
||||
final colors = context.nooColors;
|
||||
final files = context.read<FilesController>();
|
||||
final row = NooFileRow(
|
||||
@@ -130,6 +138,20 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGridCard(BuildContext context, NextcloudItem item) {
|
||||
final colors = context.nooColors;
|
||||
final files = context.read<FilesController>();
|
||||
return NooGridCard(
|
||||
name: item.name,
|
||||
meta: 'Folder',
|
||||
placeholderColor: colors.accentSoft,
|
||||
icon: LucideIcons.folder,
|
||||
iconColor: colors.accentText,
|
||||
thumbnailHeight: NooLayout.isDesktop(context) ? 118 : 104,
|
||||
onTap: () => files.navigateToFolder(item.path),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = context.nooColors;
|
||||
@@ -147,16 +169,34 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
final slivers = <Widget>[
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(gutter, NooSpace.md, gutter, NooSpace.sm),
|
||||
child: hasBreadcrumbs
|
||||
? SizedBox(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
gutter,
|
||||
NooSpace.md,
|
||||
gutter,
|
||||
NooSpace.sm,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 44,
|
||||
child: FilesControlsRow(
|
||||
folderPath: currentPath,
|
||||
showStorageScope: false,
|
||||
),
|
||||
),
|
||||
if (hasBreadcrumbs) ...[
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
height: 32,
|
||||
child: Breadcrumbs(
|
||||
pathStack: files.pathStack,
|
||||
onTap: (index) => files.navigateToPathIndex(index),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (files.isLoading)
|
||||
@@ -167,6 +207,22 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
icon: LucideIcons.folder,
|
||||
message: 'No folders here',
|
||||
)
|
||||
else if (files.isGridView)
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.symmetric(horizontal: gutter),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: desktop ? 5 : 2,
|
||||
childAspectRatio: desktop ? 1.05 : 0.92,
|
||||
crossAxisSpacing: desktop ? 16 : 10,
|
||||
mainAxisSpacing: desktop ? 16 : 10,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => _buildGridCard(context, folders[index]),
|
||||
childCount: folders.length,
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (desktop)
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.symmetric(horizontal: gutter),
|
||||
@@ -223,12 +279,20 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
color: colors.accent,
|
||||
backgroundColor: colors.surface,
|
||||
onRefresh: files.refreshData,
|
||||
child: CustomScrollView(controller: _scrollController, slivers: slivers),
|
||||
child: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: slivers,
|
||||
),
|
||||
),
|
||||
),
|
||||
// A flat surface bar with a top line, peeking up from the bottom edge -
|
||||
// the uploading-file summary (marqueed if it doesn't fit on one line)
|
||||
// sits directly above the destination button.
|
||||
// a centered item-count meta line sits directly above the destination
|
||||
// button (DESIGN_SYSTEM.md's "Upload / Move / Copy destination
|
||||
// picker" recipe: "a meta line ... above a full-width ... primary
|
||||
// CTA" - matches MoveCopyDestinationPicker's identical bar). A count,
|
||||
// not a filename: this is the one-file case just as much as the
|
||||
// many-files case, and a long filename has no good fixed-width
|
||||
// treatment the way a short count always does.
|
||||
bottomNavigationBar: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surface,
|
||||
@@ -245,48 +309,12 @@ class _ShareUploadViewState extends State<ShareUploadView> {
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Sized to the text itself (like a real chip) up to the
|
||||
// row's available width - MarqueeTitle needs a concrete
|
||||
// (not just loose) width to know whether/how far to scroll,
|
||||
// so this measures the text once up front rather than
|
||||
// leaving the chip unconstrained.
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final uploadingText = widget.files.length == 1
|
||||
? 'Uploading ${widget.files.first.name}'
|
||||
: 'Uploading ${widget.files.length} files';
|
||||
final chipTextStyle = NooText.label.copyWith(
|
||||
color: colors.fg2,
|
||||
);
|
||||
const horizontalPadding = 28.0;
|
||||
final painter = TextPainter(
|
||||
text: TextSpan(text: uploadingText, style: chipTextStyle),
|
||||
maxLines: 1,
|
||||
textDirection: Directionality.of(context),
|
||||
)..layout(maxWidth: double.infinity);
|
||||
final chipWidth = (painter.width + horizontalPadding).clamp(
|
||||
0.0,
|
||||
constraints.maxWidth,
|
||||
);
|
||||
|
||||
return Container(
|
||||
width: chipWidth,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: NooSpace.smd,
|
||||
vertical: NooSpace.xs,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surface2,
|
||||
borderRadius: BorderRadius.circular(NooRadii.pill),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 20,
|
||||
child: MarqueeTitle(text: uploadingText, style: chipTextStyle),
|
||||
),
|
||||
);
|
||||
},
|
||||
Text(
|
||||
'Uploading ${widget.files.length} item${widget.files.length == 1 ? '' : 's'}',
|
||||
textAlign: TextAlign.center,
|
||||
style: NooText.meta.copyWith(color: colors.fg3),
|
||||
),
|
||||
const SizedBox(height: NooSpace.sm),
|
||||
NooButton(
|
||||
|
||||
+66
-27
@@ -60,8 +60,12 @@ class _TrashViewState extends State<TrashView> {
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: NooBanner(
|
||||
actionLabel: trash.isEmpty ? null : 'Empty trash',
|
||||
onAction: trash.isEmpty ? null : () => _confirmEmptyTrash(context, trash),
|
||||
child: const Text('Deleted items are kept for 30 days, then removed automatically.'),
|
||||
onAction: trash.isEmpty
|
||||
? null
|
||||
: () => _confirmEmptyTrash(context, trash),
|
||||
child: const Text(
|
||||
'Deleted items are kept for 30 days, then removed automatically.',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -115,6 +119,10 @@ class _TrashViewState extends State<TrashView> {
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final item = trash[index];
|
||||
// The overflow menu's only action here is "Delete forever" - a
|
||||
// bare ellipsis obscures that behind an extra tap to discover it,
|
||||
// so this row shows the delete icon directly instead of going
|
||||
// through `NooFileRow.onMore`.
|
||||
final row = NooFileRow(
|
||||
kind: NooFileKind.from(
|
||||
name: item.name,
|
||||
@@ -124,19 +132,30 @@ class _TrashViewState extends State<TrashView> {
|
||||
name: item.name,
|
||||
meta: _meta(item),
|
||||
iosStyle: NooLayout.iosStyle(context),
|
||||
trailing: SizedBox.square(
|
||||
dimension: 40,
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: 'Restore',
|
||||
child: InkResponse(
|
||||
onTap: () => _restore(context, item),
|
||||
radius: 20,
|
||||
child: const Icon(LucideIcons.rotateCcw),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Semantics(
|
||||
button: true,
|
||||
label: 'Restore',
|
||||
child: InkResponse(
|
||||
onTap: () => _restore(context, item),
|
||||
radius: 20,
|
||||
child: const Icon(LucideIcons.rotateCcw),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: NooSpace.xs),
|
||||
Semantics(
|
||||
button: true,
|
||||
label: 'Delete forever',
|
||||
child: InkResponse(
|
||||
onTap: () => _confirmDeleteForever(context, item),
|
||||
radius: 20,
|
||||
child: Icon(LucideIcons.trash2, color: colors.danger),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onMore: () => _confirmDeleteForever(context, item),
|
||||
);
|
||||
final isFirst = index == 0;
|
||||
final isLast = index == trash.length - 1;
|
||||
@@ -186,7 +205,7 @@ class _TrashViewState extends State<TrashView> {
|
||||
col2: _deletedLabel(item),
|
||||
col3: item.originalLocation ?? 'Unknown location',
|
||||
onRestore: () => _restore(context, item),
|
||||
onMore: () => _confirmDeleteForever(context, item),
|
||||
onDelete: () => _confirmDeleteForever(context, item),
|
||||
);
|
||||
}, childCount: trash.length + 1),
|
||||
),
|
||||
@@ -196,8 +215,9 @@ class _TrashViewState extends State<TrashView> {
|
||||
String _meta(NextcloudItem item) =>
|
||||
'Deleted ${_deletedLabel(item)} · ${tabLocationLabel(item.originalLocation ?? item.path, item.name)}';
|
||||
|
||||
String _deletedLabel(NextcloudItem item) =>
|
||||
item.deletedAt != null ? DateFormat.yMMMd().format(item.deletedAt!) : 'recently';
|
||||
String _deletedLabel(NextcloudItem item) => item.deletedAt != null
|
||||
? DateFormat.yMMMd().format(item.deletedAt!)
|
||||
: 'recently';
|
||||
|
||||
Future<void> _restore(BuildContext context, NextcloudItem item) async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
@@ -301,19 +321,23 @@ class _TrashViewState extends State<TrashView> {
|
||||
}
|
||||
|
||||
/// Desktop trash row: like `NooFileTableRow`, but its 120px last column
|
||||
/// holds a tonal "Restore" button plus the overflow button (permanent
|
||||
/// delete) instead of status icons - `NooFileTableRow` has no slot for
|
||||
/// screen-specific actions there. Column widths (180/160/120) are matched
|
||||
/// by hand to `NooFileTableHeader`'s (private in `noo_file_table.dart`) so
|
||||
/// this still lines up under it; promoting an optional `actions` slot onto
|
||||
/// `NooFileTableRow` would let this fold back into the shared component.
|
||||
/// holds a tonal "Restore" button plus a danger-tonal "Delete" button
|
||||
/// (permanent delete) instead of status icons - `NooFileTableRow` has no
|
||||
/// slot for screen-specific actions there. A labelled pill, not a bare
|
||||
/// overflow icon: it's the row's only destructive action, not a menu of
|
||||
/// several, and desktop already prefers labelled buttons to bare icons
|
||||
/// (see `NooSelectionBar`'s own desktop actions). Column widths
|
||||
/// (180/160/120) are matched by hand to `NooFileTableHeader`'s (private in
|
||||
/// `noo_file_table.dart`) so this still lines up under it; promoting an
|
||||
/// optional `actions` slot onto `NooFileTableRow` would let this fold back
|
||||
/// into the shared component.
|
||||
class _TrashDesktopRow extends StatelessWidget {
|
||||
final NooFileKind kind;
|
||||
final String name;
|
||||
final String col2;
|
||||
final String col3;
|
||||
final VoidCallback onRestore;
|
||||
final VoidCallback onMore;
|
||||
final VoidCallback onDelete;
|
||||
|
||||
const _TrashDesktopRow({
|
||||
required this.kind,
|
||||
@@ -321,7 +345,7 @@ class _TrashDesktopRow extends StatelessWidget {
|
||||
required this.col2,
|
||||
required this.col3,
|
||||
required this.onRestore,
|
||||
required this.onMore,
|
||||
required this.onDelete,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -358,11 +382,21 @@ class _TrashDesktopRow extends StatelessWidget {
|
||||
),
|
||||
SizedBox(
|
||||
width: 180,
|
||||
child: Text(col2, maxLines: 1, overflow: TextOverflow.ellipsis, style: metaStyle),
|
||||
child: Text(
|
||||
col2,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: metaStyle,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 160,
|
||||
child: Text(col3, maxLines: 1, overflow: TextOverflow.ellipsis, style: metaStyle),
|
||||
child: Text(
|
||||
col3,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: metaStyle,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 120,
|
||||
@@ -379,7 +413,12 @@ class _TrashDesktopRow extends StatelessWidget {
|
||||
child: const Text('Restore'),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
NooOverflowButton(size: 24, onTap: onMore),
|
||||
NooButton(
|
||||
variant: NooButtonVariant.danger,
|
||||
size: NooButtonSize.xs,
|
||||
onTap: onDelete,
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user