From 1644b1cd89d3d7496a65e6002c2c7831f91e10cc Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:42:22 +0530 Subject: [PATCH 1/5] [auth] Show sort option on home screen --- auth/lib/l10n/arb/app_en.arb | 3 + auth/lib/services/preference_service.dart | 8 ++ .../buttons/icon_button_widget.dart | 2 +- auth/lib/ui/home_page.dart | 17 ++-- auth/lib/ui/sort_option_menu.dart | 80 +++++++++++++++++++ 5 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 auth/lib/ui/sort_option_menu.dart diff --git a/auth/lib/l10n/arb/app_en.arb b/auth/lib/l10n/arb/app_en.arb index b5f7121ff6..c9776fc9f5 100644 --- a/auth/lib/l10n/arb/app_en.arb +++ b/auth/lib/l10n/arb/app_en.arb @@ -328,6 +328,9 @@ } } }, + "manualSort": "Custom", + "mostFrequentlyUsed": "Frequently used", + "mostRecentlyUsed": "Recently used", "activeSessions": "Active sessions", "somethingWentWrongPleaseTryAgain": "Something went wrong, please try again", "thisWillLogYouOutOfThisDevice": "This will log you out of this device!", diff --git a/auth/lib/services/preference_service.dart b/auth/lib/services/preference_service.dart index ba43e69be3..46c46787c8 100644 --- a/auth/lib/services/preference_service.dart +++ b/auth/lib/services/preference_service.dart @@ -2,6 +2,14 @@ import 'package:ente_auth/core/event_bus.dart'; import 'package:ente_auth/events/icons_changed_event.dart'; import 'package:shared_preferences/shared_preferences.dart'; +enum CodeSortKey { + issuerName, + accountName, + mostFrequentlyUsed, + leastRecentlyUsed, + manual, +} + class PreferenceService { PreferenceService._privateConstructor(); static final PreferenceService instance = diff --git a/auth/lib/ui/components/buttons/icon_button_widget.dart b/auth/lib/ui/components/buttons/icon_button_widget.dart index eb5554318e..d57438ee61 100644 --- a/auth/lib/ui/components/buttons/icon_button_widget.dart +++ b/auth/lib/ui/components/buttons/icon_button_widget.dart @@ -16,7 +16,7 @@ class IconButtonWidget extends StatefulWidget { final Color? defaultColor; final Color? pressedColor; final Color? iconColor; - const IconButtonWidget({ +const IconButtonWidget({ super.key, required this.icon, required this.iconButtonType, diff --git a/auth/lib/ui/home_page.dart b/auth/lib/ui/home_page.dart index f091e67307..a06f4a924f 100644 --- a/auth/lib/ui/home_page.dart +++ b/auth/lib/ui/home_page.dart @@ -31,6 +31,7 @@ import 'package:ente_auth/ui/home/speed_dial_label_widget.dart'; import 'package:ente_auth/ui/reorder_codes_page.dart'; import 'package:ente_auth/ui/scanner_page.dart'; import 'package:ente_auth/ui/settings_page.dart'; +import 'package:ente_auth/ui/sort_option_menu.dart'; import 'package:ente_auth/ui/tools/app_lock.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/platform_util.dart'; @@ -358,13 +359,17 @@ class _HomePageState extends State { ), centerTitle: PlatformUtil.isDesktop() ? false : true, actions: [ - IconButton( - icon: const Icon(Icons.edit), - tooltip: l10n.edit, - onPressed: () { - navigateToReorderPage(_allCodes!); - }, + SortCodeMenuWidget( + currentKey: CodeSortKey.accountName, + onSelected: (p0) => {}, ), + // IconButton( + // icon: const Icon(Icons.edit), + // tooltip: l10n.edit, + // onPressed: () { + // navigateToReorderPage(_allCodes!); + // }, + // ), PlatformUtil.isDesktop() ? IconButton( icon: const Icon(Icons.lock), diff --git a/auth/lib/ui/sort_option_menu.dart b/auth/lib/ui/sort_option_menu.dart new file mode 100644 index 0000000000..1cdc8515a2 --- /dev/null +++ b/auth/lib/ui/sort_option_menu.dart @@ -0,0 +1,80 @@ +import 'package:ente_auth/l10n/l10n.dart'; +import 'package:ente_auth/services/preference_service.dart'; +import 'package:ente_auth/theme/ente_theme.dart'; +import 'package:ente_auth/ui/components/buttons/icon_button_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +class SortCodeMenuWidget extends StatelessWidget { + final CodeSortKey currentKey; + final void Function(CodeSortKey) onSelected; + const SortCodeMenuWidget({ + super.key, + required this.currentKey, + required this.onSelected, + }); + + @override + Widget build(BuildContext context) { + Text sortOptionText(CodeSortKey key) { + String text = key.toString(); + switch (key) { + case CodeSortKey.issuerName: + text = context.l10n.codeIssuerHint; + break; + case CodeSortKey.accountName: + text = context.l10n.account; + break; + case CodeSortKey.mostFrequentlyUsed: + text = context.l10n.mostFrequentlyUsed; + break; + case CodeSortKey.leastRecentlyUsed: + text = context.l10n.mostRecentlyUsed; + break; + case CodeSortKey.manual: + text = context.l10n.manualSort; + } + return Text( + text, + style: Theme.of(context).textTheme.titleMedium!.copyWith( + fontSize: 14, + color: Theme.of(context).iconTheme.color!.withOpacity(0.7), + ), + ); + } + + return GestureDetector( + onTapDown: (TapDownDetails details) async { + final int? selectedValue = await showMenu( + context: context, + position: RelativeRect.fromLTRB( + details.globalPosition.dx, + details.globalPosition.dy, + details.globalPosition.dx, + details.globalPosition.dy + 300, + ), + items: List.generate(CodeSortKey.values.length, (index) { + return PopupMenuItem( + value: index, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + sortOptionText(CodeSortKey.values[index]), + if (CodeSortKey.values[index] == currentKey) + Icon( + Icons.check, + color: Theme.of(context).iconTheme.color, + ), + ], + ), + ); + }), + ); + if (selectedValue != null) { + onSelected(CodeSortKey.values[selectedValue]); + } + }, + child: const Icon(Icons.sort_outlined), + ); + } +} From c99a465c85a7da0998e8299debc2392995a0a33d Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:59:52 +0530 Subject: [PATCH 2/5] [auth] Show sorting menu on home screen --- auth/lib/services/preference_service.dart | 11 +++- auth/lib/ui/home_page.dart | 51 ++++++++++++---- auth/lib/ui/reorder_codes_page.dart | 72 +---------------------- auth/lib/ui/sort_option_menu.dart | 4 +- 4 files changed, 52 insertions(+), 86 deletions(-) diff --git a/auth/lib/services/preference_service.dart b/auth/lib/services/preference_service.dart index 46c46787c8..218eb4ffa3 100644 --- a/auth/lib/services/preference_service.dart +++ b/auth/lib/services/preference_service.dart @@ -6,7 +6,7 @@ enum CodeSortKey { issuerName, accountName, mostFrequentlyUsed, - leastRecentlyUsed, + recentlyUsed, manual, } @@ -36,6 +36,15 @@ class PreferenceService { } } + CodeSortKey codeSortKey() { + return CodeSortKey + .values[_prefs.getInt("codeSortKey") ?? CodeSortKey.manual.index]; + } + + Future setCodeSortKey(CodeSortKey key) async { + await _prefs.setInt("codeSortKey", key.index); + } + Future setHasShownCoachMark(bool value) { return _prefs.setBool(kHasShownCoachMarkKey, value); } diff --git a/auth/lib/ui/home_page.dart b/auth/lib/ui/home_page.dart index a06f4a924f..445c8bb180 100644 --- a/auth/lib/ui/home_page.dart +++ b/auth/lib/ui/home_page.dart @@ -83,11 +83,13 @@ class _HomePageState extends State { bool _isFavouriteOpen = false; bool hasFavouriteCodes = false; bool hasNonFavouriteCodes = false; + late CodeSortKey _codeSortKey; @override void initState() { super.initState(); _textController.addListener(_applyFilteringAndRefresh); + _codeSortKey = PreferenceService.instance.codeSortKey(); _loadCodes(); _streamSubscription = Bus.instance.on().listen((event) { _loadCodes(); @@ -96,6 +98,7 @@ class _HomePageState extends State { Bus.instance.on().listen((event) async { await autoLogoutAlert(context); }); + _initDeepLinks(); Future.delayed( const Duration(seconds: 1), @@ -220,8 +223,7 @@ class _HomePageState extends State { []; } - _filteredCodes - .sort((a, b) => a.display.position.compareTo(b.display.position)); + sortFilteredCodes(_filteredCodes, _codeSortKey); if (mounted) { setState(() {}); @@ -240,6 +242,28 @@ class _HomePageState extends State { super.dispose(); } + void sortFilteredCodes(List codes, CodeSortKey sortKey) { + switch (sortKey) { + case CodeSortKey.issuerName: + codes.sort((a, b) => a.issuer.compareTo(b.issuer)); + break; + case CodeSortKey.accountName: + codes.sort((a, b) => a.account.compareTo(b.account)); + break; + case CodeSortKey.mostFrequentlyUsed: + codes.sort((a, b) => b.display.tapCount.compareTo(a.display.tapCount)); + break; + case CodeSortKey.recentlyUsed: + codes.sort( + (a, b) => b.display.lastUsedAt.compareTo(a.display.lastUsedAt)); + break; + case CodeSortKey.manual: + default: + codes.sort((a, b) => a.display.position.compareTo(b.display.position)); + break; + } + } + Future _redirectToScannerPage() async { final Code? code = await Navigator.of(context).push( MaterialPageRoute( @@ -360,16 +384,21 @@ class _HomePageState extends State { centerTitle: PlatformUtil.isDesktop() ? false : true, actions: [ SortCodeMenuWidget( - currentKey: CodeSortKey.accountName, - onSelected: (p0) => {}, + currentKey: PreferenceService.instance.codeSortKey(), + onSelected: (p0) async { + await PreferenceService.instance.setCodeSortKey(p0); + + if (p0 == CodeSortKey.manual) { + await navigateToReorderPage(_allCodes!); + } + setState(() { + _codeSortKey = p0; + }); + if (mounted) { + _applyFilteringAndRefresh(); + } + }, ), - // IconButton( - // icon: const Icon(Icons.edit), - // tooltip: l10n.edit, - // onPressed: () { - // navigateToReorderPage(_allCodes!); - // }, - // ), PlatformUtil.isDesktop() ? IconButton( icon: const Icon(Icons.lock), diff --git a/auth/lib/ui/reorder_codes_page.dart b/auth/lib/ui/reorder_codes_page.dart index 3ba6160a23..f7d35c401d 100644 --- a/auth/lib/ui/reorder_codes_page.dart +++ b/auth/lib/ui/reorder_codes_page.dart @@ -34,7 +34,7 @@ class _ReorderCodesPageState extends State { }, child: Scaffold( appBar: AppBar( - title: const Text("Edit Codes"), + title: const Text("Custom order"), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () async { @@ -44,66 +44,6 @@ class _ReorderCodesPageState extends State { } }, ), - actions: [ - PopupMenuButton( - icon: const Icon(Icons.sort), - onSelected: (int value) { - selectedSortOption = value; - switch (value) { - case 0: - sortByIssuer(); - break; - case 1: - sortByAccount(); - break; - case 2: - setState(() {}); - break; - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 0, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - selectedSortOption == 0 - ? const Icon(Icons.check) - : const SizedBox.square(dimension: 24), - const SizedBox(width: 10), - const Text("Issuer"), - ], - ), - ), - PopupMenuItem( - value: 1, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - selectedSortOption == 1 - ? const Icon(Icons.check) - : const SizedBox.square(dimension: 24), - const SizedBox(width: 10), - const Text("Account"), - ], - ), - ), - PopupMenuItem( - value: 2, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - selectedSortOption == 2 - ? const Icon(Icons.check) - : const SizedBox.square(dimension: 24), - const SizedBox(width: 10), - const Text("Manual"), - ], - ), - ), - ], - ), - ], ), body: ReorderableListView( buildDefaultDragHandles: false, @@ -158,14 +98,4 @@ class _ReorderCodesPageState extends State { widget.codes.insert(newIndex, code); }); } - - void sortByIssuer() { - widget.codes.sort((a, b) => a.issuer.compareTo(b.issuer)); - setState(() {}); - } - - void sortByAccount() { - widget.codes.sort((a, b) => a.account.compareTo(b.account)); - setState(() {}); - } } diff --git a/auth/lib/ui/sort_option_menu.dart b/auth/lib/ui/sort_option_menu.dart index 1cdc8515a2..afc57515eb 100644 --- a/auth/lib/ui/sort_option_menu.dart +++ b/auth/lib/ui/sort_option_menu.dart @@ -1,7 +1,5 @@ import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/services/preference_service.dart'; -import 'package:ente_auth/theme/ente_theme.dart'; -import 'package:ente_auth/ui/components/buttons/icon_button_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; @@ -28,7 +26,7 @@ class SortCodeMenuWidget extends StatelessWidget { case CodeSortKey.mostFrequentlyUsed: text = context.l10n.mostFrequentlyUsed; break; - case CodeSortKey.leastRecentlyUsed: + case CodeSortKey.recentlyUsed: text = context.l10n.mostRecentlyUsed; break; case CodeSortKey.manual: From 9a50915678617fa17d68e9ea37b93ee18a9aa388 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 4 Dec 2024 14:16:45 +0530 Subject: [PATCH 3/5] [auth] Track tapCount & lastUsed in e2ee manner --- auth/lib/ui/code_widget.dart | 25 ++++++++++++++++++++++++- auth/lib/ui/home_page.dart | 1 + 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/auth/lib/ui/code_widget.dart b/auth/lib/ui/code_widget.dart index 3fa40a1f86..8a09e4b0c3 100644 --- a/auth/lib/ui/code_widget.dart +++ b/auth/lib/ui/code_widget.dart @@ -31,11 +31,13 @@ import 'package:move_to_background/move_to_background.dart'; class CodeWidget extends StatefulWidget { final Code code; final bool isCompactMode; + final CodeSortKey? sortKey; const CodeWidget( this.code, { super.key, required this.isCompactMode, + this.sortKey, }); @override @@ -454,11 +456,12 @@ class _CodeWidgetState extends State { ); } - void _copyCurrentOTPToClipboard() async { + void _copyCurrentOTPToClipboard() { _copyToClipboard( _getCurrentOTP(), confirmationMessage: context.l10n.copiedToClipboard, ); + _udateCodeMetadata().ignore(); } void _copyNextToClipboard() { @@ -466,6 +469,26 @@ class _CodeWidgetState extends State { _getNextTotp(), confirmationMessage: context.l10n.copiedNextToClipboard, ); + _udateCodeMetadata().ignore(); + } + + Future _udateCodeMetadata() async { + if (widget.sortKey == null) return; + Future.delayed(const Duration(milliseconds: 100), () { + if (mounted) { + if (widget.sortKey == CodeSortKey.mostFrequentlyUsed || + widget.sortKey == CodeSortKey.recentlyUsed) { + final display = widget.code.display; + final Code code = widget.code.copyWith( + display: display.copyWith( + tapCount: display.tapCount + 1, + lastUsedAt: DateTime.now().microsecondsSinceEpoch, + ), + ); + unawaited(CodeStore.instance.addCode(code)); + } + } + }); } void _copyToClipboard( diff --git a/auth/lib/ui/home_page.dart b/auth/lib/ui/home_page.dart index 445c8bb180..09520eff47 100644 --- a/auth/lib/ui/home_page.dart +++ b/auth/lib/ui/home_page.dart @@ -577,6 +577,7 @@ class _HomePageState extends State { key: ValueKey('${code.hashCode}_$newIndex'), code, isCompactMode: isCompactMode, + sortKey: _codeSortKey, ), ); }), From 0814f048a0306036ac3d01625d987f1a998c9232 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 4 Dec 2024 14:22:12 +0530 Subject: [PATCH 4/5] [auth] Lint fix --- auth/lib/ui/home_page.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/auth/lib/ui/home_page.dart b/auth/lib/ui/home_page.dart index 09520eff47..937d05bc28 100644 --- a/auth/lib/ui/home_page.dart +++ b/auth/lib/ui/home_page.dart @@ -255,7 +255,8 @@ class _HomePageState extends State { break; case CodeSortKey.recentlyUsed: codes.sort( - (a, b) => b.display.lastUsedAt.compareTo(a.display.lastUsedAt)); + (a, b) => b.display.lastUsedAt.compareTo(a.display.lastUsedAt), + ); break; case CodeSortKey.manual: default: From 529666545190a883edafec765ca8a768a37ff634 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 4 Dec 2024 14:45:52 +0530 Subject: [PATCH 5/5] [auth] Handle null value --- auth/lib/services/user_service.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/auth/lib/services/user_service.dart b/auth/lib/services/user_service.dart index f123fdf1b0..194b22bf45 100644 --- a/auth/lib/services/user_service.dart +++ b/auth/lib/services/user_service.dart @@ -380,7 +380,8 @@ class UserService { Widget page; final String passkeySessionID = response.data["passkeySessionID"]; String twoFASessionID = response.data["twoFactorSessionID"]; - if (twoFASessionID.isEmpty) { + if (twoFASessionID.isEmpty && + response.data["twoFactorSessionIDV2"] != null) { twoFASessionID = response.data["twoFactorSessionIDV2"]; } if (passkeySessionID.isNotEmpty) { @@ -692,7 +693,8 @@ class UserService { Widget? page; final String passkeySessionID = response.data["passkeySessionID"]; String twoFASessionID = response.data["twoFactorSessionID"]; - if (twoFASessionID.isEmpty) { + if (twoFASessionID.isEmpty && + response.data["twoFactorSessionIDV2"] != null) { twoFASessionID = response.data["twoFactorSessionIDV2"]; } Configuration.instance.setVolatilePassword(userPassword);