Files
noo/lib/widgets/files_controls_row.dart
T
ayushyaandClaude Sonnet 5 b73f451fff Fix pull-to-refresh, header tint, sheet controls, and Shares tap
- Every tab's RefreshIndicator+CustomScrollView was missing an explicit
  AlwaysScrollableScrollPhysics, so pull-to-refresh silently couldn't be
  triggered once the list was short enough to fit the viewport (empty,
  or a single item) - the platform default physics only allows the
  overscroll drag RefreshIndicator listens for once content overflows.
- StickyHeaderDelegate (Files'/Photos'/Favorites' pinned controls row)
  painted its background from Theme.of(context).colorScheme.surface -
  Flutter's own Material 3 scheme, reseeded by the user's accent color
  choice - instead of context.nooColors.bg like the rest of the screen,
  showing as a stray colored cast behind the row.
- NooSegmentedControl gains onSurface: true wherever one sits inside a
  sheet (Files'/Photos' sort and type-filter controls) - it defaulted to
  the same surface fill as the sheet itself, so the control had no
  visible boundary showing it as a grouped unit.
- Shares tab: removed the per-row "..." overflow menu on every scope
  (With you/By you/Links); tapping a row now opens the full Share sheet
  for that item instead, fetched via the new
  FilesController.fetchItemAtPath (a share only carries enough metadata
  for its own row, not what the sheet's header needs).
- fetchShares() now reuses _shareFromJson instead of its own thinner
  inline parser, so url/token/permissions/expireDate are actually
  populated - a public-link share could never show "Copy link" before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 10:45:05 -04:00

229 lines
8.3 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.
///
/// A plain `Row`, not a horizontally-scrolling one - it used to wrap in a
/// `SingleChildScrollView`, which gives its child unbounded width, silently
/// breaking the trailing `Spacer` (a flex child needs a bounded width to
/// size against) and leaving the List/Grid toggle sitting right after the
/// filter chip instead of pinned to the row's right edge. The two chips
/// plus an icon-only toggle never come close to needing to scroll anyway.
class FilesControlsRow extends StatelessWidget {
final String folderPath;
final bool showStorageScope;
const FilesControlsRow({
super.key,
required this.folderPath,
this.showStorageScope = true,
});
@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: 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,
onSurface: 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, neither the type
// filter's selected segment nor the toggles below would move until
// the sheet is closed and reopened.
ListenableBuilder(
listenable: files,
builder: (context, _) => Column(
mainAxisSize: MainAxisSize.min,
children: [
// Same "icon always, label only when selected" pill as
// Photos' own type filter below - not a checkmark list, so
// the two screens' filter sheets look and behave the same.
NooSegmentedControl<FilesTypeFilter>(
fill: true,
onSurface: true,
labelOnlySelected: true,
value: files.filesTypeFilter,
onChanged: files.setFilesTypeFilter,
options: const [
NooSegmentOption(
value: FilesTypeFilter.all,
icon: LucideIcons.layoutGrid,
label: 'All',
),
NooSegmentOption(
value: FilesTypeFilter.filesOnly,
icon: LucideIcons.file,
label: 'Files',
),
NooSegmentOption(
value: FilesTypeFilter.foldersOnly,
icon: LucideIcons.folder,
label: 'Folders',
),
],
),
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,
),
),
),
],
),
],
),
),
],
);
}
}