fix: logics and ui (button, error code)
This commit is contained in:
parent
8370d2a9f1
commit
838983ec61
16 changed files with 796 additions and 693 deletions
11
auth/assets/svg/button-tint.svg
Normal file
11
auth/assets/svg/button-tint.svg
Normal file
|
@ -0,0 +1,11 @@
|
|||
<svg width="286" height="44" viewBox="0 0 286 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="286" height="44" fill="#531DAB"/>
|
||||
<rect width="286" height="44" fill="url(#paint0_linear_25656_80129)" style="mix-blend-mode:soft-light"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_25656_80129" x1="286.174" y1="43.5575" x2="272.305" y2="-42.2227" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.00134129" stop-color="#B37FEB"/>
|
||||
<stop offset="0.15" stop-color="#D2AEF5" stop-opacity="0"/>
|
||||
<stop offset="0.829143" stop-color="#EFDBFF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 578 B |
|
@ -17,6 +17,7 @@ import 'package:ente_auth/services/update_service.dart';
|
|||
import 'package:ente_auth/services/user_remote_flag_service.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/services/window_listener_service.dart';
|
||||
import 'package:ente_auth/store/code_display_store.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/ui/tools/app_lock.dart';
|
||||
import 'package:ente_auth/ui/tools/lock_screen.dart';
|
||||
|
@ -145,6 +146,7 @@ Future<void> _init(bool bool, {String? via}) async {
|
|||
|
||||
await PreferenceService.instance.init();
|
||||
await CodeStore.instance.init();
|
||||
await CodeDisplayStore.instance.init();
|
||||
await Configuration.instance.init();
|
||||
await Network.instance.init();
|
||||
await UserService.instance.init();
|
||||
|
|
|
@ -1,26 +1,13 @@
|
|||
import 'package:ente_auth/models/code.dart';
|
||||
|
||||
class CodeState {
|
||||
final Code? code;
|
||||
final String? error;
|
||||
class AllCodes {
|
||||
final List<Code> codes;
|
||||
final AllCodesState state;
|
||||
|
||||
CodeState({
|
||||
required this.code,
|
||||
required this.error,
|
||||
}) : assert(code != null || error != null);
|
||||
AllCodes({required this.codes, required this.state});
|
||||
}
|
||||
|
||||
class Codes {
|
||||
final List<CodeState> allCodes;
|
||||
final List<String> tags;
|
||||
|
||||
Codes({
|
||||
required this.allCodes,
|
||||
required this.tags,
|
||||
});
|
||||
|
||||
List<Code> get validCodes => allCodes
|
||||
.where((element) => element.code != null)
|
||||
.map((e) => e.code!)
|
||||
.toList();
|
||||
enum AllCodesState {
|
||||
value,
|
||||
error,
|
||||
}
|
||||
|
|
10
auth/lib/onboarding/model/tag_enums.dart
Normal file
10
auth/lib/onboarding/model/tag_enums.dart
Normal file
|
@ -0,0 +1,10 @@
|
|||
enum TagChipState {
|
||||
selected,
|
||||
unselected,
|
||||
}
|
||||
|
||||
enum TagChipAction {
|
||||
none,
|
||||
menu,
|
||||
check,
|
||||
}
|
27
auth/lib/onboarding/view/common/add_chip.dart
Normal file
27
auth/lib/onboarding/view/common/add_chip.dart
Normal file
|
@ -0,0 +1,27 @@
|
|||
import "package:flutter/material.dart";
|
||||
|
||||
class AddChip extends StatelessWidget {
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const AddChip({
|
||||
super.key,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Icon(
|
||||
Icons.add_circle_outline,
|
||||
size: 30,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? const Color(0xFF9610D6)
|
||||
: const Color(0xFF8232E1),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
77
auth/lib/onboarding/view/common/add_tag.dart
Normal file
77
auth/lib/onboarding/view/common/add_tag.dart
Normal file
|
@ -0,0 +1,77 @@
|
|||
import "package:ente_auth/l10n/l10n.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class AddTagDialog extends StatefulWidget {
|
||||
const AddTagDialog({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final void Function(String) onTap;
|
||||
|
||||
@override
|
||||
State<AddTagDialog> createState() => _AddTagDialogState();
|
||||
}
|
||||
|
||||
class _AddTagDialogState extends State<AddTagDialog> {
|
||||
String _tag = "";
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return AlertDialog(
|
||||
title: Text(l10n.createNewTag),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.tag,
|
||||
hintStyle: const TextStyle(
|
||||
color: Colors.white30,
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_tag = value;
|
||||
});
|
||||
},
|
||||
autocorrect: false,
|
||||
initialValue: _tag,
|
||||
autofocus: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(
|
||||
l10n.cancel,
|
||||
style: const TextStyle(
|
||||
color: Colors.redAccent,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
l10n.create,
|
||||
style: const TextStyle(
|
||||
color: Colors.purple,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
if (_tag.trim().isEmpty) return;
|
||||
|
||||
widget.onTap(_tag);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
89
auth/lib/onboarding/view/common/edit_tag.dart
Normal file
89
auth/lib/onboarding/view/common/edit_tag.dart
Normal file
|
@ -0,0 +1,89 @@
|
|||
import "package:ente_auth/l10n/l10n.dart";
|
||||
import 'package:ente_auth/store/code_display_store.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EditTagDialog extends StatefulWidget {
|
||||
const EditTagDialog({
|
||||
super.key,
|
||||
required this.tag,
|
||||
});
|
||||
|
||||
final String tag;
|
||||
|
||||
@override
|
||||
State<EditTagDialog> createState() => _EditTagDialogState();
|
||||
}
|
||||
|
||||
class _EditTagDialogState extends State<EditTagDialog> {
|
||||
late String _tag = widget.tag;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return AlertDialog(
|
||||
title: Text(l10n.editTag),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.tag,
|
||||
hintStyle: const TextStyle(
|
||||
color: Colors.white30,
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_tag = value;
|
||||
});
|
||||
},
|
||||
autocorrect: false,
|
||||
initialValue: _tag,
|
||||
autofocus: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(
|
||||
l10n.cancel,
|
||||
style: const TextStyle(
|
||||
color: Colors.redAccent,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
l10n.saveAction,
|
||||
style: const TextStyle(
|
||||
color: Colors.purple,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
if (_tag.trim().isEmpty) return;
|
||||
|
||||
final dialog = createProgressDialog(
|
||||
context,
|
||||
context.l10n.pleaseWait,
|
||||
);
|
||||
await dialog.show();
|
||||
|
||||
await CodeDisplayStore.instance.editTag(widget.tag, _tag);
|
||||
|
||||
await dialog.hide();
|
||||
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
145
auth/lib/onboarding/view/common/tag_chip.dart
Normal file
145
auth/lib/onboarding/view/common/tag_chip.dart
Normal file
|
@ -0,0 +1,145 @@
|
|||
import "package:ente_auth/l10n/l10n.dart";
|
||||
import "package:ente_auth/onboarding/model/tag_enums.dart";
|
||||
import "package:ente_auth/store/code_display_store.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:gradient_borders/box_borders/gradient_box_border.dart";
|
||||
|
||||
class TagChip extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback? onTap;
|
||||
final TagChipState state;
|
||||
final TagChipAction action;
|
||||
|
||||
const TagChip({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.state = TagChipState.unselected,
|
||||
this.action = TagChipAction.none,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: state == TagChipState.selected
|
||||
? const Color(0xFF722ED1)
|
||||
: Theme.of(context).brightness == Brightness.dark
|
||||
? const Color(0xFF1C0F22)
|
||||
: const Color(0xFFFCF5FF),
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
border: GradientBoxBorder(
|
||||
gradient: LinearGradient(
|
||||
colors: state == TagChipState.selected
|
||||
? [
|
||||
const Color(0xFFB37FEB),
|
||||
const Color(0xFFAE40E3).withOpacity(
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? .53
|
||||
: 1,
|
||||
),
|
||||
]
|
||||
: [
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? const Color(0xFFAD00FF)
|
||||
: const Color(0xFFAD00FF).withOpacity(0.2),
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? const Color(0xFFA269BD).withOpacity(0.53)
|
||||
: const Color(0xFF8609C2).withOpacity(0.2),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16)
|
||||
.copyWith(right: 0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: state == TagChipState.selected ||
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white
|
||||
: const Color(0xFF8232E1),
|
||||
),
|
||||
),
|
||||
if (state == TagChipState.selected &&
|
||||
action == TagChipAction.check) ...[
|
||||
const SizedBox(width: 16),
|
||||
const Icon(
|
||||
Icons.check,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
] else if (state == TagChipState.selected &&
|
||||
action == TagChipAction.menu) ...[
|
||||
SizedBox(
|
||||
width: 48,
|
||||
child: PopupMenuButton<int>(
|
||||
iconSize: 16,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
surfaceTintColor: Theme.of(context).cardColor,
|
||||
iconColor: Colors.white,
|
||||
initialValue: -1,
|
||||
onSelected: (value) {
|
||||
if (value == 0) {
|
||||
CodeDisplayStore.instance.showEditDialog(context, label);
|
||||
} else if (value == 1) {
|
||||
CodeDisplayStore.instance
|
||||
.showDeleteTagDialog(context, label);
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.edit_outlined, size: 16),
|
||||
const SizedBox(width: 12),
|
||||
Text(context.l10n.edit),
|
||||
],
|
||||
),
|
||||
value: 0,
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.delete_outline,
|
||||
size: 16,
|
||||
color: Color(0xFFF53434),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
context.l10n.delete,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFF53434),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
value: 1,
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,19 +1,25 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/events/codes_updated_event.dart';
|
||||
import "package:ente_auth/l10n/l10n.dart";
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/models/code_display.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/onboarding/model/tag_enums.dart';
|
||||
import 'package:ente_auth/onboarding/view/common/add_chip.dart';
|
||||
import 'package:ente_auth/onboarding/view/common/add_tag.dart';
|
||||
import 'package:ente_auth/onboarding/view/common/tag_chip.dart';
|
||||
import 'package:ente_auth/store/code_display_store.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_result.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/totp_util.dart';
|
||||
import "package:flutter/material.dart";
|
||||
import 'package:gradient_borders/box_borders/gradient_box_border.dart';
|
||||
|
||||
class SetupEnterSecretKeyPage extends StatefulWidget {
|
||||
final Code? code;
|
||||
final List<String> tags;
|
||||
|
||||
SetupEnterSecretKeyPage({this.code, super.key, required this.tags});
|
||||
SetupEnterSecretKeyPage({this.code, super.key});
|
||||
|
||||
@override
|
||||
State<SetupEnterSecretKeyPage> createState() =>
|
||||
|
@ -26,7 +32,8 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
|||
late TextEditingController _secretController;
|
||||
late bool _secretKeyObscured;
|
||||
late List<String> tags = [...?widget.code?.display.tags];
|
||||
late List<String> allTags = [...widget.tags];
|
||||
List<String> allTags = [];
|
||||
StreamSubscription<CodesUpdatedEvent>? _streamSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -41,9 +48,24 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
|||
text: widget.code?.secret,
|
||||
);
|
||||
_secretKeyObscured = widget.code != null;
|
||||
_loadTags();
|
||||
_streamSubscription = Bus.instance.on<CodesUpdatedEvent>().listen((event) {
|
||||
_loadTags();
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_streamSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadTags() async {
|
||||
allTags = await CodeDisplayStore.instance.getAllTags();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
|
@ -261,403 +283,3 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AddChip extends StatelessWidget {
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const AddChip({
|
||||
super.key,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Icon(
|
||||
Icons.add_circle_outline,
|
||||
size: 30,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? const Color(0xFF9610D6)
|
||||
: const Color(0xFF8232E1),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum TagChipState {
|
||||
selected,
|
||||
unselected,
|
||||
}
|
||||
|
||||
enum TagChipAction {
|
||||
none,
|
||||
menu,
|
||||
check,
|
||||
}
|
||||
|
||||
class TagChip extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback? onTap;
|
||||
final TagChipState state;
|
||||
final TagChipAction action;
|
||||
|
||||
const TagChip({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.state = TagChipState.unselected,
|
||||
this.action = TagChipAction.none,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: state == TagChipState.selected
|
||||
? const Color(0xFF722ED1)
|
||||
: Theme.of(context).brightness == Brightness.dark
|
||||
? const Color(0xFF1C0F22)
|
||||
: const Color(0xFFFCF5FF),
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
border: GradientBoxBorder(
|
||||
gradient: LinearGradient(
|
||||
colors: state == TagChipState.selected
|
||||
? [
|
||||
const Color(0xFFB37FEB),
|
||||
const Color(0xFFAE40E3).withOpacity(
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? .53
|
||||
: 1,
|
||||
),
|
||||
]
|
||||
: [
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? const Color(0xFFAD00FF)
|
||||
: const Color(0xFFAD00FF).withOpacity(0.2),
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? const Color(0xFFA269BD).withOpacity(0.53)
|
||||
: const Color(0xFF8609C2).withOpacity(0.2),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16)
|
||||
.copyWith(right: 0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: state == TagChipState.selected ||
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white
|
||||
: const Color(0xFF8232E1),
|
||||
),
|
||||
),
|
||||
if (state == TagChipState.selected &&
|
||||
action == TagChipAction.check) ...[
|
||||
const SizedBox(width: 16),
|
||||
const Icon(
|
||||
Icons.check,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
] else if (state == TagChipState.selected &&
|
||||
action == TagChipAction.menu) ...[
|
||||
SizedBox(
|
||||
width: 48,
|
||||
child: PopupMenuButton<int>(
|
||||
iconSize: 16,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
surfaceTintColor: Theme.of(context).cardColor,
|
||||
iconColor: Colors.white,
|
||||
initialValue: -1,
|
||||
onSelected: (value) {
|
||||
if (value == 0) {
|
||||
showEditDialog(context, label);
|
||||
} else if (value == 1) {
|
||||
showDeleteTagDialog(context, label);
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.edit_outlined, size: 16),
|
||||
const SizedBox(width: 12),
|
||||
Text(context.l10n.edit),
|
||||
],
|
||||
),
|
||||
value: 0,
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.delete_outline,
|
||||
size: 16,
|
||||
color: Color(0xFFF53434),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
context.l10n.delete,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFF53434),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
value: 1,
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AddTagDialog extends StatefulWidget {
|
||||
const AddTagDialog({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final void Function(String) onTap;
|
||||
|
||||
@override
|
||||
State<AddTagDialog> createState() => _AddTagDialogState();
|
||||
}
|
||||
|
||||
class _AddTagDialogState extends State<AddTagDialog> {
|
||||
String _tag = "";
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return AlertDialog(
|
||||
title: Text(l10n.createNewTag),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.tag,
|
||||
hintStyle: const TextStyle(
|
||||
color: Colors.white30,
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_tag = value;
|
||||
});
|
||||
},
|
||||
autocorrect: false,
|
||||
initialValue: _tag,
|
||||
autofocus: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(
|
||||
l10n.cancel,
|
||||
style: const TextStyle(
|
||||
color: Colors.redAccent,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
l10n.create,
|
||||
style: const TextStyle(
|
||||
color: Colors.purple,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
if (_tag.trim().isEmpty) return;
|
||||
|
||||
widget.onTap(_tag);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EditTagDialog extends StatefulWidget {
|
||||
const EditTagDialog({
|
||||
super.key,
|
||||
required this.tag,
|
||||
});
|
||||
|
||||
final String tag;
|
||||
|
||||
@override
|
||||
State<EditTagDialog> createState() => _EditTagDialogState();
|
||||
}
|
||||
|
||||
class _EditTagDialogState extends State<EditTagDialog> {
|
||||
late String _tag = widget.tag;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return AlertDialog(
|
||||
title: Text(l10n.editTag),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.tag,
|
||||
hintStyle: const TextStyle(
|
||||
color: Colors.white30,
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_tag = value;
|
||||
});
|
||||
},
|
||||
autocorrect: false,
|
||||
initialValue: _tag,
|
||||
autofocus: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(
|
||||
l10n.cancel,
|
||||
style: const TextStyle(
|
||||
color: Colors.redAccent,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
l10n.saveAction,
|
||||
style: const TextStyle(
|
||||
color: Colors.purple,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
if (_tag.trim().isEmpty) return;
|
||||
|
||||
final dialog = createProgressDialog(
|
||||
context,
|
||||
context.l10n.pleaseWait,
|
||||
);
|
||||
await dialog.show();
|
||||
|
||||
// traverse through all the codes and edit this tag's value
|
||||
final relevantCodes = (await CodeStore.instance.getAllCodes())
|
||||
.validCodes
|
||||
.where((element) => element.display.tags.contains(widget.tag));
|
||||
|
||||
final tasks = <Future>[];
|
||||
|
||||
for (final code in relevantCodes) {
|
||||
final tags = code.display.tags;
|
||||
tags.remove(widget.tag);
|
||||
tags.add(_tag);
|
||||
tasks.add(
|
||||
CodeStore.instance.addCode(
|
||||
code.copyWith(
|
||||
display: code.display.copyWith(tags: tags),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await Future.wait(tasks);
|
||||
await dialog.hide();
|
||||
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showDeleteTagDialog(BuildContext context, String tag) async {
|
||||
FocusScope.of(context).requestFocus();
|
||||
final l10n = context.l10n;
|
||||
await showChoiceActionSheet(
|
||||
context,
|
||||
title: l10n.deleteTagTitle,
|
||||
body: l10n.deleteTagMessage,
|
||||
firstButtonLabel: l10n.delete,
|
||||
isCritical: true,
|
||||
firstButtonOnTap: () async {
|
||||
// traverse through all the codes and edit this tag's value
|
||||
final relevantCodes = (await CodeStore.instance.getAllCodes())
|
||||
.validCodes
|
||||
.where((element) => element.display.tags.contains(tag));
|
||||
|
||||
final tasks = <Future>[];
|
||||
|
||||
for (final code in relevantCodes) {
|
||||
final tags = code.display.tags;
|
||||
tags.remove(tag);
|
||||
tasks.add(
|
||||
CodeStore.instance.addCode(
|
||||
code.copyWith(
|
||||
display: code.display.copyWith(tags: tags),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await Future.wait(tasks);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showEditDialog(BuildContext context, String tag) async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return EditTagDialog(tag: tag);
|
||||
},
|
||||
barrierColor: Colors.black.withOpacity(0.85),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
}
|
||||
|
|
123
auth/lib/store/code_display_store.dart
Normal file
123
auth/lib/store/code_display_store.dart
Normal file
|
@ -0,0 +1,123 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/authenticator/entity_result.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/onboarding/view/common/edit_tag.dart';
|
||||
import 'package:ente_auth/services/authenticator_service.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class CodeDisplayStore {
|
||||
static final CodeDisplayStore instance =
|
||||
CodeDisplayStore._privateConstructor();
|
||||
|
||||
CodeDisplayStore._privateConstructor();
|
||||
|
||||
late CodeStore _codeStore;
|
||||
|
||||
late AuthenticatorService _authenticatorService;
|
||||
final _logger = Logger("CodeDisplayStore");
|
||||
|
||||
Future<void> init() async {
|
||||
_authenticatorService = AuthenticatorService.instance;
|
||||
_codeStore = CodeStore.instance;
|
||||
}
|
||||
|
||||
Future<List<String>> getAllTags({AccountMode? accountMode}) async {
|
||||
final mode = accountMode ?? _authenticatorService.getAccountMode();
|
||||
final List<EntityResult> entities =
|
||||
await _authenticatorService.getEntities(mode);
|
||||
List<String> tags = [];
|
||||
|
||||
for (final entity in entities) {
|
||||
try {
|
||||
final decodeJson = jsonDecode(entity.rawData);
|
||||
|
||||
late Code code;
|
||||
if (decodeJson is String && decodeJson.startsWith('otpauth://')) {
|
||||
code = Code.fromOTPAuthUrl(decodeJson);
|
||||
} else {
|
||||
code = Code.fromExportJson(decodeJson);
|
||||
}
|
||||
tags.addAll(code.display.tags);
|
||||
} catch (e) {
|
||||
_logger.severe("Could not parse code", e);
|
||||
}
|
||||
}
|
||||
tags = tags.toSet().toList();
|
||||
return tags;
|
||||
}
|
||||
|
||||
Future<void> showDeleteTagDialog(BuildContext context, String tag) async {
|
||||
FocusScope.of(context).requestFocus();
|
||||
final l10n = context.l10n;
|
||||
|
||||
await showChoiceActionSheet(
|
||||
context,
|
||||
title: l10n.deleteTagTitle,
|
||||
body: l10n.deleteTagMessage,
|
||||
firstButtonLabel: l10n.delete,
|
||||
isCritical: true,
|
||||
firstButtonOnTap: () async {
|
||||
// traverse through all the codes and edit this tag's value
|
||||
final relevantCodes = (await CodeStore.instance.getAllCodes())
|
||||
.codes
|
||||
.where((element) => element.display.tags.contains(tag));
|
||||
|
||||
final tasks = <Future>[];
|
||||
|
||||
for (final code in relevantCodes) {
|
||||
final tags = code.display.tags;
|
||||
tags.remove(tag);
|
||||
tasks.add(
|
||||
_codeStore.addCode(
|
||||
code.copyWith(
|
||||
display: code.display.copyWith(tags: tags),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await Future.wait(tasks);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showEditDialog(BuildContext context, String tag) async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return EditTagDialog(tag: tag);
|
||||
},
|
||||
barrierColor: Colors.black.withOpacity(0.85),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> editTag(String previousTag, String updatedTag) async {
|
||||
// traverse through all the codes and edit this tag's value
|
||||
final relevantCodes = (await CodeStore.instance.getAllCodes())
|
||||
.codes
|
||||
.where((element) => element.display.tags.contains(previousTag));
|
||||
|
||||
final tasks = <Future>[];
|
||||
|
||||
for (final code in relevantCodes) {
|
||||
final tags = code.display.tags;
|
||||
tags.remove(previousTag);
|
||||
tags.add(updatedTag);
|
||||
tasks.add(
|
||||
CodeStore.instance.addCode(
|
||||
code.copyWith(
|
||||
display: code.display.copyWith(tags: tags),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await Future.wait(tasks);
|
||||
}
|
||||
}
|
|
@ -23,12 +23,13 @@ class CodeStore {
|
|||
_authenticatorService = AuthenticatorService.instance;
|
||||
}
|
||||
|
||||
Future<Codes> getAllCodes({AccountMode? accountMode}) async {
|
||||
Future<AllCodes> getAllCodes({AccountMode? accountMode}) async {
|
||||
final mode = accountMode ?? _authenticatorService.getAccountMode();
|
||||
final List<EntityResult> entities =
|
||||
await _authenticatorService.getEntities(mode);
|
||||
final List<CodeState> codes = [];
|
||||
List<String> tags = [];
|
||||
final List<Code> codes = [];
|
||||
bool hasError = false;
|
||||
|
||||
for (final entity in entities) {
|
||||
try {
|
||||
final decodeJson = jsonDecode(entity.rawData);
|
||||
|
@ -41,23 +42,15 @@ class CodeStore {
|
|||
}
|
||||
code.generatedID = entity.generatedID;
|
||||
code.hasSynced = entity.hasSynced;
|
||||
codes.add(CodeState(code: code, error: null));
|
||||
tags.addAll(code.display.tags);
|
||||
codes.add(code);
|
||||
} catch (e) {
|
||||
codes.add(CodeState(code: null, error: e.toString()));
|
||||
hasError = true;
|
||||
_logger.severe("Could not parse code", e);
|
||||
}
|
||||
}
|
||||
|
||||
// sort codes by issuer,account
|
||||
codes.sort((a, b) {
|
||||
if (a.code == null && b.code == null) return 0;
|
||||
if (a.code == null) return 1;
|
||||
if (b.code == null) return -1;
|
||||
|
||||
final firstCode = a.code!;
|
||||
final secondCode = b.code!;
|
||||
|
||||
codes.sort((firstCode, secondCode) {
|
||||
if (secondCode.isPinned && !firstCode.isPinned) return 1;
|
||||
if (!secondCode.isPinned && firstCode.isPinned) return -1;
|
||||
|
||||
|
@ -71,8 +64,10 @@ class CodeStore {
|
|||
secondCode.account,
|
||||
);
|
||||
});
|
||||
tags = tags.toSet().toList();
|
||||
return Codes(allCodes: codes, tags: tags);
|
||||
return AllCodes(
|
||||
codes: codes,
|
||||
state: hasError ? AllCodesState.error : AllCodesState.value,
|
||||
);
|
||||
}
|
||||
|
||||
Future<AddResult> addCode(
|
||||
|
@ -81,10 +76,10 @@ class CodeStore {
|
|||
AccountMode? accountMode,
|
||||
}) async {
|
||||
final mode = accountMode ?? _authenticatorService.getAccountMode();
|
||||
final codes = await getAllCodes(accountMode: mode);
|
||||
final allCodes = await getAllCodes(accountMode: mode);
|
||||
bool isExistingCode = false;
|
||||
bool hasSameCode = false;
|
||||
for (final existingCode in codes.validCodes) {
|
||||
for (final existingCode in allCodes.codes) {
|
||||
if (code.generatedID != null &&
|
||||
existingCode.generatedID == code.generatedID) {
|
||||
isExistingCode = true;
|
||||
|
@ -143,7 +138,7 @@ class CodeStore {
|
|||
|
||||
List<Code> offlineCodes = (await CodeStore.instance
|
||||
.getAllCodes(accountMode: AccountMode.offline))
|
||||
.validCodes;
|
||||
.codes;
|
||||
if (offlineCodes.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
@ -154,7 +149,7 @@ class CodeStore {
|
|||
}
|
||||
final List<Code> onlineCodes = (await CodeStore.instance
|
||||
.getAllCodes(accountMode: AccountMode.online))
|
||||
.validCodes;
|
||||
.codes;
|
||||
logger.info(
|
||||
'importing ${offlineCodes.length} offline codes with ${onlineCodes.length} online codes',
|
||||
);
|
||||
|
|
|
@ -28,9 +28,13 @@ import 'package:move_to_background/move_to_background.dart';
|
|||
|
||||
class CodeWidget extends StatefulWidget {
|
||||
final Code code;
|
||||
final List<String> tags;
|
||||
final bool hasError;
|
||||
|
||||
const CodeWidget(this.code, this.tags, {super.key});
|
||||
const CodeWidget(
|
||||
this.code, {
|
||||
super.key,
|
||||
this.hasError = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CodeWidget> createState() => _CodeWidgetState();
|
||||
|
@ -88,135 +92,145 @@ class _CodeWidgetState extends State<CodeWidget> {
|
|||
_isInitialized = true;
|
||||
}
|
||||
final l10n = context.l10n;
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(left: 16, right: 16, bottom: 8, top: 8),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (PlatformUtil.isDesktop()) {
|
||||
return ContextMenuRegion(
|
||||
contextMenu: ContextMenu(
|
||||
entries: <ContextMenuEntry>[
|
||||
MenuItem(
|
||||
label: 'QR',
|
||||
icon: Icons.qr_code_2_outlined,
|
||||
onSelected: () => _onShowQrPressed(null),
|
||||
),
|
||||
MenuItem(
|
||||
label: widget.code.isPinned ? l10n.unpinText : l10n.pinText,
|
||||
icon: widget.code.isPinned
|
||||
? Icons.push_pin
|
||||
: Icons.push_pin_outlined,
|
||||
onSelected: () => _onShowQrPressed(null),
|
||||
),
|
||||
MenuItem(
|
||||
label: l10n.edit,
|
||||
icon: Icons.edit,
|
||||
onSelected: () => _onEditPressed(null),
|
||||
),
|
||||
const MenuDivider(),
|
||||
MenuItem(
|
||||
label: l10n.delete,
|
||||
value: "Delete",
|
||||
icon: Icons.delete,
|
||||
onSelected: () => _onDeletePressed(null),
|
||||
),
|
||||
],
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
),
|
||||
child: _clippedCard(l10n),
|
||||
);
|
||||
}
|
||||
|
||||
return Slidable(
|
||||
key: ValueKey(widget.code.hashCode),
|
||||
endActionPane: ActionPane(
|
||||
extentRatio: 0.90,
|
||||
motion: const ScrollMotion(),
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 14,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: _onShowQrPressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
icon: Icons.qr_code_2_outlined,
|
||||
label: "QR",
|
||||
padding: const EdgeInsets.only(left: 4, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 14,
|
||||
),
|
||||
CustomSlidableAction(
|
||||
onPressed: _onPinPressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (widget.code.isPinned)
|
||||
SvgPicture.asset(
|
||||
"assets/svg/pin-active.svg",
|
||||
colorFilter: ui.ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
)
|
||||
else
|
||||
SvgPicture.asset(
|
||||
"assets/svg/pin-inactive.svg",
|
||||
colorFilter: ui.ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
widget.code.isPinned ? l10n.unpinText : l10n.pinText,
|
||||
return IgnorePointer(
|
||||
ignoring: widget.hasError,
|
||||
child: Opacity(
|
||||
opacity: widget.hasError ? 0.5 : 1.0,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 16, right: 16, bottom: 8, top: 8),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (PlatformUtil.isDesktop()) {
|
||||
return ContextMenuRegion(
|
||||
contextMenu: ContextMenu(
|
||||
entries: <ContextMenuEntry>[
|
||||
MenuItem(
|
||||
label: 'QR',
|
||||
icon: Icons.qr_code_2_outlined,
|
||||
onSelected: () => _onShowQrPressed(null),
|
||||
),
|
||||
MenuItem(
|
||||
label: widget.code.isPinned
|
||||
? l10n.unpinText
|
||||
: l10n.pinText,
|
||||
icon: widget.code.isPinned
|
||||
? Icons.push_pin
|
||||
: Icons.push_pin_outlined,
|
||||
onSelected: () => _onShowQrPressed(null),
|
||||
),
|
||||
MenuItem(
|
||||
label: l10n.edit,
|
||||
icon: Icons.edit,
|
||||
onSelected: () => _onEditPressed(null),
|
||||
),
|
||||
const MenuDivider(),
|
||||
MenuItem(
|
||||
label: l10n.delete,
|
||||
value: "Delete",
|
||||
icon: Icons.delete,
|
||||
onSelected: () => _onDeletePressed(null),
|
||||
),
|
||||
],
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 4, right: 0),
|
||||
child: _clippedCard(l10n),
|
||||
);
|
||||
}
|
||||
|
||||
return Slidable(
|
||||
key: ValueKey(widget.code.hashCode),
|
||||
endActionPane: ActionPane(
|
||||
extentRatio: 0.90,
|
||||
motion: const ScrollMotion(),
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 14,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: _onShowQrPressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
icon: Icons.qr_code_2_outlined,
|
||||
label: "QR",
|
||||
padding: const EdgeInsets.only(left: 4, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 14,
|
||||
),
|
||||
CustomSlidableAction(
|
||||
onPressed: _onPinPressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (widget.code.isPinned)
|
||||
SvgPicture.asset(
|
||||
"assets/svg/pin-active.svg",
|
||||
colorFilter: ui.ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
)
|
||||
else
|
||||
SvgPicture.asset(
|
||||
"assets/svg/pin-inactive.svg",
|
||||
colorFilter: ui.ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
widget.code.isPinned
|
||||
? l10n.unpinText
|
||||
: l10n.pinText,
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 4, right: 0),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 14,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: _onEditPressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
icon: Icons.edit_outlined,
|
||||
label: l10n.edit,
|
||||
padding: const EdgeInsets.only(left: 4, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 14,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: _onDeletePressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
foregroundColor: const Color(0xFFFE4A49),
|
||||
icon: Icons.delete,
|
||||
label: l10n.delete,
|
||||
padding: const EdgeInsets.only(left: 0, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
width: 14,
|
||||
child: Builder(
|
||||
builder: (context) => _clippedCard(l10n),
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: _onEditPressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
icon: Icons.edit_outlined,
|
||||
label: l10n.edit,
|
||||
padding: const EdgeInsets.only(left: 4, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 14,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: _onDeletePressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
foregroundColor: const Color(0xFFFE4A49),
|
||||
icon: Icons.delete,
|
||||
label: l10n.delete,
|
||||
padding: const EdgeInsets.only(left: 0, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) => _clippedCard(l10n),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -520,7 +534,6 @@ class _CodeWidgetState extends State<CodeWidget> {
|
|||
builder: (BuildContext context) {
|
||||
return SetupEnterSecretKeyPage(
|
||||
code: widget.code,
|
||||
tags: widget.tags,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:gradient_borders/box_borders/gradient_box_border.dart';
|
||||
|
||||
class GradientButton extends StatefulWidget {
|
||||
|
@ -113,22 +114,19 @@ class _GradientButtonState extends State<GradientButton> {
|
|||
],
|
||||
),
|
||||
),
|
||||
if (!isTapped)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(widget.borderRadius),
|
||||
child: SvgPicture.asset(
|
||||
'assets/svg/button-tint.svg',
|
||||
fit: BoxFit.fill,
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
gradient: isTapped
|
||||
? null
|
||||
: const LinearGradient(
|
||||
begin: Alignment.bottomRight,
|
||||
end: Alignment.topLeft,
|
||||
stops: [0, 0.16, 0.88],
|
||||
colors: [
|
||||
Color.fromRGBO(179, 127, 235, 1),
|
||||
Color.fromRGBO(210, 174, 245, 0),
|
||||
Color.fromRGBO(239, 219, 255, 1),
|
||||
],
|
||||
),
|
||||
backgroundBlendMode: isTapped ? null : BlendMode.hue,
|
||||
border: GradientBoxBorder(
|
||||
width: widget.borderWidth,
|
||||
gradient: const LinearGradient(
|
||||
|
|
|
@ -11,9 +11,12 @@ import 'package:ente_auth/events/trigger_logout_event.dart';
|
|||
import "package:ente_auth/l10n/l10n.dart";
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/models/codes.dart';
|
||||
import 'package:ente_auth/onboarding/model/tag_enums.dart';
|
||||
import 'package:ente_auth/onboarding/view/common/tag_chip.dart';
|
||||
import 'package:ente_auth/onboarding/view/setup_enter_secret_key_page.dart';
|
||||
import 'package:ente_auth/services/preference_service.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/store/code_display_store.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/ui/account/logout_dialog.dart';
|
||||
import 'package:ente_auth/ui/code_error_widget.dart';
|
||||
|
@ -56,8 +59,9 @@ class _HomePageState extends State<HomePage> {
|
|||
final FocusNode searchInputFocusNode = FocusNode();
|
||||
bool _showSearchBox = false;
|
||||
String _searchText = "";
|
||||
Codes? _codes;
|
||||
List<CodeState> _filteredCodes = [];
|
||||
AllCodes? _allCodes;
|
||||
List<String> tags = [];
|
||||
List<Code> _filteredCodes = [];
|
||||
StreamSubscription<CodesUpdatedEvent>? _streamSubscription;
|
||||
StreamSubscription<TriggerLogoutEvent>? _triggerLogoutEvent;
|
||||
StreamSubscription<IconsChangedEvent>? _iconsChangedEvent;
|
||||
|
@ -99,47 +103,56 @@ class _HomePageState extends State<HomePage> {
|
|||
|
||||
void _loadCodes() {
|
||||
CodeStore.instance.getAllCodes().then((codes) {
|
||||
_codes = codes;
|
||||
_allCodes = codes;
|
||||
_hasLoaded = true;
|
||||
_applyFilteringAndRefresh();
|
||||
}).onError((error, stackTrace) {
|
||||
_logger.severe('Error while loading codes', error, stackTrace);
|
||||
});
|
||||
CodeDisplayStore.instance.getAllTags().then((value) {
|
||||
tags = value;
|
||||
|
||||
if (mounted) {
|
||||
if (!tags.contains(selectedTag)) {
|
||||
selectedTag = "";
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
}).onError((error, stackTrace) {
|
||||
_logger.severe('Error while loading tags', error, stackTrace);
|
||||
});
|
||||
}
|
||||
|
||||
void _applyFilteringAndRefresh() {
|
||||
if (_searchText.isNotEmpty && _showSearchBox && _codes != null) {
|
||||
if (_searchText.isNotEmpty && _showSearchBox && _allCodes != null) {
|
||||
final String val = _searchText.toLowerCase();
|
||||
// Prioritize issuer match above account for better UX while searching
|
||||
// for a specific TOTP for email providers. Searching for "emailProvider" like (gmail, proton) should
|
||||
// show the email provider first instead of other accounts where protonmail
|
||||
// is the account name.
|
||||
final List<CodeState> issuerMatch = [];
|
||||
final List<CodeState> accountMatch = [];
|
||||
|
||||
for (final CodeState codeState in _codes!.allCodes) {
|
||||
if (codeState.error != null) continue;
|
||||
final List<Code> issuerMatch = [];
|
||||
final List<Code> accountMatch = [];
|
||||
|
||||
for (final Code codeState in _allCodes!.codes) {
|
||||
if (selectedTag != "" &&
|
||||
!codeState.code!.display.tags.contains(selectedTag)) {
|
||||
!codeState.display.tags.contains(selectedTag)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (codeState.code!.issuer.toLowerCase().contains(val)) {
|
||||
if (codeState.issuer.toLowerCase().contains(val)) {
|
||||
issuerMatch.add(codeState);
|
||||
} else if (codeState.code!.account.toLowerCase().contains(val)) {
|
||||
} else if (codeState.account.toLowerCase().contains(val)) {
|
||||
accountMatch.add(codeState);
|
||||
}
|
||||
}
|
||||
_filteredCodes = issuerMatch;
|
||||
_filteredCodes.addAll(accountMatch);
|
||||
} else {
|
||||
_filteredCodes = _codes?.allCodes
|
||||
_filteredCodes = _allCodes?.codes
|
||||
.where(
|
||||
(element) =>
|
||||
selectedTag == "" ||
|
||||
element.code != null &&
|
||||
element.code!.display.tags.contains(selectedTag),
|
||||
element.display.tags.contains(selectedTag),
|
||||
)
|
||||
.toList() ??
|
||||
[];
|
||||
|
@ -169,7 +182,7 @@ class _HomePageState extends State<HomePage> {
|
|||
if (code != null) {
|
||||
await CodeStore.instance.addCode(code);
|
||||
// Focus the new code by searching
|
||||
if ((_codes?.allCodes.length ?? 0) > 2) {
|
||||
if ((_allCodes?.codes.length ?? 0) > 2) {
|
||||
_focusNewCode(code);
|
||||
}
|
||||
}
|
||||
|
@ -179,9 +192,7 @@ class _HomePageState extends State<HomePage> {
|
|||
final Code? code = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return SetupEnterSecretKeyPage(
|
||||
tags: _codes?.tags ?? [],
|
||||
);
|
||||
return SetupEnterSecretKeyPage();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -193,10 +204,7 @@ class _HomePageState extends State<HomePage> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
if (!(_codes?.tags.contains(selectedTag) ?? true)) {
|
||||
selectedTag = "";
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
return PopScope(
|
||||
onPopInvoked: (_) async {
|
||||
if (_isSettingsOpen) {
|
||||
|
@ -245,30 +253,32 @@ class _HomePageState extends State<HomePage> {
|
|||
),
|
||||
centerTitle: true,
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: _showSearchBox
|
||||
? const Icon(Icons.clear)
|
||||
: const Icon(Icons.search),
|
||||
tooltip: l10n.search,
|
||||
onPressed: () {
|
||||
setState(
|
||||
() {
|
||||
_showSearchBox = !_showSearchBox;
|
||||
if (!_showSearchBox) {
|
||||
_textController.clear();
|
||||
_searchText = "";
|
||||
} else {
|
||||
_searchText = _textController.text;
|
||||
}
|
||||
_applyFilteringAndRefresh();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
if (_allCodes?.state == AllCodesState.value)
|
||||
IconButton(
|
||||
icon: _showSearchBox
|
||||
? const Icon(Icons.clear)
|
||||
: const Icon(Icons.search),
|
||||
tooltip: l10n.search,
|
||||
onPressed: () {
|
||||
setState(
|
||||
() {
|
||||
_showSearchBox = !_showSearchBox;
|
||||
if (!_showSearchBox) {
|
||||
_textController.clear();
|
||||
_searchText = "";
|
||||
} else {
|
||||
_searchText = _textController.text;
|
||||
}
|
||||
_applyFilteringAndRefresh();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: !_hasLoaded ||
|
||||
(_codes?.allCodes.isEmpty ?? true) ||
|
||||
(_allCodes?.codes.isEmpty ?? true) ||
|
||||
_allCodes?.state == AllCodesState.error ||
|
||||
!PreferenceService.instance.hasShownCoachMark()
|
||||
? null
|
||||
: _getFab(),
|
||||
|
@ -288,71 +298,75 @@ class _HomePageState extends State<HomePage> {
|
|||
final list = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 2),
|
||||
separatorBuilder: (context, index) => const SizedBox(width: 8),
|
||||
itemCount: _codes?.tags == null ? 0 : _codes!.tags.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
if (_allCodes?.state == AllCodesState.value)
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 2),
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(width: 8),
|
||||
itemCount: tags.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return TagChip(
|
||||
label: "All",
|
||||
state: selectedTag == ""
|
||||
? TagChipState.selected
|
||||
: TagChipState.unselected,
|
||||
onTap: () {
|
||||
selectedTag = "";
|
||||
setState(() {});
|
||||
_applyFilteringAndRefresh();
|
||||
},
|
||||
);
|
||||
}
|
||||
return TagChip(
|
||||
label: "All",
|
||||
state: selectedTag == ""
|
||||
label: tags[index - 1],
|
||||
action: TagChipAction.menu,
|
||||
state: selectedTag == tags[index - 1]
|
||||
? TagChipState.selected
|
||||
: TagChipState.unselected,
|
||||
onTap: () {
|
||||
selectedTag = "";
|
||||
if (selectedTag == tags[index - 1]) {
|
||||
selectedTag = "";
|
||||
setState(() {});
|
||||
_applyFilteringAndRefresh();
|
||||
return;
|
||||
}
|
||||
selectedTag = tags[index - 1];
|
||||
setState(() {});
|
||||
_applyFilteringAndRefresh();
|
||||
},
|
||||
);
|
||||
}
|
||||
return TagChip(
|
||||
label: _codes!.tags[index - 1],
|
||||
action: TagChipAction.menu,
|
||||
state: selectedTag == _codes!.tags[index - 1]
|
||||
? TagChipState.selected
|
||||
: TagChipState.unselected,
|
||||
onTap: () {
|
||||
if (selectedTag == _codes!.tags[index - 1]) {
|
||||
selectedTag = "";
|
||||
setState(() {});
|
||||
_applyFilteringAndRefresh();
|
||||
return;
|
||||
}
|
||||
selectedTag = _codes!.tags[index - 1];
|
||||
setState(() {});
|
||||
_applyFilteringAndRefresh();
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: AlignedGridView.count(
|
||||
crossAxisCount: (MediaQuery.sizeOf(context).width ~/ 400)
|
||||
.clamp(1, double.infinity)
|
||||
.toInt(),
|
||||
physics: _allCodes?.state == AllCodesState.value
|
||||
? const AlwaysScrollableScrollPhysics()
|
||||
: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.only(bottom: 80),
|
||||
itemBuilder: ((context, index) {
|
||||
try {
|
||||
if (_filteredCodes[index].error != null) {
|
||||
return const CodeErrorWidget();
|
||||
}
|
||||
return ClipRect(
|
||||
child: CodeWidget(
|
||||
_filteredCodes[index].code!,
|
||||
_codes?.tags ?? [],
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
return const Text("Failed");
|
||||
if (index == 0 && _allCodes?.state == AllCodesState.error) {
|
||||
return const CodeErrorWidget();
|
||||
}
|
||||
final newIndex =
|
||||
index - (_allCodes?.state == AllCodesState.error ? 1 : 0);
|
||||
return ClipRect(
|
||||
child: CodeWidget(
|
||||
_filteredCodes[newIndex],
|
||||
hasError: _allCodes?.state == AllCodesState.error,
|
||||
),
|
||||
);
|
||||
}),
|
||||
itemCount: _filteredCodes.length,
|
||||
itemCount: (_allCodes?.state == AllCodesState.error ? 1 : 0) +
|
||||
_filteredCodes.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -377,18 +391,9 @@ class _HomePageState extends State<HomePage> {
|
|||
padding: const EdgeInsets.only(bottom: 80),
|
||||
itemBuilder: ((context, index) {
|
||||
final codeState = _filteredCodes[index];
|
||||
if (codeState.code != null) {
|
||||
return CodeWidget(
|
||||
codeState.code!,
|
||||
_codes?.tags ?? [],
|
||||
);
|
||||
} else {
|
||||
_logger.severe(
|
||||
"code widget error",
|
||||
codeState.error,
|
||||
);
|
||||
return const CodeErrorWidget();
|
||||
}
|
||||
return CodeWidget(
|
||||
codeState,
|
||||
);
|
||||
}),
|
||||
itemCount: _filteredCodes.length,
|
||||
)
|
||||
|
|
|
@ -171,9 +171,9 @@ Future<void> _exportCodes(BuildContext context, String fileContent) async {
|
|||
}
|
||||
|
||||
Future<String> _getAuthDataForExport() async {
|
||||
final codes = await CodeStore.instance.getAllCodes();
|
||||
final allCodes = await CodeStore.instance.getAllCodes();
|
||||
String data = "";
|
||||
for (final code in codes.validCodes) {
|
||||
for (final code in allCodes.codes) {
|
||||
data += "${code.rawData}\n";
|
||||
}
|
||||
|
||||
|
|
|
@ -108,9 +108,8 @@ class SettingsPage extends StatelessWidget {
|
|||
await handleExportClick(context);
|
||||
} else {
|
||||
if (result.action == ButtonAction.second) {
|
||||
bool hasCodes = (await CodeStore.instance.getAllCodes())
|
||||
.validCodes
|
||||
.isNotEmpty;
|
||||
bool hasCodes =
|
||||
(await CodeStore.instance.getAllCodes()).codes.isNotEmpty;
|
||||
if (hasCodes) {
|
||||
final hasAuthenticated = await LocalAuthenticationService
|
||||
.instance
|
||||
|
|
Loading…
Reference in a new issue