Browse Source

feat: tags ui and logic complete

Prateek Sunal 1 year ago
parent
commit
35672eeec0

+ 8 - 1
auth/lib/l10n/arb/app_en.arb

@@ -427,5 +427,12 @@
   "pinText": "Pin",
   "unpinText": "Unpin",
   "pinnedCodeMessage": "{code} has been pinned",
-  "unpinnedCodeMessage": "{code} has been unpinned"
+  "unpinnedCodeMessage": "{code} has been unpinned",
+  "tags": "Tags",
+  "createNewTag": "Create New Tag",
+  "tag": "Tag",
+  "create": "Create",
+  "editTag": "Edit Tag",
+  "deleteTagTitle": "Delete tag?",
+  "deleteTagMessage": "Are you sure you want to delete this tag? This action is irreversible."
 }

+ 362 - 58
auth/lib/onboarding/view/setup_enter_secret_key_page.dart

@@ -6,6 +6,7 @@ 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;
@@ -22,9 +23,9 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
   late TextEditingController _issuerController;
   late TextEditingController _accountController;
   late TextEditingController _secretController;
-  late TextEditingController _tagController;
   late bool _secretKeyObscured;
   late List<String> tags = [...?widget.code?.display.tags];
+  late List<String> allTags = [...widget.tags];
 
   @override
   void initState() {
@@ -38,7 +39,6 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
     _secretController = TextEditingController(
       text: widget.code?.secret,
     );
-    _tagController = TextEditingController();
     _secretKeyObscured = widget.code != null;
     super.initState();
   }
@@ -117,51 +117,59 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
                 const SizedBox(
                   height: 20,
                 ),
-                TextFormField(
-                  // The validator receives the text that the user has entered.
-                  validator: (value) {
-                    if (value == null || value.isEmpty) {
-                      return "Please enter some text";
-                    }
-                    return null;
-                  },
-                  onFieldSubmitted: (str) {
-                    if (str.trim().isEmpty) {
-                      return;
-                    }
-
-                    if (!tags.contains(str)) {
-                      tags.add(str);
-                    }
-
-                    // Clear the tag
-                    _tagController.text = "";
-                    setState(() {});
-                  },
-                  decoration: InputDecoration(
-                    hintText: l10n.codeTagHint,
+                Text(
+                  l10n.tags,
+                  style: const TextStyle(
+                    fontWeight: FontWeight.bold,
                   ),
-                  controller: _tagController,
                 ),
-                if (tags.isNotEmpty) ...[
-                  const SizedBox(height: 10),
-                  Wrap(
-                    spacing: 12,
-                    alignment: WrapAlignment.start,
-                    children: tags
-                        .map(
-                          (e) => TagChip(
-                            label: e,
-                            onTap: () {
-                              tags.remove(e);
-                              setState(() {});
-                            },
-                          ),
-                        )
-                        .toList(),
-                  ),
-                  const SizedBox(height: 10),
-                ],
+                const SizedBox(height: 10),
+                Wrap(
+                  spacing: 12,
+                  alignment: WrapAlignment.start,
+                  children: [
+                    ...allTags.map(
+                      (e) => TagChip(
+                        label: e,
+                        action: TagChipAction.check,
+                        state: tags.contains(e)
+                            ? TagChipState.selected
+                            : TagChipState.unselected,
+                        onTap: () {
+                          if (tags.contains(e)) {
+                            tags.remove(e);
+                          } else {
+                            tags.add(e);
+                          }
+                          setState(() {});
+                        },
+                      ),
+                    ),
+                    AddChip(
+                      onTap: () {
+                        showDialog(
+                          context: context,
+                          builder: (BuildContext context) {
+                            return AddTagDialog(
+                              onTap: (tag) {
+                                if (allTags.contains(tag) &&
+                                    tags.contains(tag)) {
+                                  return;
+                                }
+                                allTags.add(tag);
+                                tags.add(tag);
+                                setState(() {});
+                                Navigator.pop(context);
+                              },
+                            );
+                          },
+                          barrierColor: Colors.black.withOpacity(0.85),
+                          barrierDismissible: false,
+                        );
+                      },
+                    ),
+                  ],
+                ),
                 const SizedBox(
                   height: 40,
                 ),
@@ -187,7 +195,6 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
                     child: Padding(
                       padding: const EdgeInsets.symmetric(
                         horizontal: 16.0,
-                        vertical: 4,
                       ),
                       child: Text(l10n.saveAction),
                     ),
@@ -254,15 +261,54 @@ 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 bool isSelected;
+  final TagChipState state;
+  final TagChipAction action;
 
   const TagChip({
     super.key,
     required this.label,
-    this.isSelected = false,
+    this.state = TagChipState.unselected,
+    this.action = TagChipAction.none,
     this.onTap,
   });
 
@@ -270,35 +316,293 @@ class TagChip extends StatelessWidget {
   Widget build(BuildContext context) {
     return GestureDetector(
       onTap: onTap,
-      child: Chip(
-        label: Row(
+      child: Container(
+        decoration: BoxDecoration(
+          color: state == TagChipState.selected
+              ? Theme.of(context).brightness == Brightness.dark
+                  ? const Color(0xFF722ED1)
+                  : 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(0x00B37FEB),
+                      const Color(0x33AE40E3),
+                    ]
+                  : [
+                      Theme.of(context).brightness == Brightness.dark
+                          ? const Color(0xFFAD00FF)
+                          : const Color(0x00AD00FF),
+                      Theme.of(context).brightness == Brightness.dark
+                          ? const Color(0x43A269BD)
+                          : const Color(0x338609C2),
+                    ],
+              begin: Alignment.topLeft,
+              end: Alignment.bottomRight,
+            ),
+          ),
+        ),
+        margin: const EdgeInsets.symmetric(vertical: 4),
+        padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
+        child: Row(
           mainAxisSize: MainAxisSize.min,
           children: [
             Text(
               label,
               style: TextStyle(
-                color: isSelected ? Colors.white : const Color(0xFF8232E1),
+                color: state == TagChipState.selected ||
+                        Theme.of(context).brightness == Brightness.dark
+                    ? Colors.white
+                    : const Color(0xFF8232E1),
               ),
             ),
-            if (isSelected) ...[
+            if (state == TagChipState.selected &&
+                action == TagChipAction.check) ...[
               const SizedBox(width: 4),
               const Icon(
                 Icons.check,
                 size: 16,
                 color: Colors.white,
               ),
+            ] else if (state == TagChipState.selected &&
+                action == TagChipAction.menu) ...[
+              SizedBox(
+                width: 18,
+                child: PopupMenuButton<int>(
+                  iconSize: 16,
+                  shape: RoundedRectangleBorder(
+                    borderRadius: BorderRadius.circular(8),
+                  ),
+                  surfaceTintColor: Theme.of(context).cardColor,
+                  iconColor: Colors.white,
+                  initialValue: -1,
+                  padding: EdgeInsets.zero,
+                  onSelected: (value) {
+                    if (value == 0) {
+                      showEditDialog(context, label);
+                    } else if (value == 1) {
+                      showDeleteTagDialog(context);
+                    }
+                  },
+                  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,
+                      ),
+                    ];
+                  },
+                ),
+              ),
             ],
           ],
         ),
-        backgroundColor:
-            isSelected ? const Color(0xFF722ED1) : const Color(0xFFFCF5FF),
-        side: BorderSide(
-          color: isSelected ? const Color(0xFF722ED1) : const Color(0xFFFCF5FF),
+      ),
+    );
+  }
+}
+
+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,
+            ),
+          ],
         ),
