[auth] Remove duplicate code (#4383)

This commit is contained in:
Aman Raj Singh Mourya 2024-12-14 08:32:39 +05:30 committed by GitHub
commit ca02c20d09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 355 additions and 1 deletions

View File

@ -491,5 +491,12 @@
"appLockNotEnabled": "App lock not enabled",
"appLockNotEnabledDescription": "Please enable app lock from Security > App Lock",
"authToViewPasskey": "Please authenticate to view passkey",
"appLockOfflineModeWarning": "You have chosen to proceed without backups. If you forget your applock, you will be locked out from accessing your data."
"appLockOfflineModeWarning": "You have chosen to proceed without backups. If you forget your applock, you will be locked out from accessing your data.",
"duplicateCodes": "Duplicate codes",
"noDuplicates": "✨ No duplicates",
"youveNoDuplicateCodesThatCanBeCleared": "You've no duplicate codes that can be cleared",
"deduplicateCodes": "Deduplicate codes",
"deselectAll": "Deselect all",
"selectAll": "Select all",
"deleteDuplicates": "Delete duplicates"
}

View File

@ -0,0 +1,56 @@
import 'package:ente_auth/models/code.dart';
import 'package:ente_auth/store/code_store.dart';
import 'package:logging/logging.dart';
class DeduplicationService {
final _logger = Logger("DeduplicationService");
DeduplicationService._privateConstructor();
static final DeduplicationService instance =
DeduplicationService._privateConstructor();
Future<List<DuplicateCodes>> getDuplicateCodes() async {
try {
final List<DuplicateCodes> result = await _getDuplicateCodes();
return result;
} catch (e, s) {
_logger.severe("failed to get dedupeCode", e, s);
rethrow;
}
}
Future<List<DuplicateCodes>> _getDuplicateCodes() async {
final codes = await CodeStore.instance.getAllCodes();
final List<DuplicateCodes> duplicateCodes = [];
Map<String, List<Code>> uniqueCodes = {};
for (final code in codes) {
if (code.hasError || code.isTrashed) continue;
final uniqueKey = "${code.secret}_${code.issuer}_${code.account}";
if (uniqueCodes.containsKey(uniqueKey)) {
uniqueCodes[uniqueKey]!.add(code);
} else {
uniqueCodes[uniqueKey] = [code];
}
}
for (final key in uniqueCodes.keys) {
if (uniqueCodes[key]!.length > 1) {
duplicateCodes.add(DuplicateCodes(key, uniqueCodes[key]!));
}
}
return duplicateCodes;
}
}
class DuplicateCodes {
String hash;
final List<Code> codes;
DuplicateCodes(
this.hash,
this.codes,
);
}

View File

