Trash and Shares built their mobile lists eagerly via NooGroupedList's own Column instead of a lazy sliver, so an account with hundreds of items made those tabs laggy - both now use a SliverList/ SliverChildBuilderDelegate instead, matching Files' own lazy list, while keeping the same one-continuous-card look via per-row corner rounding and a 1px divider. FilesControlsRow's sort/filter sheets also had a live-update bug: since showNooSheet's children are built once up front, tapping a row inside the open sheet updated the controller but the already-built checkmarks/toggles stayed stale until the sheet was closed and reopened. Wrapping each sheet's content in a ListenableBuilder fixes that. Also adds NooSelectionBar to styling.md's widget inventory table.
213 lines
8.0 KiB
Dart
213 lines
8.0 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 'All types';
|
|
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,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|