-        shape: RoundedRectangleBorder(
-          borderRadius: BorderRadius.circular(100),
+      ),
+      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: () {
+            if (_tag.trim().isEmpty) return;
+
+            // traverse through all the codes and edit this tag's value
+          },
+        ),
+      ],
     );
   }
 }
+
+Future<void> showDeleteTagDialog(BuildContext context) 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 codes and remove this tag
+    },
+  );
+}
+
+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,
+  );
+}

+ 1 - 1
auth/lib/theme/colors.dart

@@ -200,7 +200,7 @@ const Color _primary500 = Color.fromARGB(255, 204, 10, 101);
 const Color _primary400 = Color.fromARGB(255, 122, 41, 193);
 const Color _primary300 = Color.fromARGB(255, 152, 77, 244);
 
-const Color _warning700 = Color.fromRGBO(234, 63, 63, 1);
+const Color _warning700 = Color.fromRGBO(245, 52, 52, 1);
 const Color _warning500 = Color.fromRGBO(255, 101, 101, 1);
 const Color _warning800 = Color(0xFFF53434);
 const Color warning500 = Color.fromRGBO(255, 101, 101, 1);

+ 25 - 12
auth/lib/ui/code_widget.dart

@@ -1,5 +1,6 @@
 import 'dart:async';
 import 'dart:io';
+import 'dart:ui' as ui;
 
 import 'package:clipboard/clipboard.dart';
 import 'package:ente_auth/core/configuration.dart';
