Add a Settings option to reorder the selection action bar's actions
NooSelectionBar's fixed inline slots (3 mobile / 4 desktop, the rest behind "More") previously just took the first N actions in whatever order each screen happened to build them in. Give bulk actions a stable SelectionActionKind identity (independent of their dynamic icon/label, e.g. favorite vs. unfavorite) so Settings can offer a drag-to-reorder list - mirroring the existing Tabs section, minus the pin/hide concept, which doesn't apply here - and each screen applies that saved priority order via orderSelectionActions before handing its action list to the bar.
This commit is contained in:
@@ -178,7 +178,9 @@ multi-select)
|
||||
desktop - there can be up to 9: favorite, share, download, delete, copy,
|
||||
move, rename, sync, details); the rest sit behind a trailing "More"
|
||||
button that opens the same grouped-list sheet a file row's own overflow
|
||||
menu (`ellipsis`/`ellipsis-vertical`) already uses.
|
||||
menu (`ellipsis`/`ellipsis-vertical`) already uses. Which actions land in
|
||||
the inline slots vs. "More" is the user's own priority order (Settings
|
||||
→ Action bar, §4's 9-part list), not a fixed per-kind assignment.
|
||||
- Mobile: inline actions are plain 20px accent-text icons, no fill; "More"
|
||||
is the same 40px `NooOverflowButton` treatment (`ellipsis`/
|
||||
`ellipsis-vertical`, fg-3) file rows use for their own overflow menu.
|
||||
@@ -317,7 +319,12 @@ Sidebar items are 38px tall with radius 12, an 18px icon and a 14/500 label. The
|
||||
5. Files cache
|
||||
6. Appearance
|
||||
7. Tabs
|
||||
8. Swipe on a file
|
||||
8. Action bar - a reorder-only list (no pin/hide, unlike Tabs): the
|
||||
priority order for the Selection action bar's bulk actions (favorite,
|
||||
share, download, delete, copy, move, rename, sync, details) - the
|
||||
first few (3 mobile / 4 desktop) land in the bar's fixed inline
|
||||
slots, the rest sit behind "More". See §2 "Selection action bar".
|
||||
9. Swipe on a file
|
||||
|
||||
Mobile uses one column of grouped lists. Desktop uses a 2-column grid of cards with a 1px line and radius 20.
|
||||
- **Share sheet / dialog:** sections in this order:
|
||||
|
||||
@@ -1,16 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
||||
/// A bulk action's identity, stable across the dynamic icon/label a
|
||||
/// [SelectionAction] actually renders (e.g. `favorite` covers both
|
||||
/// "Favorite" and "Remove from favorites"). This is what the user's
|
||||
/// configured [order] - Settings' "Action bar" reorder list - and
|
||||
/// [NooSelectionBar]'s inline/overflow split key off, since the live
|
||||
/// [SelectionAction] list is rebuilt fresh on every selection change and
|
||||
/// can't be compared by icon/label alone.
|
||||
///
|
||||
/// Not every screen offers every kind (e.g. Favorites has no `rename` or
|
||||
/// `sync`) - [orderSelectionActions] just skips whatever kind a screen
|
||||
/// didn't build.
|
||||
enum SelectionActionKind {
|
||||
favorite,
|
||||
share,
|
||||
download,
|
||||
delete,
|
||||
copy,
|
||||
move,
|
||||
rename,
|
||||
sync,
|
||||
details;
|
||||
|
||||
/// Generic label for the Settings reorder row - the live bar uses each
|
||||
/// screen's own contextual label (e.g. "Remove from favorites") instead.
|
||||
String get label => switch (this) {
|
||||
SelectionActionKind.favorite => 'Favorite',
|
||||
SelectionActionKind.share => 'Share',
|
||||
SelectionActionKind.download => 'Download',
|
||||
SelectionActionKind.delete => 'Delete',
|
||||
SelectionActionKind.copy => 'Copy',
|
||||
SelectionActionKind.move => 'Move',
|
||||
SelectionActionKind.rename => 'Rename',
|
||||
SelectionActionKind.sync => 'Sync to device',
|
||||
SelectionActionKind.details => 'Details',
|
||||
};
|
||||
|
||||
IconData get icon => switch (this) {
|
||||
SelectionActionKind.favorite => LucideIcons.star,
|
||||
SelectionActionKind.share => LucideIcons.share2,
|
||||
SelectionActionKind.download => LucideIcons.download,
|
||||
SelectionActionKind.delete => LucideIcons.trash2,
|
||||
SelectionActionKind.copy => LucideIcons.copy,
|
||||
SelectionActionKind.move => LucideIcons.folderInput,
|
||||
SelectionActionKind.rename => LucideIcons.filePen,
|
||||
SelectionActionKind.sync => LucideIcons.hardDriveDownload,
|
||||
SelectionActionKind.details => LucideIcons.info,
|
||||
};
|
||||
}
|
||||
|
||||
/// One bulk action available for the current multi-selection (e.g. favorite,
|
||||
/// download, delete), shown as an icon button in the sticky selection
|
||||
/// toolbar each tab (Files, Photos, ...) renders inline in its own content.
|
||||
class SelectionAction {
|
||||
final SelectionActionKind kind;
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const SelectionAction({
|
||||
required this.kind,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
});
|
||||
}
|
||||
|
||||
/// Reorders [actions] to match the user's configured [order] (Settings'
|
||||
/// "Action bar" section, [SelectionActionKind] values) - this is what makes
|
||||
/// that setting take effect, since it's [order] that decides which actions
|
||||
/// land in `NooSelectionBar`'s inline slots vs. behind "More". A kind in
|
||||
/// [order] the caller didn't build (e.g. `rename` on a multi-item
|
||||
/// selection) is just skipped; a kind [actions] has that [order] doesn't
|
||||
/// mention (e.g. a kind added in a later app update, before the saved
|
||||
/// order is migrated - see [SettingsController.selectionActionOrder]) is
|
||||
/// appended at the end in its original position.
|
||||
List<SelectionAction> orderSelectionActions(
|
||||
List<SelectionAction> actions,
|
||||
List<SelectionActionKind> order,
|
||||
) {
|
||||
final byKind = {for (final a in actions) a.kind: a};
|
||||
final ordered = <SelectionAction>[];
|
||||
for (final kind in order) {
|
||||
final action = byKind.remove(kind);
|
||||
if (action != null) ordered.add(action);
|
||||
}
|
||||
ordered.addAll(byKind.values);
|
||||
return ordered;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/app_tab.dart';
|
||||
import '../models/selection_action.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
|
||||
/// What swiping a Files list-view item left/right does, user-configurable
|
||||
@@ -24,6 +25,7 @@ class SettingsController extends ChangeNotifier {
|
||||
static const _prefUseDynamicColor = 'ui_use_dynamic_color';
|
||||
static const _prefSeedColor = 'ui_seed_color';
|
||||
static const _prefTabOrder = 'ui_tab_order';
|
||||
static const _prefSelectionActionOrder = 'ui_selection_action_order';
|
||||
static const _prefHiddenTabs = 'ui_hidden_tabs';
|
||||
static const _prefDefaultTab = 'ui_default_tab';
|
||||
static const _prefSwipeLeftAction = 'ui_swipe_left_action';
|
||||
@@ -47,6 +49,9 @@ class SettingsController extends ChangeNotifier {
|
||||
Set<AppTab> _hiddenTabs = {};
|
||||
AppTab _defaultTab = AppTab.files;
|
||||
|
||||
List<SelectionActionKind> _selectionActionOrder = SelectionActionKind.values
|
||||
.toList();
|
||||
|
||||
SwipeAction _swipeLeftAction = SwipeAction.delete;
|
||||
SwipeAction _swipeRightAction = SwipeAction.favorite;
|
||||
|
||||
@@ -86,6 +91,12 @@ class SettingsController extends ChangeNotifier {
|
||||
Set<AppTab> get hiddenTabs => _hiddenTabs;
|
||||
AppTab get defaultTab => _defaultTab;
|
||||
|
||||
/// The priority order bulk actions (favorite, share, download, ...) show
|
||||
/// in on the multi-select action bar - see [orderSelectionActions]. Every
|
||||
/// [SelectionActionKind] is always present here (nothing is hidden, only
|
||||
/// reordered), so [NooSelectionBar]'s fixed inline slots are always full.
|
||||
List<SelectionActionKind> get selectionActionOrder => _selectionActionOrder;
|
||||
|
||||
/// The tabs the bottom nav bar should actually show, in order.
|
||||
List<AppTab> get visibleTabs =>
|
||||
_tabOrder.where((t) => !_hiddenTabs.contains(t)).toList();
|
||||
@@ -134,6 +145,25 @@ class SettingsController extends ChangeNotifier {
|
||||
_tabOrder = order;
|
||||
}
|
||||
|
||||
final savedActionOrderNames = prefs.getStringList(
|
||||
_prefSelectionActionOrder,
|
||||
);
|
||||
if (savedActionOrderNames != null) {
|
||||
final order = <SelectionActionKind>[];
|
||||
for (final name in savedActionOrderNames) {
|
||||
final match = SelectionActionKind.values
|
||||
.where((k) => k.name == name)
|
||||
.firstOrNull;
|
||||
if (match != null) order.add(match);
|
||||
}
|
||||
// Forward-compat: a kind added in a later app update won't be in an
|
||||
// older saved order yet, so append anything missing.
|
||||
for (final kind in SelectionActionKind.values) {
|
||||
if (!order.contains(kind)) order.add(kind);
|
||||
}
|
||||
_selectionActionOrder = order;
|
||||
}
|
||||
|
||||
final savedHiddenNames = prefs.getStringList(_prefHiddenTabs);
|
||||
if (savedHiddenNames != null) {
|
||||
_hiddenTabs = savedHiddenNames
|
||||
@@ -234,6 +264,17 @@ class SettingsController extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
void setSelectionActionOrder(List<SelectionActionKind> order) {
|
||||
_selectionActionOrder = order;
|
||||
notifyListeners();
|
||||
_prefsFuture.then(
|
||||
(p) => p.setStringList(
|
||||
_prefSelectionActionOrder,
|
||||
order.map((k) => k.name).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Shows or hides a tab in the bottom nav bar. Refuses to hide the last
|
||||
/// remaining visible tab. If the tab being hidden is the current default,
|
||||
/// the default falls back to the next visible tab.
|
||||
|
||||
@@ -5,6 +5,7 @@ import '../widgets/noo/nav/noo_toolbar.dart';
|
||||
import '../widgets/noo/noo_layout.dart';
|
||||
import '../widgets/settings/settings_account_card.dart';
|
||||
import '../widgets/settings/settings_accounts.dart';
|
||||
import '../widgets/settings/settings_action_bar.dart';
|
||||
import '../widgets/settings/settings_appearance.dart';
|
||||
import '../widgets/settings/settings_file_sync.dart';
|
||||
import '../widgets/settings/settings_files_cache.dart';
|
||||
@@ -12,9 +13,11 @@ import '../widgets/settings/settings_security.dart';
|
||||
import '../widgets/settings/settings_swipe.dart';
|
||||
import '../widgets/settings/settings_tabs.dart';
|
||||
|
||||
/// Settings, pushed on top of the shell (DESIGN_SYSTEM.md 4's 8-section
|
||||
/// Settings, pushed on top of the shell (DESIGN_SYSTEM.md 4's 9-section
|
||||
/// order: account card, accounts, security, file sync, files cache,
|
||||
/// appearance, tabs, swipe on a file). One column of [SettingsSection]s on
|
||||
/// appearance, tabs, action bar, swipe on a file). One column of
|
||||
/// [SettingsSection]s (and [SettingsActionBarSection], which has no option
|
||||
/// rows of its own to put in one - just the reorder list) on
|
||||
/// mobile; a 2-column grid of cards on desktop - see each
|
||||
/// `widgets/settings/*.dart` file for a section's own content and any
|
||||
/// setting that had to be slotted in or grouped under "Advanced appearance".
|
||||
@@ -29,6 +32,7 @@ class AccountView extends StatelessWidget {
|
||||
SettingsFilesCacheSection(),
|
||||
SettingsAppearanceSection(),
|
||||
SettingsTabsSection(),
|
||||
SettingsActionBarSection(),
|
||||
SettingsSwipeSection(),
|
||||
];
|
||||
|
||||
@@ -48,7 +52,9 @@ class AccountView extends StatelessWidget {
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: desktop ? _DesktopGrid(sections: _sections) : _MobileList(sections: _sections),
|
||||
child: desktop
|
||||
? _DesktopGrid(sections: _sections)
|
||||
: _MobileList(sections: _sections),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -61,10 +67,18 @@ class _MobileList extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(NooSpace.sm, NooSpace.sm, NooSpace.sm, NooSpace.xxl),
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
NooSpace.sm,
|
||||
NooSpace.sm,
|
||||
NooSpace.sm,
|
||||
NooSpace.xxl,
|
||||
),
|
||||
physics: const BouncingScrollPhysics(),
|
||||
children: [
|
||||
for (final section in sections) ...[section, const SizedBox(height: NooSpace.xl)],
|
||||
for (final section in sections) ...[
|
||||
section,
|
||||
const SizedBox(height: NooSpace.xl),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -112,7 +126,10 @@ class _Column extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final child in children) ...[child, const SizedBox(height: NooSpace.xl)],
|
||||
for (final child in children) ...[
|
||||
child,
|
||||
const SizedBox(height: NooSpace.xl),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -96,13 +96,15 @@ class _FavoritesViewState extends State<FavoritesView> {
|
||||
/// The bulk actions shown in the sticky selection toolbar for the
|
||||
/// currently-selected items - same set Files/Photos offer.
|
||||
List<SelectionAction> _buildSelectionActions(List<NextcloudItem> selected) {
|
||||
return [
|
||||
final actions = [
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.favorite,
|
||||
icon: LucideIcons.starOff,
|
||||
label: 'Remove from favorites',
|
||||
onTap: () => _unfavoriteSelected(selected),
|
||||
),
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.share,
|
||||
icon: LucideIcons.share2,
|
||||
label: 'Share',
|
||||
onTap: () => selected.length == 1
|
||||
@@ -110,32 +112,41 @@ class _FavoritesViewState extends State<FavoritesView> {
|
||||
: _shareSelected(context, selected),
|
||||
),
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.download,
|
||||
icon: LucideIcons.download,
|
||||
label: 'Download',
|
||||
onTap: () => _downloadSelected(context, selected),
|
||||
),
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.delete,
|
||||
icon: LucideIcons.trash2,
|
||||
label: 'Delete',
|
||||
onTap: () => _confirmDeleteSelected(context, selected),
|
||||
),
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.copy,
|
||||
icon: LucideIcons.copy,
|
||||
label: 'Copy',
|
||||
onTap: () => _moveOrCopySelected(selected, copy: true),
|
||||
),
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.move,
|
||||
icon: LucideIcons.folderInput,
|
||||
label: 'Move',
|
||||
onTap: () => _moveOrCopySelected(selected, copy: false),
|
||||
),
|
||||
if (selected.length == 1)
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.details,
|
||||
icon: LucideIcons.info,
|
||||
label: 'Details',
|
||||
onTap: () => DetailsSheet.show(context, selected.single),
|
||||
),
|
||||
];
|
||||
return orderSelectionActions(
|
||||
actions,
|
||||
context.read<SettingsController>().selectionActionOrder,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _unfavoriteSelected(List<NextcloudItem> items) async {
|
||||
|
||||
@@ -419,6 +419,7 @@ class _FilesViewState extends State<FilesView>
|
||||
if (pick.isPicking) {
|
||||
return [
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.favorite,
|
||||
icon: LucideIcons.check,
|
||||
label: 'Use ${selected.length} item(s)',
|
||||
onTap: () => pick.confirmPick(selected),
|
||||
@@ -427,13 +428,15 @@ class _FilesViewState extends State<FilesView>
|
||||
}
|
||||
final allFavorited = selected.every((i) => i.isFavorite);
|
||||
final allSynced = selected.every((i) => sync.isPathSynced(i.path));
|
||||
return [
|
||||
final actions = [
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.favorite,
|
||||
icon: allFavorited ? LucideIcons.starOff : LucideIcons.star,
|
||||
label: allFavorited ? 'Remove from favorites' : 'Favorite',
|
||||
onTap: () => _favoriteSelected(context, selected),
|
||||
),
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.share,
|
||||
icon: LucideIcons.share2,
|
||||
label: 'Share',
|
||||
onTap: () => selected.length == 1
|
||||
@@ -441,43 +444,54 @@ class _FilesViewState extends State<FilesView>
|
||||
: _shareSelected(context, selected),
|
||||
),
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.download,
|
||||
icon: LucideIcons.download,
|
||||
label: 'Download',
|
||||
onTap: () => _downloadSelected(context, selected),
|
||||
),
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.delete,
|
||||
icon: LucideIcons.trash2,
|
||||
label: 'Delete',
|
||||
onTap: () => _confirmDeleteSelected(context, selected),
|
||||
),
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.copy,
|
||||
icon: LucideIcons.copy,
|
||||
label: 'Copy',
|
||||
onTap: () => _moveOrCopySelected(context, selected, copy: true),
|
||||
),
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.move,
|
||||
icon: LucideIcons.folderInput,
|
||||
label: 'Move',
|
||||
onTap: () => _moveOrCopySelected(context, selected, copy: false),
|
||||
),
|
||||
if (selected.length == 1)
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.rename,
|
||||
icon: LucideIcons.filePen,
|
||||
label: 'Rename',
|
||||
onTap: () => _renameItem(selected.single),
|
||||
),
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.sync,
|
||||
icon: LucideIcons.hardDriveDownload,
|
||||
label: allSynced ? 'Stop syncing to device' : 'Sync to device',
|
||||
onTap: () => _toggleSyncSelected(context, sync, selected),
|
||||
),
|
||||
if (selected.length == 1)
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.details,
|
||||
icon: LucideIcons.info,
|
||||
label: 'Details',
|
||||
onTap: () => DetailsSheet.show(context, selected.single),
|
||||
),
|
||||
];
|
||||
return orderSelectionActions(
|
||||
actions,
|
||||
context.read<SettingsController>().selectionActionOrder,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _renameItem(NextcloudItem item) async {
|
||||
|
||||
@@ -11,6 +11,7 @@ import '../providers/item_operations.dart';
|
||||
import '../providers/photos_controller.dart';
|
||||
import '../providers/pick_controller.dart';
|
||||
import '../providers/session_controller.dart';
|
||||
import '../providers/settings_controller.dart';
|
||||
import '../services/download_service.dart';
|
||||
import '../theme/design_tokens.dart';
|
||||
import '../widgets/details/details_sheet.dart';
|
||||
@@ -113,6 +114,7 @@ class _PhotosViewState extends State<PhotosView> {
|
||||
if (pick.isPicking) {
|
||||
return [
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.favorite,
|
||||
icon: LucideIcons.check,
|
||||
label: 'Use ${selected.length} item(s)',
|
||||
onTap: () => pick.confirmPick(selected),
|
||||
@@ -123,13 +125,15 @@ class _PhotosViewState extends State<PhotosView> {
|
||||
// uses for favorite/unfavorite (see `files_view.dart`) rather than a
|
||||
// filled heart.
|
||||
final allFavorited = selected.every((i) => i.isFavorite);
|
||||
return [
|
||||
final actions = [
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.favorite,
|
||||
icon: allFavorited ? LucideIcons.starOff : LucideIcons.star,
|
||||
label: allFavorited ? 'Remove from favorites' : 'Favorite',
|
||||
onTap: () => _favoriteSelected(selected),
|
||||
),
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.share,
|
||||
icon: LucideIcons.share2,
|
||||
label: 'Share',
|
||||
onTap: () => selected.length == 1
|
||||
@@ -137,32 +141,41 @@ class _PhotosViewState extends State<PhotosView> {
|
||||
: _shareSelected(context, selected),
|
||||
),
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.download,
|
||||
icon: LucideIcons.download,
|
||||
label: 'Download',
|
||||
onTap: () => _downloadSelected(context, selected),
|
||||
),
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.delete,
|
||||
icon: LucideIcons.trash2,
|
||||
label: 'Delete',
|
||||
onTap: () => _confirmDeleteSelected(context, selected),
|
||||
),
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.copy,
|
||||
icon: LucideIcons.copy,
|
||||
label: 'Copy',
|
||||
onTap: () => _moveOrCopySelected(selected, copy: true),
|
||||
),
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.move,
|
||||
icon: LucideIcons.folderInput,
|
||||
label: 'Move',
|
||||
onTap: () => _moveOrCopySelected(selected, copy: false),
|
||||
),
|
||||
if (selected.length == 1)
|
||||
SelectionAction(
|
||||
kind: SelectionActionKind.details,
|
||||
icon: LucideIcons.info,
|
||||
label: 'Details',
|
||||
onTap: () => DetailsSheet.show(context, selected.single),
|
||||
),
|
||||
];
|
||||
return orderSelectionActions(
|
||||
actions,
|
||||
context.read<SettingsController>().selectionActionOrder,
|
||||
);
|
||||
}
|
||||
|
||||
/// Splits an already-filtered/sorted list into contiguous month runs
|
||||
|
||||
@@ -11,6 +11,11 @@ class NooTabOrderRow extends StatelessWidget {
|
||||
final bool pinned;
|
||||
final VoidCallback? onTogglePin;
|
||||
|
||||
/// False hides the trailing pin toggle entirely - for reorder-only lists
|
||||
/// (Settings' Action bar section) that have no "pinned" concept, just an
|
||||
/// order.
|
||||
final bool showPin;
|
||||
|
||||
/// This row's index inside its `ReorderableListView` - when set, only the
|
||||
/// grip icon starts a drag (`ReorderableDragStartListener` wraps just the
|
||||
/// icon, not the whole row), so a vertical drag/scroll starting anywhere
|
||||
@@ -25,6 +30,7 @@ class NooTabOrderRow extends StatelessWidget {
|
||||
required this.label,
|
||||
this.pinned = false,
|
||||
this.onTogglePin,
|
||||
this.showPin = true,
|
||||
this.dragIndex,
|
||||
});
|
||||
|
||||
@@ -50,23 +56,24 @@ class NooTabOrderRow extends StatelessWidget {
|
||||
style: NooText.bodyL.copyWith(height: 1, color: colors.fg1),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: onTogglePin,
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: pinned ? colors.accentSoft : colors.surface2,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
pinned ? LucideIcons.pin : LucideIcons.pinOff,
|
||||
size: 16,
|
||||
color: pinned ? colors.accentText : colors.fg3,
|
||||
if (showPin)
|
||||
GestureDetector(
|
||||
onTap: onTogglePin,
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: pinned ? colors.accentSoft : colors.surface2,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
pinned ? LucideIcons.pin : LucideIcons.pinOff,
|
||||
size: 16,
|
||||
color: pinned ? colors.accentText : colors.fg3,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../models/selection_action.dart';
|
||||
import '../../providers/settings_controller.dart';
|
||||
import '../../theme/design_tokens.dart';
|
||||
import '../noo/lists/noo_tab_order_row.dart';
|
||||
import '../noo/noo_layout.dart';
|
||||
|
||||
/// Settings section: the priority order bulk actions (favorite, share,
|
||||
/// download, ...) show in on the Files/Photos/Favorites multi-select
|
||||
/// action bar. Every action stays available everywhere - this only decides
|
||||
/// which ones land in `NooSelectionBar`'s fixed inline slots (3 mobile / 4
|
||||
/// desktop, see its own doc comment) vs. behind its trailing "More" -
|
||||
/// there's no pin/hide concept here the way Tabs has, so this is just a
|
||||
/// reorder list, not a whole section of its own option rows.
|
||||
class SettingsActionBarSection extends StatelessWidget {
|
||||
const SettingsActionBarSection({super.key});
|
||||
|
||||
static const _subtitle =
|
||||
'Drag to reorder - the first ones shown appear directly in the bar, the rest sit behind "More"';
|
||||
|
||||
void _handleReorder(SettingsController settings, int oldIndex, int newIndex) {
|
||||
final order = List<SelectionActionKind>.from(settings.selectionActionOrder);
|
||||
final kind = order.removeAt(oldIndex);
|
||||
order.insert(newIndex, kind);
|
||||
settings.setSelectionActionOrder(order);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final settings = context.watch<SettingsController>();
|
||||
final desktop = NooLayout.isDesktop(context);
|
||||
|
||||
final orderList = _ActionOrderList(
|
||||
order: settings.selectionActionOrder,
|
||||
onReorder: (oldIndex, newIndex) =>
|
||||
_handleReorder(settings, oldIndex, newIndex),
|
||||
);
|
||||
|
||||
final colors = context.nooColors;
|
||||
|
||||
if (!desktop) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
'Action bar',
|
||||
style: NooText.label.copyWith(color: colors.fg2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
_subtitle,
|
||||
style: NooText.meta.copyWith(color: colors.fg3),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
orderList,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surface,
|
||||
border: Border.all(color: colors.line),
|
||||
borderRadius: BorderRadius.circular(NooRadii.card),
|
||||
),
|
||||
padding: const EdgeInsets.all(NooSpace.lg),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Action bar',
|
||||
style: NooText.cardTitle.copyWith(color: colors.fg1),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(_subtitle, style: NooText.meta.copyWith(color: colors.fg3)),
|
||||
const SizedBox(height: NooSpace.md),
|
||||
orderList,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The reorderable [NooTabOrderRow] stack for [SelectionActionKind]s - same
|
||||
/// radius-20, line-divided card `settings_tabs.dart`'s `_TabOrderList`
|
||||
/// draws, minus the pin toggle (`showPin: false`): there's nothing to
|
||||
/// show/hide here, only an order.
|
||||
class _ActionOrderList extends StatelessWidget {
|
||||
final List<SelectionActionKind> order;
|
||||
final void Function(int oldIndex, int newIndex) onReorder;
|
||||
|
||||
const _ActionOrderList({required this.order, required this.onReorder});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = context.nooColors;
|
||||
final list = ReorderableListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
buildDefaultDragHandles: false,
|
||||
itemCount: order.length,
|
||||
onReorderItem: onReorder,
|
||||
itemBuilder: (context, index) {
|
||||
final kind = order[index];
|
||||
return Padding(
|
||||
key: ValueKey(kind),
|
||||
padding: EdgeInsets.only(top: index > 0 ? 1 : 0),
|
||||
child: NooTabOrderRow(
|
||||
icon: kind.icon,
|
||||
label: kind.label,
|
||||
showPin: false,
|
||||
dragIndex: index,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(NooRadii.card),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(color: colors.line),
|
||||
child: list,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,15 @@ import 'package:noo/widgets/noo/lists/noo_tab_order_row.dart';
|
||||
import 'noo_test_utils.dart';
|
||||
|
||||
/// [n] actions labelled A, B, C, ... each incrementing an entry in [taps]
|
||||
/// keyed by its own label when tapped.
|
||||
/// keyed by its own label when tapped. [n] must be at most
|
||||
/// `SelectionActionKind.values.length` - each generated action needs its
|
||||
/// own distinct kind.
|
||||
List<SelectionAction> _actions(int n, Map<String, int> taps) =>
|
||||
List.generate(n, (i) {
|
||||
final label = String.fromCharCode(65 + i);
|
||||
taps[label] = 0;
|
||||
return SelectionAction(
|
||||
kind: SelectionActionKind.values[i],
|
||||
icon: LucideIcons.star,
|
||||
label: label,
|
||||
onTap: () => taps[label] = taps[label]! + 1,
|
||||
|
||||
Reference in New Issue
Block a user