@ -1,11 +1,16 @@
import 'dart:async';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/services/deduplication_service.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
import 'package:ente_auth/ui/components/menu_item_widget.dart';
import 'package:ente_auth/ui/settings/common_settings.dart';
import 'package:ente_auth/ui/settings/data/duplicate_code_page.dart';
import 'package:ente_auth/ui/settings/data/export_widget.dart';
import 'package:ente_auth/ui/settings/data/import_page.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/navigation_util.dart';
import 'package:flutter/material.dart';
@ -53,6 +58,33 @@ class DataSectionWidget extends StatelessWidget {
},
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: l10n.duplicateCodes,
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
final List<DuplicateCodes> duplicateCodes =
await DeduplicationService.instance.getDuplicateCodes();
if (duplicateCodes.isEmpty) {
unawaited(
showErrorDialog(
context,
l10n.noDuplicates,
l10n.youveNoDuplicateCodesThatCanBeCleared,
),
);
return;
}
await routeToPage(
context,
DuplicateCodePage(duplicateCodes: duplicateCodes),
);
},
),
sectionOptionSpacing,
]);
return Column(
children: children,

View File

@ -0,0 +1,259 @@
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/code.dart';
import 'package:ente_auth/services/deduplication_service.dart';
import 'package:ente_auth/services/local_authentication_service.dart';
import 'package:ente_auth/store/code_store.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/code_widget.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:logging/logging.dart';
class DuplicateCodePage extends StatefulWidget {
final List<DuplicateCodes> duplicateCodes;
const DuplicateCodePage({
super.key,
required this.duplicateCodes,
});
@override
State<DuplicateCodePage> createState() => _DuplicateCodePageState();
}
class _DuplicateCodePageState extends State<DuplicateCodePage> {
final Logger _logger = Logger("DuplicateCodePage");
late List<DuplicateCodes> _duplicateCodes;
final Set<int> selectedGrids = <int>{};
@override
void initState() {
_duplicateCodes = widget.duplicateCodes;
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.deduplicateCodes),
elevation: 0,
),
body: _getBody(),
);
}
Widget _getBody() {
final l10n = context.l10n;
return SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
GestureDetector(
onTap: () {
if (selectedGrids.length == _duplicateCodes.length) {
_removeAllGrids();
} else {
_selectAllGrids();
}
setState(() {});
},
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
selectedGrids.length == _duplicateCodes.length
? l10n.deselectAll
: l10n.selectAll,
style:
Theme.of(context).textTheme.titleMedium!.copyWith(
fontSize: 14,
color: Theme.of(context)
.iconTheme
.color!
.withOpacity(0.7),
),
),
const Padding(padding: EdgeInsets.only(left: 4)),
selectedGrids.length == _duplicateCodes.length
? const Icon(
Icons.check_circle,
size: 24,
)
: Icon(
Icons.check_circle_outlined,
color: getEnteColorScheme(context).strokeMuted,
size: 24,
),
],
),
),
],
),
),
),
const SizedBox(height: 8),
Expanded(
child: ListView.builder(
itemCount: _duplicateCodes.length,
shrinkWrap: true,
itemBuilder: (context, index) {
final List<Code> codes = _duplicateCodes[index].codes;
return _getGridView(
codes,
index,
);
},
),
),
selectedGrids.isEmpty ? const SizedBox.shrink() : _getDeleteButton(),
],
),
);
}
Widget _getGridView(List<Code> code, int itemIndex) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
child: GestureDetector(
onTap: () {
if (selectedGrids.contains(itemIndex)) {
selectedGrids.remove(itemIndex);
} else {
selectedGrids.add(itemIndex);
}
setState(() {});
},
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${code[0].issuer}, ${code.length} items",
),
!selectedGrids.contains(itemIndex)
? Icon(
Icons.check_circle_outlined,
color: getEnteColorScheme(context).strokeMuted,
size: 24,
)
: const Icon(
Icons.check_circle,
size: 24,
),
],
),
),
),
AlignedGridView.count(
crossAxisCount: (MediaQuery.sizeOf(context).width ~/ 400)
.clamp(1, double.infinity)
.toInt(),
padding: const EdgeInsets.only(bottom: 40),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return CodeWidget(
key: ValueKey('${code.hashCode}_$index'),
code[index],
isCompactMode: false,
);
},
itemCount: code.length,
),
],
);
}
Widget _getDeleteButton() {
int selectedItemsCount = 0;
for (int idx = 0; idx < _duplicateCodes.length; idx++) {
if (selectedGrids.contains(idx)) {
selectedItemsCount += _duplicateCodes[idx].codes.length - 1;
}
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 20),
child: SizedBox(
width: 400,
child: OutlinedButton(
onPressed: () async {
await deleteDuplicates(selectedItemsCount);
},
child: Text(
"Delete $selectedItemsCount items",
),
),
),
);
}
void _selectAllGrids() {
selectedGrids.clear();
for (int idx = 0; idx < _duplicateCodes.length; idx++) {
selectedGrids.add(idx);
}
}
void _removeAllGrids() {
selectedGrids.clear();
}
Future<void> deleteDuplicates(int itemCount) async {
bool isAuthSuccessful =
await LocalAuthenticationService.instance.requestLocalAuthentication(
context,
context.l10n.deleteCodeAuthMessage,
);
if (!isAuthSuccessful) {
return;
}
FocusScope.of(context).requestFocus();
final l10n = context.l10n;
final String message = "Are you sure you want to trash $itemCount items?";
await showChoiceActionSheet(
context,
title: l10n.deleteDuplicates,
body: message,
firstButtonLabel: l10n.trash,
isCritical: true,
firstButtonOnTap: () async {
try {
for (int idx = 0; idx < _duplicateCodes.length; idx++) {
if (selectedGrids.contains(idx)) {
final List<Code> codes = _duplicateCodes[idx].codes;
for (int i = 1; i < codes.length; i++) {
final display = codes[i].display;
final Code code = codes[i].copyWith(
display: display.copyWith(trashed: true),
);
await CodeStore.instance.addCode(code);
}
}
}
Navigator.of(context).pop();
} catch (e) {
_logger.severe('Failed to trash duplicate codes: ${e.toString()}');
showGenericErrorDialog(context: context, error: e).ignore();
}
},
);
}
}