diff --git a/auth/assets/svg/button-tint.svg b/auth/assets/svg/button-tint.svg
new file mode 100644
index 000000000..1751aece1
--- /dev/null
+++ b/auth/assets/svg/button-tint.svg
@@ -0,0 +1,11 @@
+
diff --git a/auth/lib/main.dart b/auth/lib/main.dart
index ae1bbd69c..9fa2841ff 100644
--- a/auth/lib/main.dart
+++ b/auth/lib/main.dart
@@ -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 _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();
diff --git a/auth/lib/models/codes.dart b/auth/lib/models/codes.dart
index 249b36394..898ed78f9 100644
--- a/auth/lib/models/codes.dart
+++ b/auth/lib/models/codes.dart
@@ -1,26 +1,13 @@
import 'package:ente_auth/models/code.dart';
-class CodeState {
- final Code? code;
- final String? error;
+class AllCodes {
+ final List 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 allCodes;
- final List tags;
-
- Codes({
- required this.allCodes,
- required this.tags,
- });
-
- List get validCodes => allCodes
- .where((element) => element.code != null)
- .map((e) => e.code!)
- .toList();
+enum AllCodesState {
+ value,
+ error,
}
diff --git a/auth/lib/onboarding/model/tag_enums.dart b/auth/lib/onboarding/model/tag_enums.dart
new file mode 100644
index 000000000..6661b6770
--- /dev/null
+++ b/auth/lib/onboarding/model/tag_enums.dart
@@ -0,0 +1,10 @@
+enum TagChipState {
+ selected,
+ unselected,
+}
+
+enum TagChipAction {
+ none,
+ menu,
+ check,
+}
diff --git a/auth/lib/onboarding/view/common/add_chip.dart b/auth/lib/onboarding/view/common/add_chip.dart
new file mode 100644
index 000000000..a93e5c392
--- /dev/null
+++ b/auth/lib/onboarding/view/common/add_chip.dart
@@ -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),
+ ),
+ ),
+ );
+ }
+}
diff --git a/auth/lib/onboarding/view/common/add_tag.dart b/auth/lib/onboarding/view/common/add_tag.dart
new file mode 100644
index 000000000..3fb42071e
--- /dev/null
+++ b/auth/lib/onboarding/view/common/add_tag.dart
@@ -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 createState() => _AddTagDialogState();
+}
+
+class _AddTagDialogState extends State {
+ 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);
+ },
+ ),
+ ],
+ );
+ }
+}
diff --git a/auth/lib/onboarding/view/common/edit_tag.dart b/auth/lib/onboarding/view/common/edit_tag.dart
new file mode 100644
index 000000000..fe1f38564
--- /dev/null
+++ b/auth/lib/onboarding/view/common/edit_tag.dart
@@ -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 createState() => _EditTagDialogState();
+}
+
+class _EditTagDialogState extends State {
+ 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);
+ },
+ ),
+ ],
+ );
+ }
+}
diff --git a/auth/lib/onboarding/view/common/tag_chip.dart b/auth/lib/onboarding/view/common/tag_chip.dart
new file mode 100644
index 000000000..efd246cfe
--- /dev/null
+++ b/auth/lib/onboarding/view/common/tag_chip.dart
@@ -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(
+ 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),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/auth/lib/onboarding/view/setup_enter_secret_key_page.dart b/auth/lib/onboarding/view/setup_enter_secret_key_page.dart
index d690bcfad..4993567cf 100644
--- a/auth/lib/onboarding/view/setup_enter_secret_key_page.dart
+++ b/auth/lib/onboarding/view/setup_enter_secret_key_page.dart
@@ -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 tags;
- SetupEnterSecretKeyPage({this.code, super.key, required this.tags});
+ SetupEnterSecretKeyPage({this.code, super.key});
@override
State createState() =>
@@ -26,7 +32,8 @@ class _SetupEnterSecretKeyPageState extends State {
late TextEditingController _secretController;
late bool _secretKeyObscured;
late List tags = [...?widget.code?.display.tags];
- late List allTags = [...widget.tags];
+ List allTags = [];
+ StreamSubscription? _streamSubscription;
@override
void initState() {
@@ -41,9 +48,24 @@ class _SetupEnterSecretKeyPageState extends State {
text: widget.code?.secret,
);
_secretKeyObscured = widget.code != null;
+ _loadTags();
+ _streamSubscription = Bus.instance.on().listen((event) {
+ _loadTags();
+ });
super.initState();
}
+ @override
+ void dispose() {
+ _streamSubscription?.cancel();
+ super.dispose();
+ }
+
+ Future _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 {
);
}
}
-
-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(
- 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 createState() => _AddTagDialogState();
-}
-
-class _AddTagDialogState extends State {
- 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 createState() => _EditTagDialogState();
-}
-
-class _EditTagDialogState extends State {
- 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 = [];
-
- 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 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 = [];
-
- 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 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,
- );
-}
diff --git a/auth/lib/store/code_display_store.dart b/auth/lib/store/code_display_store.dart
new file mode 100644
index 000000000..37a1cf969
--- /dev/null
+++ b/auth/lib/store/code_display_store.dart
@@ -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 init() async {
+ _authenticatorService = AuthenticatorService.instance;
+ _codeStore = CodeStore.instance;
+ }
+
+ Future> getAllTags({AccountMode? accountMode}) async {
+ final mode = accountMode ?? _authenticatorService.getAccountMode();
+ final List entities =
+ await _authenticatorService.getEntities(mode);
+ List 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 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 = [];
+
+ 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 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 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 = [];
+
+ 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);
+ }
+}
diff --git a/auth/lib/store/code_store.dart b/auth/lib/store/code_store.dart
index 4028af4f0..3ed6d6f3e 100644
--- a/auth/lib/store/code_store.dart
+++ b/auth/lib/store/code_store.dart
@@ -23,12 +23,13 @@ class CodeStore {
_authenticatorService = AuthenticatorService.instance;
}
- Future getAllCodes({AccountMode? accountMode}) async {
+ Future getAllCodes({AccountMode? accountMode}) async {
final mode = accountMode ?? _authenticatorService.getAccountMode();
final List entities =
await _authenticatorService.getEntities(mode);
- final List codes = [];
- List tags = [];
+ final List 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 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 offlineCodes = (await CodeStore.instance
.getAllCodes(accountMode: AccountMode.offline))
- .validCodes;
+ .codes;
if (offlineCodes.isEmpty) {
return;
}
@@ -154,7 +149,7 @@ class CodeStore {
}
final List onlineCodes = (await CodeStore.instance
.getAllCodes(accountMode: AccountMode.online))
- .validCodes;
+ .codes;
logger.info(
'importing ${offlineCodes.length} offline codes with ${onlineCodes.length} online codes',
);
diff --git a/auth/lib/ui/code_widget.dart b/auth/lib/ui/code_widget.dart
index 3504da333..a350f5ee4 100644
--- a/auth/lib/ui/code_widget.dart
+++ b/auth/lib/ui/code_widget.dart
@@ -28,9 +28,13 @@ import 'package:move_to_background/move_to_background.dart';
class CodeWidget extends StatefulWidget {
final Code code;
- final List tags;
+ final bool hasError;
- const CodeWidget(this.code, this.tags, {super.key});
+ const CodeWidget(
+ this.code, {
+ super.key,
+ this.hasError = false,
+ });
@override
State createState() => _CodeWidgetState();
@@ -88,135 +92,145 @@ class _CodeWidgetState extends State {
_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: [
- 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: [
+ 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 {
builder: (BuildContext context) {
return SetupEnterSecretKeyPage(
code: widget.code,
- tags: widget.tags,
);
},
),
diff --git a/auth/lib/ui/common/gradient_button.dart b/auth/lib/ui/common/gradient_button.dart
index 39317e1e2..89a666bc4 100644
--- a/auth/lib/ui/common/gradient_button.dart
+++ b/auth/lib/ui/common/gradient_button.dart
@@ -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 {
],
),
),
+ 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(
diff --git a/auth/lib/ui/home_page.dart b/auth/lib/ui/home_page.dart
index 2f48b4f6f..1e46890f4 100644
--- a/auth/lib/ui/home_page.dart
+++ b/auth/lib/ui/home_page.dart
@@ -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 {
final FocusNode searchInputFocusNode = FocusNode();
bool _showSearchBox = false;
String _searchText = "";
- Codes? _codes;
- List _filteredCodes = [];
+ AllCodes? _allCodes;
+ List tags = [];
+ List _filteredCodes = [];
StreamSubscription? _streamSubscription;
StreamSubscription? _triggerLogoutEvent;
StreamSubscription? _iconsChangedEvent;
@@ -99,47 +103,56 @@ class _HomePageState extends State {
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 issuerMatch = [];
- final List accountMatch = [];
-
- for (final CodeState codeState in _codes!.allCodes) {
- if (codeState.error != null) continue;
+ final List issuerMatch = [];
+ final List 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 {
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 {
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 {
@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 {
),
centerTitle: true,
actions: [
- 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 {
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 {
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,
)
diff --git a/auth/lib/ui/settings/data/export_widget.dart b/auth/lib/ui/settings/data/export_widget.dart
index dcaf15afa..e7372f604 100644
--- a/auth/lib/ui/settings/data/export_widget.dart
+++ b/auth/lib/ui/settings/data/export_widget.dart
@@ -171,9 +171,9 @@ Future _exportCodes(BuildContext context, String fileContent) async {
}
Future _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";
}
diff --git a/auth/lib/ui/settings_page.dart b/auth/lib/ui/settings_page.dart
index 087b9f9ea..512322753 100644
--- a/auth/lib/ui/settings_page.dart
+++ b/auth/lib/ui/settings_page.dart
@@ -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