Add action bar preview and all swipe actions; rename App lock
Check PR / check (pull_request) Successful in 56s

This commit is contained in:
Ayushya Amitabh
2026-10-07 23:14:21 -04:00
parent 91ab8b4a07
commit 2d244ce335
10 changed files with 277 additions and 82 deletions
+18 -1
View File
@@ -7,7 +7,24 @@ import '../widgets/noo/nav/noo_nav_style.dart';
/// What swiping a Files list-view item left/right does, user-configurable
/// in Settings.
enum SwipeAction { none, favorite, delete, share }
enum SwipeAction {
none(null),
favorite(SelectionActionKind.favorite),
delete(SelectionActionKind.delete),
share(SelectionActionKind.share),
download(SelectionActionKind.download),
copy(SelectionActionKind.copy),
move(SelectionActionKind.move),
rename(SelectionActionKind.rename),
sync(SelectionActionKind.sync),
details(SelectionActionKind.details);
const SwipeAction(this.selectionKind);
final SelectionActionKind? selectionKind;
String get label => selectionKind?.label ?? 'None';
}
/// How the mobile Upload button is sized: [auto] expands on Files/Photos and
/// shrinks to an icon elsewhere, [mini] is always icon-only, [expanded]
+19 -27
View File
@@ -629,30 +629,23 @@ class _FilesViewState extends State<FilesView> {
);
}
/// The user's configured swipe action for one side of a row, or null if
/// that side is off or set to an action the design's two-slot
/// `NooSwipeAction` has no room for (`SwipeAction.share` - still reachable
/// via the row's overflow menu / the "Share" bulk action).
NooSwipeActionSpec? _swipeSpec(
SwipeAction action,
NextcloudItem item,
ItemOperations ops,
) {
switch (action) {
case SwipeAction.favorite:
return NooSwipeActionSpec(
kind: NooSwipeActionKind.favorite,
onTriggered: () => ops.toggleItemFavorite(item),
);
case SwipeAction.delete:
return NooSwipeActionSpec(
kind: NooSwipeActionKind.delete,
onTriggered: () => _confirmAndDeleteViaSwipe(item),
);
case SwipeAction.share:
case SwipeAction.none:
return null;
}
/// Reuse the single-item action handlers so swipe gestures and the action
/// bar agree on labels, sheets and mutations. Delete retains its swipe
/// confirmation before making any server changes.
NooSwipeActionSpec? _swipeSpec(SwipeAction action, NextcloudItem item) {
final kind = action.selectionKind;
if (kind == null) return null;
final selectedAction = _buildSelectionActions(context, [
item,
]).firstWhere((candidate) => candidate.kind == kind);
return NooSwipeActionSpec(
kind: kind,
label: selectedAction.label,
icon: selectedAction.icon,
onTriggered: kind == SelectionActionKind.delete
? () => _confirmAndDeleteViaSwipe(item)
: selectedAction.onTap,
);
}
@override
@@ -1343,7 +1336,6 @@ class _FilesViewState extends State<FilesView> {
final sync = context.watch<SyncStatusController>();
final session = context.watch<SessionController>();
final settings = context.watch<SettingsController>();
final ops = context.read<ItemOperations>();
final isSelected = _selectedIds.contains(item.id);
// Pick mode and selection are server-side features - never on Offline.
final picking = !_offline && pick.isPicking;
@@ -1381,8 +1373,8 @@ class _FilesViewState extends State<FilesView> {
final swipeable = _isSelecting || picking || _offline
? row
: NooSwipeAction(
startAction: _swipeSpec(settings.swipeRightAction, item, ops),
endAction: _swipeSpec(settings.swipeLeftAction, item, ops),
startAction: _swipeSpec(settings.swipeRightAction, item),
endAction: _swipeSpec(settings.swipeLeftAction, item),
child: row,
);
+8 -11
View File
@@ -1,23 +1,21 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../../models/selection_action.dart';
import '../../../theme/design_tokens.dart';
/// The two swipe actions the design system defines (`DESIGN_SYSTEM.md` 2,
/// "Swipe action"); which one sits on which side is a user setting
/// (Settings -> Swipe on a file).
enum NooSwipeActionKind { delete, favorite }
/// Swipe blocks share the action bar's identities, icons and default labels.
typedef NooSwipeActionKind = SelectionActionKind;
/// One side's action for [NooSwipeAction].
class NooSwipeActionSpec {
final NooSwipeActionKind kind;
final VoidCallback onTriggered;
/// Overrides the default label ("Delete" / "Favorite"), e.g.
/// Overrides the action's default label, e.g.
/// "Unfavorite" for an already-starred file.
final String? label;
/// Overrides the default icon (`trash-2` / `star`).
/// Overrides the action's default icon.
final IconData? icon;
const NooSwipeActionSpec({
@@ -30,7 +28,7 @@ class NooSwipeActionSpec {
/// Wraps a row (normally a `NooFileRow`) so dragging it horizontally slides
/// it aside to uncover a 96px action block: delete is white on
/// `danger-fill`, favorite is white on `accent`, each a 20px icon over a
/// `danger-fill`, other actions are white on `accent`, each a 20px icon over a
/// 12/600 label. Releasing past half the block snaps it open, otherwise it
/// springs shut - both on [NooMotion.base] / [NooMotion.ease], no bounce.
/// Tapping the block fires the action and closes it; tapping the row while
@@ -256,9 +254,8 @@ class _ActionBlock extends StatelessWidget {
Widget build(BuildContext context) {
final isDelete = spec.kind == NooSwipeActionKind.delete;
final bg = isDelete ? colors.dangerFill : colors.accent;
final icon =
spec.icon ?? (isDelete ? LucideIcons.trash2 : LucideIcons.star);
final label = spec.label ?? (isDelete ? 'Delete' : 'Favorite');
final icon = spec.icon ?? spec.kind.icon;
final label = spec.label ?? spec.kind.label;
return GestureDetector(
behavior: HitTestBehavior.opaque,
+57 -1
View File
@@ -4,6 +4,7 @@ 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/lists/noo_selection_bar.dart';
import '../noo/lists/noo_settings_row.dart';
import '../noo/lists/noo_grouped_list.dart';
import '../noo/core/noo_segmented_control.dart';
@@ -58,7 +59,11 @@ class SettingsActionBarSection extends StatelessWidget {
if (!desktop) {
return NooGroupedList(
footer: const Text(_subtitle),
children: [positionRow, orderList],
children: [
_ActionBarPreview(order: settings.selectionActionOrder),
positionRow,
orderList,
],
);
}
@@ -79,6 +84,8 @@ class SettingsActionBarSection extends StatelessWidget {
const SizedBox(height: 4),
Text(_subtitle, style: NooText.meta.copyWith(color: colors.fg3)),
const SizedBox(height: NooSpace.md),
_ActionBarPreview(order: settings.selectionActionOrder),
const SizedBox(height: NooSpace.md),
positionRow,
const SizedBox(height: NooSpace.md),
orderList,
@@ -88,6 +95,55 @@ class SettingsActionBarSection extends StatelessWidget {
}
}
class _ActionBarPreview extends StatelessWidget {
final List<SelectionActionKind> order;
const _ActionBarPreview({required this.order});
@override
Widget build(BuildContext context) {
final colors = context.nooColors;
final desktop = NooLayout.isDesktop(context);
return Container(
padding: const EdgeInsets.all(NooSpace.sm),
color: colors.surface2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Preview', style: NooText.meta.copyWith(color: colors.fg3)),
const SizedBox(height: NooSpace.xs),
// Settings has a narrower column than the file browser. Scale the
// real bar to fit rather than changing its inline/overflow split.
FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: SizedBox(
width: desktop ? 1000 : 400,
child: NooSelectionBar(
count: 1,
isDesktop: desktop,
iosStyle: NooLayout.iosStyle(context),
onClose: () {},
actions: [
for (final kind in order)
SelectionAction(
kind: kind,
icon: kind.icon,
label: kind.label,
onTap: () => ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${kind.label} preview')),
),
),
],
),
),
),
],
),
);
}
}
/// 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
+1 -22
View File
@@ -6,26 +6,6 @@ import '../noo/core/noo_toggle.dart';
import '../noo/lists/noo_settings_row.dart';
import 'settings_section.dart';
/// Per-platform copy for the login-lock row's label (DESIGN_SYSTEM.md 3's
/// "Biometric label" row, both the mobile table and the macOS/Windows
/// desktop table). Falls back to generic copy on platforms the spec
/// doesn't name (Linux, tests).
String biometricLabel(TargetPlatform platform) {
switch (platform) {
case TargetPlatform.iOS:
return 'Lock with Face ID';
case TargetPlatform.android:
return 'Lock with fingerprint';
case TargetPlatform.macOS:
return 'Touch ID';
case TargetPlatform.windows:
return 'Windows Hello';
case TargetPlatform.linux:
case TargetPlatform.fuchsia:
return 'Lock with device credential';
}
}
/// Settings section 3: three independent locks - opening the app, switching
/// accounts, and revealing hidden files - each behind the device's own PIN/
/// biometric credential (see `AppLockService` - this app never stores or
@@ -58,14 +38,13 @@ class SettingsSecuritySection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final session = context.watch<SessionController>();
final platform = Theme.of(context).platform;
return SettingsSection(
title: 'Security',
children: [
NooSettingsRow(
icon: LucideIcons.lock,
label: Text(biometricLabel(platform)),
label: const Text('App lock'),
subtitle: const Text(
"Require this device's PIN or biometric to open Noo",
),
+4 -16
View File
@@ -6,19 +6,6 @@ import '../../theme/design_tokens.dart';
import '../noo/lists/noo_settings_row.dart';
import 'settings_section.dart';
String _swipeActionLabel(SwipeAction action) {
switch (action) {
case SwipeAction.none:
return 'None';
case SwipeAction.favorite:
return 'Favorite';
case SwipeAction.delete:
return 'Delete';
case SwipeAction.share:
return 'Share';
}
}
/// Settings section 8: what swiping a file left/right does in list view.
class SettingsSwipeSection extends StatelessWidget {
const SettingsSwipeSection({super.key});
@@ -35,7 +22,8 @@ class SettingsSwipeSection extends StatelessWidget {
options: [
for (final action in SwipeAction.values)
NooSettingsRow(
label: Text(_swipeActionLabel(action)),
icon: action.selectionKind?.icon,
label: Text(action.label),
trailing: action == current
? Icon(
LucideIcons.check,
@@ -64,7 +52,7 @@ class SettingsSwipeSection extends StatelessWidget {
NooSettingsRow(
icon: LucideIcons.chevronsRight,
label: const Text('Swipe right'),
value: _swipeActionLabel(settings.swipeRightAction),
value: settings.swipeRightAction.label,
onTap: () => _openPicker(
context,
title: 'Swipe right',
@@ -75,7 +63,7 @@ class SettingsSwipeSection extends StatelessWidget {
NooSettingsRow(
icon: LucideIcons.chevronsLeft,
label: const Text('Swipe left'),
value: _swipeActionLabel(settings.swipeLeftAction),
value: settings.swipeLeftAction.label,
onTap: () => _openPicker(
context,
title: 'Swipe left',