Add action bar preview and all swipe actions; rename App lock
Check PR / check (pull_request) Successful in 56s
Check PR / check (pull_request) Successful in 56s
This commit is contained in:
@@ -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
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user