Add action bar preview and all swipe actions; rename App lock
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.
|
||||
|
||||
**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.
|
||||
- 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
|
||||
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 |
|
||||
| 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. |
|
||||
| Biometric label | "Lock with Face ID" | "Lock with fingerprint" |
|
||||
| App lock label | "App lock" | "App lock" |
|
||||
|
||||
### Desktop
|
||||
|
||||
@@ -366,7 +366,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,
|
||||
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".
|
||||
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
|
||||
|
||||
Mobile is a two-level menu, the way native iOS/Android Settings apps
|
||||
|
||||
@@ -111,6 +111,17 @@ Gotchas:
|
||||
in one gesture, it's not tap-only anymore.
|
||||
- Window chrome (macOS traffic lights, the Windows 40px title bar) isn't
|
||||
built yet. `NooSidebar.windowControls` is the slot for it.
|
||||
- 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
|
||||
`Theme.of(context).colorScheme` - the latter is Flutter's own Material 3
|
||||
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
|
||||
/// 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]
|
||||
|
||||
+17
-25
@@ -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:
|
||||
/// 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: NooSwipeActionKind.favorite,
|
||||
onTriggered: () => ops.toggleItemFavorite(item),
|
||||
kind: kind,
|
||||
label: selectedAction.label,
|
||||
icon: selectedAction.icon,
|
||||
onTriggered: kind == SelectionActionKind.delete
|
||||
? () => _confirmAndDeleteViaSwipe(item)
|
||||
: selectedAction.onTap,
|
||||
);
|
||||
case SwipeAction.delete:
|
||||
return NooSwipeActionSpec(
|
||||
kind: NooSwipeActionKind.delete,
|
||||
onTriggered: () => _confirmAndDeleteViaSwipe(item),
|
||||
);
|
||||
case SwipeAction.share:
|
||||
case SwipeAction.none:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@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',
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.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';
|
||||
|
||||
@@ -8,6 +10,52 @@ void main() {
|
||||
setUpNooTests();
|
||||
|
||||
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', (
|
||||
tester,
|
||||
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