@@ -159,9 +160,21 @@ class _CodeWidgetState extends State<CodeWidget> {
                     mainAxisAlignment: MainAxisAlignment.center,
                     children: [
                       if (widget.code.isPinned)
-                        SvgPicture.asset("assets/svg/pin-active.svg")
+                        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"),
+                        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,
@@ -276,7 +289,11 @@ class _CodeWidgetState extends State<CodeWidget> {
           Align(
             alignment: Alignment.topRight,
             child: CustomPaint(
-              painter: PinBgPainter(strokeColor: const Color(0xFFF9ECFF)),
+              painter: PinBgPainter(
+                color: Theme.of(context).brightness == Brightness.dark
+                    ? const Color(0xFF390C4F)
+                    : const Color(0xFFF9ECFF),
+              ),
               size: const Size(39, 39),
             ),
           ),
@@ -600,21 +617,18 @@ class _CodeWidgetState extends State<CodeWidget> {
 }
 
 class PinBgPainter extends CustomPainter {
-  final Color strokeColor;
+  final Color color;
   final PaintingStyle paintingStyle;
-  final double strokeWidth;
 
   PinBgPainter({
-    this.strokeColor = Colors.black,
-    this.strokeWidth = 3,
+    this.color = Colors.black,
     this.paintingStyle = PaintingStyle.fill,
   });
 
   @override
   void paint(Canvas canvas, Size size) {
     Paint paint = Paint()
-      ..color = strokeColor
-      ..strokeWidth = strokeWidth
+      ..color = color
       ..style = paintingStyle;
 
     canvas.drawPath(getTrianglePath(size.width, size.height), paint);
@@ -630,8 +644,7 @@ class PinBgPainter extends CustomPainter {
 
   @override
   bool shouldRepaint(PinBgPainter oldDelegate) {
-    return oldDelegate.strokeColor != strokeColor ||
-        oldDelegate.paintingStyle != paintingStyle ||
-        oldDelegate.strokeWidth != strokeWidth;
+    return oldDelegate.color != color ||
+        oldDelegate.paintingStyle != paintingStyle;
   }
 }

+ 40 - 4
auth/lib/ui/home_page.dart

@@ -60,6 +60,7 @@ class _HomePageState extends State<HomePage> {
   StreamSubscription<CodesUpdatedEvent>? _streamSubscription;
   StreamSubscription<TriggerLogoutEvent>? _triggerLogoutEvent;
   StreamSubscription<IconsChangedEvent>? _iconsChangedEvent;
+  String selectedTag = "";
 
   @override
   void initState() {
@@ -118,6 +119,11 @@ class _HomePageState extends State<HomePage> {
       for (final CodeState codeState in _codes!.allCodes) {
         if (codeState.error != null) continue;
 
+        if (selectedTag != "" &&
+            !codeState.code!.display.tags.contains(selectedTag)) {
+          continue;
+        }
+
         if (codeState.code!.issuer.toLowerCase().contains(val)) {
           issuerMatch.add(codeState);
         } else if (codeState.code!.account.toLowerCase().contains(val)) {
@@ -127,7 +133,15 @@ class _HomePageState extends State<HomePage> {
       _filteredCodes = issuerMatch;
       _filteredCodes.addAll(accountMatch);
     } else {
-      _filteredCodes = _codes?.allCodes ?? [];
+      _filteredCodes = _codes?.allCodes
+              .where(
+                (element) =>
+                    selectedTag == "" ||
+                    element.code != null &&
+                        element.code!.display.tags.contains(selectedTag),
+              )
+              .toList() ??
+          [];
     }
     if (mounted) {
       setState(() {});
@@ -178,6 +192,10 @@ 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) {
@@ -279,12 +297,30 @@ class _HomePageState extends State<HomePage> {
                 itemCount: _codes?.tags == null ? 0 : _codes!.tags.length + 1,
                 itemBuilder: (context, index) {
                   if (index == 0) {
-                    return const TagChip(
+                    return TagChip(
                       label: "All",
-                      isSelected: true,
+                      state: selectedTag == ""
+                          ? TagChipState.selected
+                          : TagChipState.unselected,
+                      onTap: () {
+                        selectedTag = "";
+                        setState(() {});
+                        _applyFilteringAndRefresh();
+                      },
                     );
                   }
-                  return TagChip(label: _codes!.tags[index - 1]);
+                  return TagChip(
+                    label: _codes!.tags[index - 1],
+                    action: TagChipAction.menu,
+                    state: selectedTag == _codes!.tags[index - 1]
+                        ? TagChipState.selected
+                        : TagChipState.unselected,
+                    onTap: () {
+                      selectedTag = _codes!.tags[index - 1];
+                      setState(() {});
+                      _applyFilteringAndRefresh();
+                    },
+                  );
                 },
               ),
             ),

+ 8 - 0
auth/pubspec.lock

@@ -721,6 +721,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "5.0.6"
+  gradient_borders:
+    dependency: "direct main"
+    description:
+      name: gradient_borders
+      sha256: "69eeaff519d145a4c6c213ada1abae386bcc8981a4970d923e478ce7ba19e309"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.0"
   graphs:
     dependency: transitive
     description:

+ 1 - 0
auth/pubspec.yaml

@@ -61,6 +61,7 @@ dependencies:
   flutter_svg: ^2.0.5
   fluttertoast: ^8.1.1
   google_nav_bar: ^5.0.5 #supported
+  gradient_borders: ^1.0.0
   http: ^1.1.0
   intl: ^0.18.0
   json_annotation: ^4.5.0