[auth] Fix sorting + add option to sort by recently and most frequently used (#4304)
## Description ## Tests
This commit is contained in:
@@ -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!",
|
||||
|
||||
@@ -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,
|
||||
recentlyUsed,
|
||||
manual,
|
||||
}
|
||||
|
||||
class PreferenceService {
|
||||
PreferenceService._privateConstructor();
|
||||
static final PreferenceService instance =
|
||||
@@ -28,6 +36,15 @@ class PreferenceService {
|
||||
}
|
||||
}
|
||||
|
||||
CodeSortKey codeSortKey() {
|
||||
return CodeSortKey
|
||||
.values[_prefs.getInt("codeSortKey") ?? CodeSortKey.manual.index];
|
||||
}
|
||||
|
||||
Future<void> setCodeSortKey(CodeSortKey key) async {
|
||||
await _prefs.setInt("codeSortKey", key.index);
|
||||
}
|
||||
|
||||
Future<void> setHasShownCoachMark(bool value) {
|
||||
return _prefs.setBool(kHasShownCoachMarkKey, value);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<CodeWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
void _copyCurrentOTPToClipboard() async {
|
||||
void _copyCurrentOTPToClipboard() {
|
||||
_copyToClipboard(
|
||||
_getCurrentOTP(),
|
||||
confirmationMessage: context.l10n.copiedToClipboard,
|
||||
);
|
||||
_udateCodeMetadata().ignore();
|
||||
}
|
||||
|
||||
void _copyNextToClipboard() {
|
||||
@@ -466,6 +469,26 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
_getNextTotp(),
|
||||
confirmationMessage: context.l10n.copiedNextToClipboard,
|
||||
);
|
||||
_udateCodeMetadata().ignore();
|
||||
}
|
||||
|
||||
Future<void> _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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
@@ -82,11 +83,13 @@ class _HomePageState extends State<HomePage> {
|
||||
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<CodesUpdatedEvent>().listen((event) {
|
||||
_loadCodes();
|
||||
@@ -95,6 +98,7 @@ class _HomePageState extends State<HomePage> {
|
||||
Bus.instance.on<TriggerLogoutEvent>().listen((event) async {
|
||||
await autoLogoutAlert(context);
|
||||
});
|
||||
|
||||
_initDeepLinks();
|
||||
Future.delayed(
|
||||
const Duration(seconds: 1),
|
||||
@@ -219,8 +223,7 @@ class _HomePageState extends State<HomePage> {
|
||||
[];
|
||||
}
|
||||
|
||||
_filteredCodes
|
||||
.sort((a, b) => a.display.position.compareTo(b.display.position));
|
||||
sortFilteredCodes(_filteredCodes, _codeSortKey);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
@@ -239,6 +242,29 @@ class _HomePageState extends State<HomePage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void sortFilteredCodes(List<Code> 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<void> _redirectToScannerPage() async {
|
||||
final Code? code = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
@@ -358,11 +384,20 @@ class _HomePageState extends State<HomePage> {
|
||||
),
|
||||
centerTitle: PlatformUtil.isDesktop() ? false : true,
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
tooltip: l10n.edit,
|
||||
onPressed: () {
|
||||
navigateToReorderPage(_allCodes!);
|
||||
SortCodeMenuWidget(
|
||||
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();
|
||||
}
|
||||
},
|
||||
),
|
||||
PlatformUtil.isDesktop()
|
||||
@@ -543,6 +578,7 @@ class _HomePageState extends State<HomePage> {
|
||||
key: ValueKey('${code.hashCode}_$newIndex'),
|
||||
code,
|
||||
isCompactMode: isCompactMode,
|
||||
sortKey: _codeSortKey,
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -34,7 +34,7 @@ class _ReorderCodesPageState extends State<ReorderCodesPage> {
|
||||
},
|
||||
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<ReorderCodesPage> {
|
||||
}
|
||||
},
|
||||
),
|
||||
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<ReorderCodesPage> {
|
||||
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(() {});
|
||||
}
|
||||
}
|
||||
|
||||
78
auth/lib/ui/sort_option_menu.dart
Normal file
78
auth/lib/ui/sort_option_menu.dart
Normal file
@@ -0,0 +1,78 @@
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/services/preference_service.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.recentlyUsed:
|
||||
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<int>(
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user