[auth] Fix sorting + add option to sort by recently and most frequently used (#4304)

## Description

## Tests
This commit is contained in:
Neeraj Gupta
2024-12-04 15:03:23 +05:30
committed by GitHub
8 changed files with 171 additions and 82 deletions

View File

@@ -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!",

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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(

View File

@@ -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,

View File

@@ -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,
),
);
}),

View File

@@ -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(() {});
}
}

View 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),
);
}
}