- 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>
229 lines
8.3 KiB
Dart
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,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|