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:
@@ -168,10 +168,10 @@ Use **Lucide** (pinned to `lucide-static@0.460.0`) at a 1.8 stroke. Sizes are 14
|
|||||||
- Shared: `users` in fg-3.
|
- Shared: `users` in fg-3.
|
||||||
|
|
||||||
**Swipe action**
|
**Swipe action**
|
||||||
- The row slides and uncovers a 96px action block. Delete is white on danger-fill; Favorite is white on accent.
|
- The row slides and uncovers a 96px action block. Delete is white on danger-fill; all other actions are white on accent.
|
||||||
- The block has a 20px icon above a 12/600 label.
|
- The block has a 20px icon above a 12/600 label.
|
||||||
- Releasing past half the block snaps it open; dragging further still (past 1.8× the block's width, a haptic tick marks the crossing and the icon bumps up 15%) arms the action, and releasing while armed fires it immediately - one swipe-through gesture instead of open-then-tap. The block grows to fill however far it's dragged, so there's never a gap behind it.
|
- Releasing past half the block snaps it open; dragging further still (past 1.8× the block's width, a haptic tick marks the crossing and the icon bumps up 15%) arms the action, and releasing while armed fires it immediately - one swipe-through gesture instead of open-then-tap. The block grows to fill however far it's dragged, so there's never a gap behind it.
|
||||||
- Swipe actions are set in Settings → Swipe on a file.
|
- Swipe actions are set in Settings → Swipe on a file. Both directions offer None and every action-bar action: Favorite, Share, Download, Delete, Copy, Move, Rename, Sync to device, Details. They use the same single-item handlers; Delete still asks for confirmation.
|
||||||
|
|
||||||
**Selection action bar** (Files, Photos, Favorites - anywhere with
|
**Selection action bar** (Files, Photos, Favorites - anywhere with
|
||||||
multi-select)
|
multi-select)
|
||||||
@@ -289,7 +289,7 @@ Mobile always has **5 pinned tabs in the bottom bar**. The rest go in the drawer
|
|||||||
| Upload | `plus` in the nav bar | Extended FAB - fully round (pill) when the bottom bar is floating, rounded square (`card` radius) when it's attached |
|
| Upload | `plus` in the nav bar | Extended FAB - fully round (pill) when the bottom bar is floating, rounded square (`card` radius) when it's attached |
|
||||||
| Overflow icon | `ellipsis` | `ellipsis-vertical` |
|
| Overflow icon | `ellipsis` | `ellipsis-vertical` |
|
||||||
| Settings | Pushed screen with a "‹ Files" back button and a large title. No bottom bar. | Pushed screen with a back arrow and a title. No bottom bar. |
|
| Settings | Pushed screen with a "‹ Files" back button and a large title. No bottom bar. | Pushed screen with a back arrow and a title. No bottom bar. |
|
||||||
| Biometric label | "Lock with Face ID" | "Lock with fingerprint" |
|
| App lock label | "App lock" | "App lock" |
|
||||||
|
|
||||||
### Wide tablet
|
### Wide tablet
|
||||||
|
|
||||||
@@ -359,7 +359,9 @@ Sidebar items are 38px tall with radius 12, an 18px icon and a 14/500 label. The
|
|||||||
priority order for the Selection action bar's bulk actions (favorite,
|
priority order for the Selection action bar's bulk actions (favorite,
|
||||||
share, download, delete, copy, move, rename, sync, details) - the
|
share, download, delete, copy, move, rename, sync, details) - the
|
||||||
first few (3 mobile / 4 wide tablet) land in the bar's fixed inline
|
first few (3 mobile / 4 wide tablet) land in the bar's fixed inline
|
||||||
slots, the rest sit behind "More". See §2 "Selection action bar".
|
slots, the rest sit behind "More". A live preview above the list uses
|
||||||
|
the actual bar and updates on reorder, including its More sheet. Preview
|
||||||
|
actions show feedback without touching files. See §2 "Selection action bar".
|
||||||
9. Swipe on a file
|
9. Swipe on a file
|
||||||
|
|
||||||
Mobile is a two-level menu, the way native iOS/Android Settings apps
|
Mobile is a two-level menu, the way native iOS/Android Settings apps
|
||||||
|
|||||||
@@ -109,6 +109,17 @@ Gotchas:
|
|||||||
trigger it) but also fires it directly if the drag goes far enough (past
|
trigger it) but also fires it directly if the drag goes far enough (past
|
||||||
1.8x the block's width) before release - a full swipe-through does delete
|
1.8x the block's width) before release - a full swipe-through does delete
|
||||||
in one gesture, it's not tap-only anymore.
|
in one gesture, it's not tap-only anymore.
|
||||||
|
- Swipe settings offer every `SelectionActionKind` plus None. `SwipeAction`
|
||||||
|
persists the existing enum names and maps to that shared action catalog;
|
||||||
|
`FilesView` uses the single-item action-bar handlers, with the existing
|
||||||
|
delete confirmation retained. Labels/icons update for favorited and synced
|
||||||
|
items. Swipes remain disabled during selection, picking, and offline browsing.
|
||||||
|
- Settings → Action bar shows the real `NooSelectionBar` above its reorder
|
||||||
|
list. It updates immediately and supports the same More sheet; preview
|
||||||
|
actions only show feedback, never operate on files. The bar scales down
|
||||||
|
inside Settings' narrower column while keeping the mobile/tablet split.
|
||||||
|
- The Security setting is labelled **App lock** on every platform; its
|
||||||
|
subtitle still explains device PIN or biometric authentication.
|
||||||
- Always read colors through `context.nooColors`, never
|
- Always read colors through `context.nooColors`, never
|
||||||
`Theme.of(context).colorScheme` - the latter is Flutter's own Material 3
|
`Theme.of(context).colorScheme` - the latter is Flutter's own Material 3
|
||||||
scheme, reseeded by the user's accent color choice (`AppTheme.light`/
|
scheme, reseeded by the user's accent color choice (`AppTheme.light`/
|
||||||
|
|||||||
@@ -7,7 +7,24 @@ import '../widgets/noo/nav/noo_nav_style.dart';
|
|||||||
|
|
||||||
/// What swiping a Files list-view item left/right does, user-configurable
|
/// What swiping a Files list-view item left/right does, user-configurable
|
||||||
/// in Settings.
|
/// 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
|
/// How the mobile Upload button is sized: [auto] expands on Files/Photos and
|
||||||
/// shrinks to an icon elsewhere, [mini] is always icon-only, [expanded]
|
/// 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
|
/// Reuse the single-item action handlers so swipe gestures and the action
|
||||||
/// that side is off or set to an action the design's two-slot
|
/// bar agree on labels, sheets and mutations. Delete retains its swipe
|
||||||
/// `NooSwipeAction` has no room for (`SwipeAction.share` - still reachable
|
/// confirmation before making any server changes.
|
||||||
/// via the row's overflow menu / the "Share" bulk action).
|
NooSwipeActionSpec? _swipeSpec(SwipeAction action, NextcloudItem item) {
|
||||||
NooSwipeActionSpec? _swipeSpec(
|
final kind = action.selectionKind;
|
||||||
SwipeAction action,
|
if (kind == null) return null;
|
||||||
NextcloudItem item,
|
final selectedAction = _buildSelectionActions(context, [
|
||||||
ItemOperations ops,
|
item,
|
||||||
) {
|
]).firstWhere((candidate) => candidate.kind == kind);
|
||||||
switch (action) {
|
return NooSwipeActionSpec(
|
||||||
case SwipeAction.favorite:
|
kind: kind,
|
||||||
return NooSwipeActionSpec(
|
label: selectedAction.label,
|
||||||
kind: NooSwipeActionKind.favorite,
|
icon: selectedAction.icon,
|
||||||
onTriggered: () => ops.toggleItemFavorite(item),
|
onTriggered: kind == SelectionActionKind.delete
|
||||||
);
|
? () => _confirmAndDeleteViaSwipe(item)
|
||||||
case SwipeAction.delete:
|
: selectedAction.onTap,
|
||||||
return NooSwipeActionSpec(
|
);
|
||||||
kind: NooSwipeActionKind.delete,
|
|
||||||
onTriggered: () => _confirmAndDeleteViaSwipe(item),
|
|
||||||
);
|
|
||||||
case SwipeAction.share:
|
|
||||||
case SwipeAction.none:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1343,7 +1336,6 @@ class _FilesViewState extends State<FilesView> {
|
|||||||
final sync = context.watch<SyncStatusController>();
|
final sync = context.watch<SyncStatusController>();
|
||||||
final session = context.watch<SessionController>();
|
final session = context.watch<SessionController>();
|
||||||
final settings = context.watch<SettingsController>();
|
final settings = context.watch<SettingsController>();
|
||||||
final ops = context.read<ItemOperations>();
|
|
||||||
final isSelected = _selectedIds.contains(item.id);
|
final isSelected = _selectedIds.contains(item.id);
|
||||||
// Pick mode and selection are server-side features - never on Offline.
|
// Pick mode and selection are server-side features - never on Offline.
|
||||||
final picking = !_offline && pick.isPicking;
|
final picking = !_offline && pick.isPicking;
|
||||||
@@ -1381,8 +1373,8 @@ class _FilesViewState extends State<FilesView> {
|
|||||||
final swipeable = _isSelecting || picking || _offline
|
final swipeable = _isSelecting || picking || _offline
|
||||||
? row
|
? row
|
||||||
: NooSwipeAction(
|
: NooSwipeAction(
|
||||||
startAction: _swipeSpec(settings.swipeRightAction, item, ops),
|
startAction: _swipeSpec(settings.swipeRightAction, item),
|
||||||
endAction: _swipeSpec(settings.swipeLeftAction, item, ops),
|
endAction: _swipeSpec(settings.swipeLeftAction, item),
|
||||||
child: row,
|
child: row,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import '../../../models/selection_action.dart';
|
||||||
import '../../../theme/design_tokens.dart';
|
import '../../../theme/design_tokens.dart';
|
||||||
|
|
||||||
/// The two swipe actions the design system defines (`DESIGN_SYSTEM.md` 2,
|
/// Swipe blocks share the action bar's identities, icons and default labels.
|
||||||
/// "Swipe action"); which one sits on which side is a user setting
|
typedef NooSwipeActionKind = SelectionActionKind;
|
||||||
/// (Settings -> Swipe on a file).
|
|
||||||
enum NooSwipeActionKind { delete, favorite }
|
|
||||||
|
|
||||||
/// One side's action for [NooSwipeAction].
|
/// One side's action for [NooSwipeAction].
|
||||||
class NooSwipeActionSpec {
|
class NooSwipeActionSpec {
|
||||||
final NooSwipeActionKind kind;
|
final NooSwipeActionKind kind;
|
||||||
final VoidCallback onTriggered;
|
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.
|
/// "Unfavorite" for an already-starred file.
|
||||||
final String? label;
|
final String? label;
|
||||||
|
|
||||||
/// Overrides the default icon (`trash-2` / `star`).
|
/// Overrides the action's default icon.
|
||||||
final IconData? icon;
|
final IconData? icon;
|
||||||
|
|
||||||
const NooSwipeActionSpec({
|
const NooSwipeActionSpec({
|
||||||
@@ -30,7 +28,7 @@ class NooSwipeActionSpec {
|
|||||||
|
|
||||||
/// Wraps a row (normally a `NooFileRow`) so dragging it horizontally slides
|
/// Wraps a row (normally a `NooFileRow`) so dragging it horizontally slides
|
||||||
/// it aside to uncover a 96px action block: delete is white on
|
/// 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
|
/// 12/600 label. Releasing past half the block snaps it open, otherwise it
|
||||||
/// springs shut - both on [NooMotion.base] / [NooMotion.ease], no bounce.
|
/// springs shut - both on [NooMotion.base] / [NooMotion.ease], no bounce.
|
||||||
/// Tapping the block fires the action and closes it; tapping the row while
|
/// 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) {
|
Widget build(BuildContext context) {
|
||||||
final isDelete = spec.kind == NooSwipeActionKind.delete;
|
final isDelete = spec.kind == NooSwipeActionKind.delete;
|
||||||
final bg = isDelete ? colors.dangerFill : colors.accent;
|
final bg = isDelete ? colors.dangerFill : colors.accent;
|
||||||
final icon =
|
final icon = spec.icon ?? spec.kind.icon;
|
||||||
spec.icon ?? (isDelete ? LucideIcons.trash2 : LucideIcons.star);
|
final label = spec.label ?? spec.kind.label;
|
||||||
final label = spec.label ?? (isDelete ? 'Delete' : 'Favorite');
|
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import '../../models/selection_action.dart';
|
|||||||
import '../../providers/settings_controller.dart';
|
import '../../providers/settings_controller.dart';
|
||||||
import '../../theme/design_tokens.dart';
|
import '../../theme/design_tokens.dart';
|
||||||
import '../noo/lists/noo_tab_order_row.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_settings_row.dart';
|
||||||
import '../noo/lists/noo_grouped_list.dart';
|
import '../noo/lists/noo_grouped_list.dart';
|
||||||
import '../noo/core/noo_segmented_control.dart';
|
import '../noo/core/noo_segmented_control.dart';
|
||||||
@@ -58,7 +59,11 @@ class SettingsActionBarSection extends StatelessWidget {
|
|||||||
if (!desktop) {
|
if (!desktop) {
|
||||||
return NooGroupedList(
|
return NooGroupedList(
|
||||||
footer: const Text(_subtitle),
|
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),
|
const SizedBox(height: 4),
|
||||||
Text(_subtitle, style: NooText.meta.copyWith(color: colors.fg3)),
|
Text(_subtitle, style: NooText.meta.copyWith(color: colors.fg3)),
|
||||||
const SizedBox(height: NooSpace.md),
|
const SizedBox(height: NooSpace.md),
|
||||||
|
_ActionBarPreview(order: settings.selectionActionOrder),
|
||||||
|
const SizedBox(height: NooSpace.md),
|
||||||
positionRow,
|
positionRow,
|
||||||
const SizedBox(height: NooSpace.md),
|
const SizedBox(height: NooSpace.md),
|
||||||
orderList,
|
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
|
/// The reorderable [NooTabOrderRow] stack for [SelectionActionKind]s - same
|
||||||
/// radius-20, line-divided card `settings_tabs.dart`'s `_TabOrderList`
|
/// radius-20, line-divided card `settings_tabs.dart`'s `_TabOrderList`
|
||||||
/// draws, minus the pin toggle (`showPin: false`): there's nothing to
|
/// 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 '../noo/lists/noo_settings_row.dart';
|
||||||
import 'settings_section.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
|
/// Settings section 3: three independent locks - opening the app, switching
|
||||||
/// accounts, and revealing hidden files - each behind the device's own PIN/
|
/// accounts, and revealing hidden files - each behind the device's own PIN/
|
||||||
/// biometric credential (see `AppLockService` - this app never stores or
|
/// biometric credential (see `AppLockService` - this app never stores or
|
||||||
@@ -58,14 +38,13 @@ class SettingsSecuritySection extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final session = context.watch<SessionController>();
|
final session = context.watch<SessionController>();
|
||||||
final platform = Theme.of(context).platform;
|
|
||||||
|
|
||||||
return SettingsSection(
|
return SettingsSection(
|
||||||
title: 'Security',
|
title: 'Security',
|
||||||
children: [
|
children: [
|
||||||
NooSettingsRow(
|
NooSettingsRow(
|
||||||
icon: LucideIcons.lock,
|
icon: LucideIcons.lock,
|
||||||
label: Text(biometricLabel(platform)),
|
label: const Text('App lock'),
|
||||||
subtitle: const Text(
|
subtitle: const Text(
|
||||||
"Require this device's PIN or biometric to open Noo",
|
"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 '../noo/lists/noo_settings_row.dart';
|
||||||
import 'settings_section.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.
|
/// Settings section 8: what swiping a file left/right does in list view.
|
||||||
class SettingsSwipeSection extends StatelessWidget {
|
class SettingsSwipeSection extends StatelessWidget {
|
||||||
const SettingsSwipeSection({super.key});
|
const SettingsSwipeSection({super.key});
|
||||||
@@ -35,7 +22,8 @@ class SettingsSwipeSection extends StatelessWidget {
|
|||||||
options: [
|
options: [
|
||||||
for (final action in SwipeAction.values)
|
for (final action in SwipeAction.values)
|
||||||
NooSettingsRow(
|
NooSettingsRow(
|
||||||
label: Text(_swipeActionLabel(action)),
|
icon: action.selectionKind?.icon,
|
||||||
|
label: Text(action.label),
|
||||||
trailing: action == current
|
trailing: action == current
|
||||||
? Icon(
|
? Icon(
|
||||||
LucideIcons.check,
|
LucideIcons.check,
|
||||||
@@ -64,7 +52,7 @@ class SettingsSwipeSection extends StatelessWidget {
|
|||||||
NooSettingsRow(
|
NooSettingsRow(
|
||||||
icon: LucideIcons.chevronsRight,
|
icon: LucideIcons.chevronsRight,
|
||||||
label: const Text('Swipe right'),
|
label: const Text('Swipe right'),
|
||||||
value: _swipeActionLabel(settings.swipeRightAction),
|
value: settings.swipeRightAction.label,
|
||||||
onTap: () => _openPicker(
|
onTap: () => _openPicker(
|
||||||
context,
|
context,
|
||||||
title: 'Swipe right',
|
title: 'Swipe right',
|
||||||
@@ -75,7 +63,7 @@ class SettingsSwipeSection extends StatelessWidget {
|
|||||||
NooSettingsRow(
|
NooSettingsRow(
|
||||||
icon: LucideIcons.chevronsLeft,
|
icon: LucideIcons.chevronsLeft,
|
||||||
label: const Text('Swipe left'),
|
label: const Text('Swipe left'),
|
||||||
value: _swipeActionLabel(settings.swipeLeftAction),
|
value: settings.swipeLeftAction.label,
|
||||||
onTap: () => _openPicker(
|
onTap: () => _openPicker(
|
||||||
context,
|
context,
|
||||||
title: 'Swipe left',
|
title: 'Swipe left',
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:noo/widgets/noo/files/noo_swipe_action.dart';
|
import 'package:noo/widgets/noo/files/noo_swipe_action.dart';
|
||||||
|
import 'package:noo/models/selection_action.dart';
|
||||||
|
import 'package:noo/providers/settings_controller.dart';
|
||||||
|
|
||||||
import 'noo_test_utils.dart';
|
import 'noo_test_utils.dart';
|
||||||
|
|
||||||
@@ -8,6 +10,52 @@ void main() {
|
|||||||
setUpNooTests();
|
setUpNooTests();
|
||||||
|
|
||||||
group('NooSwipeAction', () {
|
group('NooSwipeAction', () {
|
||||||
|
test('every action-bar action has a swipe setting', () {
|
||||||
|
expect(
|
||||||
|
SwipeAction.values
|
||||||
|
.map((action) => action.selectionKind)
|
||||||
|
.whereType<SelectionActionKind>()
|
||||||
|
.toSet(),
|
||||||
|
SelectionActionKind.values.toSet(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (final kind in SelectionActionKind.values) {
|
||||||
|
testNooWidgets('a full swipe runs ${kind.label}', (
|
||||||
|
tester,
|
||||||
|
theme,
|
||||||
|
c,
|
||||||
|
) async {
|
||||||
|
var triggered = 0;
|
||||||
|
await pumpNoo(
|
||||||
|
tester,
|
||||||
|
SizedBox(
|
||||||
|
width: 360,
|
||||||
|
height: 64,
|
||||||
|
child: NooSwipeAction(
|
||||||
|
startAction: NooSwipeActionSpec(
|
||||||
|
kind: kind,
|
||||||
|
onTriggered: () => triggered++,
|
||||||
|
),
|
||||||
|
child: const ColoredBox(
|
||||||
|
color: Colors.white,
|
||||||
|
child: SizedBox(width: 360, height: 64),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
theme: theme,
|
||||||
|
);
|
||||||
|
await tester.drag(find.byType(NooSwipeAction), const Offset(60, 0));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(triggered, 0);
|
||||||
|
expect(find.text(kind.label), findsOneWidget);
|
||||||
|
expect(find.byIcon(kind.icon), findsOneWidget);
|
||||||
|
await tester.drag(find.byType(NooSwipeAction), const Offset(300, 0));
|
||||||
|
await tester.pump();
|
||||||
|
expect(triggered, 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
testNooWidgets('a moderate swipe opens the action without triggering it', (
|
testNooWidgets('a moderate swipe opens the action without triggering it', (
|
||||||
tester,
|
tester,
|
||||||
theme,
|
theme,
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:noo/models/selection_action.dart';
|
||||||
|
import 'package:noo/providers/settings_controller.dart';
|
||||||
|
import 'package:noo/widgets/noo/lists/noo_selection_bar.dart';
|
||||||
|
import 'package:noo/widgets/settings/settings_action_bar.dart';
|
||||||
|
import 'package:noo/widgets/settings/settings_swipe.dart';
|
||||||
|
|
||||||
|
import 'noo/noo_test_utils.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
setUpNooTests();
|
||||||
|
setUp(() => SharedPreferences.setMockInitialValues({}));
|
||||||
|
|
||||||
|
for (final desktop in [false, true]) {
|
||||||
|
testNooWidgets(
|
||||||
|
'action preview follows reorder and exposes overflow (${desktop ? 'tablet' : 'phone'})',
|
||||||
|
(tester, theme, colors) async {
|
||||||
|
tester.view.devicePixelRatio = 1;
|
||||||
|
tester.view.physicalSize = desktop
|
||||||
|
? const Size(1200, 900)
|
||||||
|
: const Size(380, 900);
|
||||||
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
|
final settings = SettingsController();
|
||||||
|
addTearDown(settings.dispose);
|
||||||
|
await pumpNoo(
|
||||||
|
tester,
|
||||||
|
ChangeNotifierProvider.value(
|
||||||
|
value: settings,
|
||||||
|
child: SizedBox(
|
||||||
|
width: desktop ? 640 : 340,
|
||||||
|
child: const SingleChildScrollView(
|
||||||
|
child: SettingsActionBarSection(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
theme: theme,
|
||||||
|
surfaceSize: desktop ? const Size(1200, 900) : const Size(380, 900),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
final bar = find.byType(NooSelectionBar);
|
||||||
|
expect(bar, findsOneWidget);
|
||||||
|
expect(tester.widget<NooSelectionBar>(bar).isDesktop, desktop);
|
||||||
|
expect(find.text('Preview'), findsOneWidget);
|
||||||
|
settings.setSelectionActionOrder([
|
||||||
|
SelectionActionKind.details,
|
||||||
|
...SelectionActionKind.values.where(
|
||||||
|
(kind) => kind != SelectionActionKind.details,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(
|
||||||
|
find.descendant(of: bar, matching: find.byIcon(LucideIcons.info)),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Details preview'), findsOneWidget);
|
||||||
|
await tester.tap(
|
||||||
|
find.descendant(
|
||||||
|
of: bar,
|
||||||
|
matching: desktop
|
||||||
|
? find.text('More')
|
||||||
|
: find.byIcon(LucideIcons.ellipsisVertical),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Sync to device'), findsNWidgets(2));
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
testNooWidgets('swipe picker offers every action and saves the choice', (
|
||||||
|
tester,
|
||||||
|
theme,
|
||||||
|
colors,
|
||||||
|
) async {
|
||||||
|
final settings = SettingsController();
|
||||||
|
addTearDown(settings.dispose);
|
||||||
|
await pumpNoo(
|
||||||
|
tester,
|
||||||
|
ChangeNotifierProvider.value(
|
||||||
|
value: settings,
|
||||||
|
child: const SettingsSwipeSection(),
|
||||||
|
),
|
||||||
|
theme: theme,
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('Swipe right'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
for (final kind in SelectionActionKind.values) {
|
||||||
|
expect(find.text(kind.label), findsAtLeastNWidgets(1));
|
||||||
|
}
|
||||||
|
await tester.ensureVisible(find.text('Details'));
|
||||||
|
await tester.tap(find.text('Details'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(settings.swipeRightAction, SwipeAction.details);
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
expect(prefs.getString('ui_swipe_right_action'), 'details');
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user