Fix Trash/Shares list perf and live-update the Files sort/filter sheet
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.
This commit is contained in:
+37
-15
@@ -11,7 +11,6 @@ import '../widgets/noo/core/noo_segmented_control.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/lists/noo_grouped_list.dart';
|
||||
import '../widgets/noo/noo_layout.dart';
|
||||
import '../widgets/tabs/tab_state_slivers.dart';
|
||||
|
||||
@@ -162,24 +161,47 @@ class _SharesViewState extends State<SharesView> {
|
||||
c.setSharedWithMe(scope == _ShareScope.withYou);
|
||||
}
|
||||
|
||||
// A lazily-built `SliverList`, not `NooGroupedList` (its own `Column`
|
||||
// isn't lazy - see files_view.dart's `_buildMobileRow` doc comment, and
|
||||
// trash_view.dart's `_buildMobileList`, which had the same bug: a heavy
|
||||
// account's full share list built eagerly up front). Each row still
|
||||
// reads as one continuous radius-20 card via per-row corner rounding +
|
||||
// a 1px `line` divider.
|
||||
Widget _buildMobileList(BuildContext context, List<NextcloudShare> shares) {
|
||||
final colors = context.nooColors;
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: NooSpace.sm),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: NooGroupedList(
|
||||
children: [
|
||||
for (final share in shares)
|
||||
NooFileRow(
|
||||
kind: NooFileKind.from(name: share.name, isDirectory: share.isFolder),
|
||||
name: share.name,
|
||||
meta: '${share.ownerDisplayName} · ${_permissionLabel(share.permissions)}',
|
||||
iosStyle: NooLayout.iosStyle(context),
|
||||
onTap: share.isFolder ? () => _openFolder(context, share) : null,
|
||||
trailing: Icon(_shareTypeIcon(share.shareType), size: 16),
|
||||
onMore: () => _confirmUnshare(context, share),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final share = shares[index];
|
||||
final row = NooFileRow(
|
||||
kind: NooFileKind.from(name: share.name, isDirectory: share.isFolder),
|
||||
name: share.name,
|
||||
meta: '${share.ownerDisplayName} · ${_permissionLabel(share.permissions)}',
|
||||
iosStyle: NooLayout.iosStyle(context),
|
||||
onTap: share.isFolder ? () => _openFolder(context, share) : null,
|
||||
trailing: Icon(_shareTypeIcon(share.shareType), size: 16),
|
||||
onMore: () => _confirmUnshare(context, share),
|
||||
);
|
||||
final isFirst = index == 0;
|
||||
final isLast = index == shares.length - 1;
|
||||
return Column(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: isFirst
|
||||
? const Radius.circular(NooRadii.card)
|
||||
: Radius.zero,
|
||||
bottom: isLast
|
||||
? const Radius.circular(NooRadii.card)
|
||||
: Radius.zero,
|
||||
),
|
||||
child: row,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!isLast) Container(height: 1, color: colors.line),
|
||||
],
|
||||
);
|
||||
}, childCount: shares.length),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+50
-28
@@ -11,7 +11,6 @@ import '../widgets/noo/files/noo_file_row.dart';
|
||||
import '../widgets/noo/files/noo_file_table.dart';
|
||||
import '../widgets/noo/files/noo_file_tile.dart';
|
||||
import '../widgets/noo/lists/noo_banner.dart';
|
||||
import '../widgets/noo/lists/noo_grouped_list.dart';
|
||||
import '../widgets/noo/noo_layout.dart';
|
||||
import '../widgets/tabs/tab_location.dart';
|
||||
import '../widgets/tabs/tab_state_slivers.dart';
|
||||
@@ -103,38 +102,61 @@ class _TrashViewState extends State<TrashView> {
|
||||
);
|
||||
}
|
||||
|
||||
// A lazily-built `SliverList`, not `NooGroupedList` (its own `Column`
|
||||
// isn't lazy - see files_view.dart's `_buildMobileRow` doc comment for
|
||||
// the same reasoning): Trash can hold hundreds of items, and building
|
||||
// every row eagerly up front is what made this tab laggy. Each row still
|
||||
// reads as one continuous radius-20 card via per-row corner rounding +
|
||||
// a 1px `line` divider, matching `NooGroupedList`'s look.
|
||||
Widget _buildMobileList(BuildContext context, List<NextcloudItem> trash) {
|
||||
final colors = context.nooColors;
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: NooSpace.sm),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: NooGroupedList(
|
||||
children: [
|
||||
for (final item in trash)
|
||||
NooFileRow(
|
||||
kind: NooFileKind.from(
|
||||
name: item.name,
|
||||
mimeType: item.mimeType,
|
||||
isDirectory: item.isFolder,
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final item = trash[index];
|
||||
final row = NooFileRow(
|
||||
kind: NooFileKind.from(
|
||||
name: item.name,
|
||||
mimeType: item.mimeType,
|
||||
isDirectory: item.isFolder,
|
||||
),
|
||||
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),
|
||||
),
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
onMore: () => _confirmDeleteForever(context, item),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onMore: () => _confirmDeleteForever(context, item),
|
||||
);
|
||||
final isFirst = index == 0;
|
||||
final isLast = index == trash.length - 1;
|
||||
return Column(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: isFirst
|
||||
? const Radius.circular(NooRadii.card)
|
||||
: Radius.zero,
|
||||
bottom: isLast
|
||||
? const Radius.circular(NooRadii.card)
|
||||
: Radius.zero,
|
||||
),
|
||||
child: row,
|
||||
),
|
||||
if (!isLast) Container(height: 1, color: colors.line),
|
||||
],
|
||||
);
|
||||
}, childCount: trash.length),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,33 +93,49 @@ class FilesControlsRow extends StatelessWidget {
|
||||
showNooSheet(
|
||||
context,
|
||||
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'),
|
||||
],
|
||||
),
|
||||
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);
|
||||
// `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);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -129,54 +145,66 @@ class FilesControlsRow extends StatelessWidget {
|
||||
showNooSheet(
|
||||
context,
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
// 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user