Files
noo/lib/widgets/files_controls_row.dart
T
ayushya 0abea46b1c 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.
2026-09-28 19:26:18 -04:00

241 lines
8.7 KiB
Dart

import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:provider/provider.dart';
import '../providers/files_controller.dart';
import '../theme/design_tokens.dart';
import 'noo/core/noo_chip.dart';
import 'noo/core/noo_segmented_control.dart';
import 'noo/core/noo_toggle.dart';
import 'noo/lists/noo_grouped_list.dart';
import 'noo/lists/noo_settings_row.dart';
import 'noo/overlays/noo_sheet.dart';
import 'sort_menu_button.dart' show sortFieldLabel;
String _typeFilterLabel(FilesTypeFilter filter) {
switch (filter) {
case FilesTypeFilter.all:
return 'Filters';
case FilesTypeFilter.filesOnly:
return 'Files only';
case FilesTypeFilter.foldersOnly:
return 'Folders only';
}
}
/// The Files tab's sort/filter/view-mode row (Noo Design System project's
/// Files screen: a sort chip, a filter chip, a spacer, then the list/grid
/// segmented toggle) - shared with the Offline tab, which reads and writes
/// the exact same [FilesController] prefs. [folderPath] picks which
/// folder's sort applies. [showStorageScope] is off for Offline - a local
/// mirror has no cloud/external distinction.
///
/// Hidden-files and storage-scope, which have no home in the mockup's
/// simple two-chip row, live inside the filter chip's sheet instead of as
/// their own row controls.
class FilesControlsRow extends StatelessWidget {
final String folderPath;
final bool showStorageScope;
final ScrollController? scrollController;
const FilesControlsRow({
super.key,
required this.folderPath,
this.showStorageScope = true,
this.scrollController,
});
@override
Widget build(BuildContext context) {
final files = context.watch<FilesController>();
final filtersActive =
files.filesTypeFilter != FilesTypeFilter.all ||
files.showHiddenFiles ||
(showStorageScope && files.storageScope != StorageScope.cloud);
return SizedBox(
height: 44,
child: SingleChildScrollView(
controller: scrollController,
scrollDirection: Axis.horizontal,
child: Row(
children: [
NooChip(
icon: files.sortAscendingFor(folderPath)
? LucideIcons.arrowUp
: LucideIcons.arrowDown,
onTap: () => _showSortSheet(context, files),
child: Text(sortFieldLabel(files.sortFieldFor(folderPath))),
),
const SizedBox(width: 8),
NooChip(
icon: LucideIcons.filter,
trailing: NooChipTrailing.menu,
selected: filtersActive,
onTap: () => _showFilterSheet(context, files),
child: Text(_typeFilterLabel(files.filesTypeFilter)),
),
const Spacer(),
NooSegmentedControl<bool>(
iconOnly: true,
value: files.isGridView,
onChanged: files.setGridView,
options: const [
NooSegmentOption(value: false, icon: LucideIcons.list),
NooSegmentOption(value: true, icon: LucideIcons.grid),
],
),
],
),
),
);
}
void _showSortSheet(BuildContext context, FilesController files) {
showNooSheet(
context,
children: [
// `showNooSheet`'s `children` are built once, up front - a bare
// checkmark/value here would freeze at whatever it was when the
// sheet opened, since tapping a row calls `files.set...` and
// notifies the controller, not this already-built widget tree.
// `ListenableBuilder` re-runs its `builder` on every
// `FilesController.notifyListeners()` instead, so the selection
// updates live - see the identical fix in `_showFilterSheet`.
ListenableBuilder(
listenable: files,
builder: (context, _) => Column(
mainAxisSize: MainAxisSize.min,
children: [
NooSegmentedControl<bool>(
fill: true,
value: files.sortAscendingFor(folderPath),
onChanged: (asc) {
if (asc != files.sortAscendingFor(folderPath)) {
files.toggleSortOrderFor(folderPath);
}
},
options: const [
NooSegmentOption(
value: true,
icon: LucideIcons.arrowUp,
label: 'Ascending',
),
NooSegmentOption(
value: false,
icon: LucideIcons.arrowDown,
label: 'Descending',
),
],
),
const SizedBox(height: 22),
NooGroupedList(
children: [
for (final field in FileSortField.values)
NooSettingsRow(
label: Text(sortFieldLabel(field)),
trailing: field == files.sortFieldFor(folderPath)
? Icon(
LucideIcons.check,
size: 18,
color: context.nooColors.accentText,
)
: null,
onTap: () {
files.setSortFieldFor(folderPath, field);
Navigator.pop(context);
},
),
],
),
],
),
),
],
);
}
void _showFilterSheet(BuildContext context, FilesController files) {
showNooSheet(
context,
children: [
// See `_showSortSheet`'s comment: without this, none of the
// checkmarks/toggles below would move until the sheet is closed
// and reopened.
ListenableBuilder(
listenable: files,
builder: (context, _) => Column(
mainAxisSize: MainAxisSize.min,
children: [
NooGroupedList(
label: 'Show',
children: [
NooSettingsRow(
label: const Text('Files and folders'),
trailing: files.filesTypeFilter == FilesTypeFilter.all
? Icon(
LucideIcons.check,
size: 18,
color: context.nooColors.accentText,
)
: null,
onTap: () => files.setFilesTypeFilter(FilesTypeFilter.all),
),
NooSettingsRow(
label: const Text('Files only'),
trailing: files.filesTypeFilter == FilesTypeFilter.filesOnly
? Icon(
LucideIcons.check,
size: 18,
color: context.nooColors.accentText,
)
: null,
onTap: () =>
files.setFilesTypeFilter(FilesTypeFilter.filesOnly),
),
NooSettingsRow(
label: const Text('Folders only'),
trailing:
files.filesTypeFilter == FilesTypeFilter.foldersOnly
? Icon(
LucideIcons.check,
size: 18,
color: context.nooColors.accentText,
)
: null,
onTap: () =>
files.setFilesTypeFilter(FilesTypeFilter.foldersOnly),
),
],
),
const SizedBox(height: 22),
NooGroupedList(
children: [
NooSettingsRow(
icon: LucideIcons.eye,
label: const Text('Show hidden files'),
trailing: NooToggle(
checked: files.showHiddenFiles,
onChanged: (_) => files.toggleShowHiddenFiles(),
),
),
if (showStorageScope)
NooSettingsRow(
icon: LucideIcons.hardDrive,
label: const Text('External storage'),
trailing: NooToggle(
checked: files.storageScope == StorageScope.external,
onChanged: (external) => files.setStorageScope(
external ? StorageScope.external : StorageScope.cloud,
),
),
),
],
),
],
),
),
],
);
}
}