From ed2e44cc544aa13701471de031815318d682bdfc Mon Sep 17 00:00:00 2001
From: LittleSheep <littlesheep.code@hotmail.com>
Date: Sat, 9 Nov 2024 21:47:40 +0800
Subject: [PATCH] :sparkles: Create, edit, list publishers

---
 assets/translations/en-US.json               |  15 +-
 assets/translations/zh-CN.json               |  15 +-
 lib/providers/userinfo.dart                  |   5 +-
 lib/router.dart                              |  22 ++-
 lib/screens/account.dart                     |  13 +-
 lib/screens/account/publisher_edit.dart      | 166 +++++++++++++++++++
 lib/screens/account/publisher_new.dart       | 131 +++++++++++++++
 lib/screens/account/publishers.dart          | 129 ++++++++++++++
 lib/screens/auth/login.dart                  |   5 +-
 lib/screens/auth/register.dart               |   5 +-
 lib/theme.dart                               |   1 +
 lib/widgets/loading_indicator.dart           |  89 ++++++++++
 lib/widgets/navigation/app_destinations.dart |   6 +-
 13 files changed, 584 insertions(+), 18 deletions(-)
 create mode 100644 lib/screens/account/publisher_edit.dart
 create mode 100644 lib/screens/account/publisher_new.dart
 create mode 100644 lib/screens/account/publishers.dart
 create mode 100644 lib/widgets/loading_indicator.dart

diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json
index 64df944..0fac258 100644
--- a/assets/translations/en-US.json
+++ b/assets/translations/en-US.json
@@ -8,6 +8,9 @@
   "screenAuthLoginGreeting": "Welcome back",
   "screenAuthRegister": "Create an account",
   "screenAuthRegisterSubtitle": "Create a Solarpass account",
+  "screenAccountPublishers": "Publishers",
+  "screenAccountPublisherNew": "New Publisher",
+  "screenAccountPublisherEdit": "Edit Publisher",
   "dialogOkay": "Okay",
   "dialogCancel": "Cancel",
   "dialogConfirm": "Confirm",
@@ -21,10 +24,17 @@
   "errorRequestUnknown": "Unknown request error, maybe you want to take screenshot and report it to us.",
   "prev": "Next",
   "next": "Previous",
+  "edit": "Edit",
+  "apply": "Apply",
+  "create": "Create",
+  "preview": "Preview",
+  "loading": "Loading...",
   "fieldUsername": "Username",
   "fieldNickname": "Nickname",
   "fieldEmail": "Email address",
   "fieldPassword": "Password",
+  "fieldDescription": "Description",
+  "fieldUsernameCannotEditHint": "Username cannot be edited after created",
   "fieldUsernameLookupHint": "You can use username, phone number or email to login",
   "forgotPassword": "Forgot password",
   "loginPickFactor": "Pick a factor",
@@ -43,5 +53,8 @@
   "accountLogoutConfirmTitle": "Are you sure you want to logout?",
   "accountLogoutConfirm": "You will need to re-enter your account password, even if you have already done so. This is required to login again.",
   "accountPublishers": "Your publishers",
-  "accountPublishersSubtitle": "Manage your publish identities."
+  "accountPublishersSubtitle": "Manage your publish identities.",
+  "publishersNew": "New Publisher",
+  "publisherNewSubtitle": "Create a new publisher identity.",
+  "publisherSyncWithAccount": "Sync with account"
 }
diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json
index 74cbce6..e37e5ea 100644
--- a/assets/translations/zh-CN.json
+++ b/assets/translations/zh-CN.json
@@ -8,6 +8,9 @@
   "screenAuthLoginGreeting": "欢迎回来",
   "screenAuthRegister": "创建账号",
   "screenAuthRegisterSubtitle": "创建一个 Solarpass 账号",
+  "screenAccountPublishers": "发布者",
+  "screenAccountPublisherNew": "新建发布者",
+  "screenAccountPublisherEdit": "编辑发布者",
   "dialogOkay": "好的",
   "dialogCancel": "取消",
   "dialogConfirm": "确认",
@@ -19,13 +22,20 @@
   "errorRequestNotFound": "您正查找的资源无法被找到。",
   "errorRequestConnection": "网络连接错误,请检查您的网络状态或者检查我们的服务状态。",
   "errorRequestUnknown": "位置请求错误,您可能想将此对话框截图并发送给我们。",
+  "loading": "加载中…",
   "prev": "上一步",
   "next": "下一步",
+  "edit": "编辑",
+  "apply": "应用",
+  "create": "创建",
+  "preview": "预览",
   "fieldUsername": "用户名",
   "fieldNickname": "显示名",
   "fieldEmail": "电子邮箱地址",
   "fieldPassword": "密码",
+  "fieldUsernameCannotEditHint": "用户名在创建后无法修改",
   "fieldUsernameLookupHint": "支持用户名、电话号码或邮箱地址",
+  "fieldDescription": "简介",
   "forgotPassword": "忘记密码",
   "loginPickFactor": "选择方式验证",
   "loginMultiFactor": {
@@ -43,5 +53,8 @@
   "accountLogoutConfirmTitle": "您确定要退出登录吗?",
   "accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。",
   "accountPublishers": "你的发布者",
-  "accountPublishersSubtitle": "管理你的公共形象。"
+  "accountPublishersSubtitle": "管理你的公共形象。",
+  "publishersNew": "新发布者",
+  "publisherNewSubtitle": "创建一个新的公共身份。",
+  "publisherSyncWithAccount": "同步账户信息"
 }
diff --git a/lib/providers/userinfo.dart b/lib/providers/userinfo.dart
index 5c3d6f8..607ec52 100644
--- a/lib/providers/userinfo.dart
+++ b/lib/providers/userinfo.dart
@@ -28,11 +28,10 @@ class UserProvider extends ChangeNotifier {
   }
 
   Future<SnAccount?> refreshUser() async {
-    if (!isAuthorized) return null;
-
     final resp = await _sn.client.get('/cgi/id/users/me');
     final out = SnAccount.fromJson(resp.data);
 
+    isAuthorized = true;
     user = out;
     notifyListeners();
 
@@ -40,7 +39,7 @@ class UserProvider extends ChangeNotifier {
   }
 
   void logoutUser() async {
-    _sn.clearTokenPair();
+    await _sn.clearTokenPair();
     isAuthorized = false;
     user = null;
     notifyListeners();
diff --git a/lib/router.dart b/lib/router.dart
index fc956ae..fe1ab37 100644
--- a/lib/router.dart
+++ b/lib/router.dart
@@ -1,5 +1,8 @@
 import 'package:go_router/go_router.dart';
 import 'package:surface/screens/account.dart';
+import 'package:surface/screens/account/publisher_edit.dart';
+import 'package:surface/screens/account/publisher_new.dart';
+import 'package:surface/screens/account/publishers.dart';
 import 'package:surface/screens/auth/login.dart';
 import 'package:surface/screens/auth/register.dart';
 import 'package:surface/screens/explore.dart';
@@ -43,10 +46,27 @@ final appRouter = GoRouter(
           builder: (context, state) => const LoginScreen(),
         ),
         GoRoute(
-          path: '/auth.register',
+          path: '/auth/register',
           name: 'authRegister',
           builder: (context, state) => const RegisterScreen(),
         ),
+        GoRoute(
+          path: '/account/publishers',
+          name: 'accountPublishers',
+          builder: (context, state) => const PublisherScreen(),
+        ),
+        GoRoute(
+          path: '/account/publishers/new',
+          name: 'accountPublisherNew',
+          builder: (context, state) => const AccountPublisherNewScreen(),
+        ),
+        GoRoute(
+          path: '/account/publishers/edit/:name',
+          name: 'accountPublisherEdit',
+          builder: (context, state) => AccountPublisherEditScreen(
+            name: state.pathParameters['name']!,
+          ),
+        ),
       ],
     ),
   ],
diff --git a/lib/screens/account.dart b/lib/screens/account.dart
index 99914be..846c2c3 100644
--- a/lib/screens/account.dart
+++ b/lib/screens/account.dart
@@ -31,7 +31,7 @@ class AccountScreen extends StatelessWidget {
 }
 
 class _AuthorizedAccountScreen extends StatelessWidget {
-  const _AuthorizedAccountScreen({super.key});
+  const _AuthorizedAccountScreen();
 
   @override
   Widget build(BuildContext context) {
@@ -80,7 +80,9 @@ class _AuthorizedAccountScreen extends StatelessWidget {
           contentPadding: const EdgeInsets.symmetric(horizontal: 24),
           leading: const Icon(Symbols.face),
           trailing: const Icon(Icons.chevron_right),
-          onTap: () {},
+          onTap: () {
+            GoRouter.of(context).pushNamed('accountPublishers');
+          },
         ),
         ListTile(
           title: Text('accountLogout').tr(),
@@ -105,7 +107,7 @@ class _AuthorizedAccountScreen extends StatelessWidget {
 }
 
 class _UnauthorizedAccountScreen extends StatelessWidget {
-  const _UnauthorizedAccountScreen({super.key});
+  const _UnauthorizedAccountScreen();
 
   @override
   Widget build(BuildContext context) {
@@ -117,7 +119,10 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
             child: Column(
               crossAxisAlignment: CrossAxisAlignment.start,
               children: [
-                Icon(Symbols.waving_hand, size: 32),
+                const CircleAvatar(
+                  radius: 28,
+                  child: Icon(Symbols.waving_hand, size: 28),
+                ),
                 const Gap(8),
                 Text('accountIntroTitle')
                     .tr()
diff --git a/lib/screens/account/publisher_edit.dart b/lib/screens/account/publisher_edit.dart
new file mode 100644
index 0000000..1c0095b
--- /dev/null
+++ b/lib/screens/account/publisher_edit.dart
@@ -0,0 +1,166 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:gap/gap.dart';
+import 'package:material_symbols_icons/symbols.dart';
+import 'package:provider/provider.dart';
+import 'package:styled_widget/styled_widget.dart';
+import 'package:surface/providers/sn_network.dart';
+import 'package:surface/providers/userinfo.dart';
+import 'package:surface/types/post.dart';
+import 'package:surface/widgets/dialog.dart';
+import 'package:surface/widgets/loading_indicator.dart';
+import 'package:surface/widgets/navigation/app_scaffold.dart';
+
+class AccountPublisherEditScreen extends StatefulWidget {
+  final String name;
+  const AccountPublisherEditScreen({super.key, required this.name});
+
+  @override
+  State<AccountPublisherEditScreen> createState() =>
+      _AccountPublisherEditScreenState();
+}
+
+class _AccountPublisherEditScreenState
+    extends State<AccountPublisherEditScreen> {
+  bool _isBusy = false;
+
+  SnPublisher? _publisher;
+
+  String? _avatar;
+  String? _banner;
+
+  final TextEditingController _nickController = TextEditingController();
+  final TextEditingController _nameController = TextEditingController();
+  final TextEditingController _descriptionController = TextEditingController();
+
+  Future<void> _fetchPublisher() async {
+    final sn = context.read<SnNetworkProvider>();
+    final ua = context.read<UserProvider>();
+    if (!ua.isAuthorized) return;
+
+    setState(() => _isBusy = true);
+
+    try {
+      final resp = await sn.client.get('/cgi/co/publishers/${widget.name}');
+      _publisher = SnPublisher.fromJson(resp.data);
+      _syncWidget();
+    } catch (err) {
+      context.showErrorDialog(err);
+    } finally {
+      setState(() => _isBusy = false);
+    }
+  }
+
+  Future<void> _performAction() async {
+    final sn = context.read<SnNetworkProvider>();
+    final ua = context.read<UserProvider>();
+    if (!ua.isAuthorized) return;
+
+    setState(() => _isBusy = true);
+
+    try {
+      await sn.client.put('/cgi/co/publishers/${widget.name}', data: {
+        'avatar': _avatar,
+        'banner': _banner,
+        'nick': _nickController.text,
+        'name': _nameController.text,
+        'description': _descriptionController.text,
+      });
+      Navigator.pop(context, true);
+    } catch (err) {
+      context.showErrorDialog(err);
+    } finally {
+      setState(() => _isBusy = false);
+    }
+  }
+
+  void _syncWidget() {
+    _avatar = _publisher!.avatar;
+    _banner = _publisher!.banner;
+    _nickController.text = _publisher!.nick;
+    _nameController.text = _publisher!.name;
+    _descriptionController.text = _publisher!.description;
+  }
+
+  void _syncWithAccount() {
+    final ua = context.read<UserProvider>();
+    _avatar = ua.user!.avatar;
+    _banner = ua.user!.banner;
+    _nickController.text = ua.user!.nick;
+    _nameController.text = ua.user!.name;
+    _descriptionController.text = ua.user!.description;
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    _fetchPublisher();
+  }
+
+  @override
+  void dispose() {
+    _nickController.dispose();
+    _nameController.dispose();
+    _descriptionController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return AppScaffold(
+      body: SingleChildScrollView(
+        child: Column(
+          children: [
+            LoadingIndicator(isActive: _isBusy),
+            TextField(
+              controller: _nameController,
+              readOnly: true,
+              decoration: InputDecoration(
+                labelText: 'fieldUsername'.tr(),
+                helperText: 'fieldUsernameCannotEditHint'.tr(),
+              ),
+              onTapOutside: (_) =>
+                  FocusManager.instance.primaryFocus?.unfocus(),
+            ),
+            const Gap(4),
+            TextField(
+              controller: _nickController,
+              decoration: InputDecoration(
+                labelText: 'fieldNickname'.tr(),
+              ),
+              onTapOutside: (_) =>
+                  FocusManager.instance.primaryFocus?.unfocus(),
+            ),
+            const Gap(4),
+            TextField(
+              controller: _descriptionController,
+              maxLines: 3,
+              minLines: 3,
+              decoration: InputDecoration(
+                labelText: 'fieldDescription'.tr(),
+              ),
+              onTapOutside: (_) =>
+                  FocusManager.instance.primaryFocus?.unfocus(),
+            ),
+            const Gap(12),
+            Row(
+              mainAxisAlignment: MainAxisAlignment.spaceBetween,
+              children: [
+                TextButton.icon(
+                  onPressed: _syncWithAccount,
+                  label: Text('publisherSyncWithAccount').tr(),
+                  icon: const Icon(Symbols.sync),
+                ),
+                ElevatedButton.icon(
+                  onPressed: _isBusy ? null : _performAction,
+                  label: Text('apply').tr(),
+                  icon: const Icon(Symbols.save),
+                ),
+              ],
+            )
+          ],
+        ).padding(horizontal: 16, vertical: 12),
+      ),
+    );
+  }
+}
diff --git a/lib/screens/account/publisher_new.dart b/lib/screens/account/publisher_new.dart
new file mode 100644
index 0000000..9d52db2
--- /dev/null
+++ b/lib/screens/account/publisher_new.dart
@@ -0,0 +1,131 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:gap/gap.dart';
+import 'package:material_symbols_icons/symbols.dart';
+import 'package:provider/provider.dart';
+import 'package:styled_widget/styled_widget.dart';
+import 'package:surface/providers/sn_network.dart';
+import 'package:surface/providers/userinfo.dart';
+import 'package:surface/widgets/account/account_image.dart';
+import 'package:surface/widgets/dialog.dart';
+import 'package:surface/widgets/navigation/app_scaffold.dart';
+
+class AccountPublisherNewScreen extends StatefulWidget {
+  const AccountPublisherNewScreen({super.key});
+
+  @override
+  State<AccountPublisherNewScreen> createState() =>
+      _AccountPublisherNewScreenState();
+}
+
+class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> {
+  String mode = 'personal';
+
+  @override
+  Widget build(BuildContext context) {
+    return AppScaffold(
+      body: SingleChildScrollView(
+        child: Column(
+          children: [
+            SizedBox(
+              width: double.infinity,
+              child: SegmentedButton<String>(
+                segments: const <ButtonSegment<String>>[
+                  ButtonSegment<String>(
+                      value: 'personal',
+                      label: Text('Personal'),
+                      icon: Icon(Symbols.account_box)),
+                  ButtonSegment<String>(
+                      value: 'organization',
+                      label: Text('Organization'),
+                      icon: Icon(Symbols.group)),
+                ],
+                selected: {mode},
+                onSelectionChanged: (Set<String> newSelection) {
+                  setState(() => mode = newSelection.first);
+                },
+              ),
+            ),
+            switch (mode) {
+              'personal' => const _PublisherNewPersonal(),
+              _ => const Placeholder(),
+            },
+          ],
+        ).padding(horizontal: 16, vertical: 12),
+      ),
+    );
+  }
+}
+
+class _PublisherNewPersonal extends StatefulWidget {
+  const _PublisherNewPersonal();
+
+  @override
+  State<_PublisherNewPersonal> createState() => _PublisherNewPersonalState();
+}
+
+class _PublisherNewPersonalState extends State<_PublisherNewPersonal> {
+  bool _isBusy = false;
+
+  void _performAction() async {
+    final sn = context.read<SnNetworkProvider>();
+    final ua = context.read<UserProvider>();
+    if (!ua.isAuthorized) return;
+
+    setState(() => _isBusy = true);
+
+    try {
+      await sn.client.post('/cgi/co/publishers/personal');
+      Navigator.pop(context, true);
+    } catch (err) {
+      context.showErrorDialog(err);
+    } finally {
+      setState(() => _isBusy = false);
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final ua = context.watch<UserProvider>();
+
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Text('preview')
+            .tr()
+            .textStyle(Theme.of(context).textTheme.titleMedium!)
+            .padding(horizontal: 16, vertical: 4),
+        Card(
+          child: SizedBox(
+            width: double.infinity,
+            child: Row(
+              children: [
+                AccountImage(content: ua.user!.avatar, radius: 24),
+                const Gap(16),
+                Column(
+                  crossAxisAlignment: CrossAxisAlignment.baseline,
+                  textBaseline: TextBaseline.alphabetic,
+                  children: [
+                    Text(ua.user!.nick)
+                        .textStyle(Theme.of(context).textTheme.titleLarge!),
+                    const Gap(4),
+                    Text('@${ua.user!.name}')
+                        .textStyle(Theme.of(context).textTheme.bodySmall!),
+                  ],
+                ),
+              ],
+            ),
+          ).padding(all: 16),
+        ),
+        SizedBox(
+          width: double.infinity,
+          child: ElevatedButton.icon(
+            onPressed: _isBusy ? null : _performAction,
+            icon: const Icon(Icons.add),
+            label: Text('create').tr(),
+          ),
+        ).padding(horizontal: 2),
+      ],
+    );
+  }
+}
diff --git a/lib/screens/account/publishers.dart b/lib/screens/account/publishers.dart
new file mode 100644
index 0000000..7ebb1f0
--- /dev/null
+++ b/lib/screens/account/publishers.dart
@@ -0,0 +1,129 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:gap/gap.dart';
+import 'package:go_router/go_router.dart';
+import 'package:material_symbols_icons/symbols.dart';
+import 'package:provider/provider.dart';
+import 'package:surface/providers/sn_network.dart';
+import 'package:surface/providers/userinfo.dart';
+import 'package:surface/types/post.dart';
+import 'package:surface/widgets/account/account_image.dart';
+import 'package:surface/widgets/dialog.dart';
+import 'package:surface/widgets/loading_indicator.dart';
+import 'package:surface/widgets/navigation/app_scaffold.dart';
+
+class PublisherScreen extends StatefulWidget {
+  const PublisherScreen({super.key});
+
+  @override
+  State<PublisherScreen> createState() => _PublisherScreenState();
+}
+
+class _PublisherScreenState extends State<PublisherScreen> {
+  bool _isBusy = false;
+
+  final List<SnPublisher> _publishers = List<SnPublisher>.empty(growable: true);
+
+  Future<void> _fetchPublishers() async {
+    final sn = context.read<SnNetworkProvider>();
+    final ua = context.read<UserProvider>();
+    if (!ua.isAuthorized) return;
+
+    setState(() => _isBusy = true);
+
+    try {
+      final resp = await sn.client.get('/cgi/co/publishers');
+      final List<SnPublisher> out = List<SnPublisher>.from(
+          resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
+
+      if (!mounted) return;
+
+      _publishers.addAll(out);
+    } catch (err) {
+      context.showErrorDialog(err);
+    } finally {
+      setState(() => _isBusy = false);
+    }
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    _fetchPublishers();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return AppScaffold(
+      body: Column(
+        children: [
+          ListTile(
+            title: Text('publishersNew').tr(),
+            subtitle: Text('publisherNewSubtitle').tr(),
+            contentPadding: const EdgeInsets.symmetric(horizontal: 24),
+            leading: const Icon(Symbols.add_circle),
+            onTap: () {
+              GoRouter.of(context)
+                  .pushNamed('accountPublisherNew')
+                  .then((value) {
+                if (value == true) {
+                  _publishers.clear();
+                  _fetchPublishers();
+                }
+              });
+            },
+          ),
+          const Divider(height: 1),
+          LoadingIndicator(isActive: _isBusy),
+          Expanded(
+            child: RefreshIndicator(
+              onRefresh: () {
+                _publishers.clear();
+                return _fetchPublishers();
+              },
+              child: ListView.builder(
+                itemCount: _publishers.length,
+                itemBuilder: (context, idx) {
+                  final publisher = _publishers[idx];
+                  return ListTile(
+                    title: Text(publisher.nick),
+                    subtitle: Text('@${publisher.name}'),
+                    contentPadding: const EdgeInsets.symmetric(horizontal: 16),
+                    leading: AccountImage(content: publisher.avatar),
+                    trailing: PopupMenuButton(
+                      itemBuilder: (BuildContext context) => [
+                        PopupMenuItem(
+                          child: Row(
+                            children: [
+                              const Icon(Icons.edit),
+                              const Gap(16),
+                              Text('edit').tr(),
+                            ],
+                          ),
+                          onTap: () {
+                            GoRouter.of(context).pushNamed(
+                              'accountPublisherEdit',
+                              pathParameters: {
+                                'name': publisher.name,
+                              },
+                            ).then((value) {
+                              if (value == true) {
+                                _publishers.clear();
+                                _fetchPublishers();
+                              }
+                            });
+                          },
+                        ),
+                      ],
+                    ),
+                  );
+                },
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/lib/screens/auth/login.dart b/lib/screens/auth/login.dart
index b34f73a..ef0f51a 100644
--- a/lib/screens/auth/login.dart
+++ b/lib/screens/auth/login.dart
@@ -158,8 +158,9 @@ class _LoginCheckScreenState extends State<_LoginCheckScreen> {
       context.showSnackbar('loginSuccess'.tr(args: [
         '@${userinfo!.name} (${userinfo.nick})',
       ]));
-
-      Navigator.pop(context);
+      await Future.delayed(const Duration(milliseconds: 1850), () {
+        Navigator.pop(context);
+      });
     } catch (err) {
       context.showErrorDialog(err);
       return;
diff --git a/lib/screens/auth/register.dart b/lib/screens/auth/register.dart
index bc70375..09ac1df 100644
--- a/lib/screens/auth/register.dart
+++ b/lib/screens/auth/register.dart
@@ -1,6 +1,7 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:gap/gap.dart';
+import 'package:go_router/go_router.dart';
 import 'package:material_symbols_icons/symbols.dart';
 import 'package:provider/provider.dart';
 import 'package:styled_widget/styled_widget.dart';
@@ -43,9 +44,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
 
       if (!mounted) return;
 
-      // TODO make celebration here
-      // ignore: use_build_context_synchronously
-      Navigator.pop(context);
+      GoRouter.of(context).replaceNamed("authLogin");
     } catch (err) {
       context.showErrorDialog(err);
     }
diff --git a/lib/theme.dart b/lib/theme.dart
index 2818356..0086af2 100644
--- a/lib/theme.dart
+++ b/lib/theme.dart
@@ -21,5 +21,6 @@ ThemeData createAppTheme() {
       seedColor: Colors.indigo,
       brightness: Brightness.light,
     ),
+    iconTheme: const IconThemeData(fill: 0, weight: 400, opticalSize: 20),
   );
 }
diff --git a/lib/widgets/loading_indicator.dart b/lib/widgets/loading_indicator.dart
new file mode 100644
index 0000000..c2f0499
--- /dev/null
+++ b/lib/widgets/loading_indicator.dart
@@ -0,0 +1,89 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:gap/gap.dart';
+
+class LoadingIndicator extends StatefulWidget {
+  final bool isActive;
+  final Color? backgroundColor;
+
+  const LoadingIndicator({
+    super.key,
+    this.isActive = true,
+    this.backgroundColor,
+  });
+
+  @override
+  State<LoadingIndicator> createState() => _LoadingIndicatorState();
+}
+
+class _LoadingIndicatorState extends State<LoadingIndicator>
+    with SingleTickerProviderStateMixin {
+  late AnimationController _controller;
+  late Animation<double> _animation;
+
+  @override
+  void initState() {
+    super.initState();
+
+    _controller = AnimationController(
+      vsync: this,
+      duration: const Duration(milliseconds: 300),
+    );
+
+    _animation = CurvedAnimation(
+      parent: _controller,
+      curve: Curves.easeInOut,
+    );
+
+    if (widget.isActive) {
+      _controller.forward();
+    } else {
+      _controller.reverse();
+    }
+  }
+
+  @override
+  void didUpdateWidget(covariant LoadingIndicator oldWidget) {
+    super.didUpdateWidget(oldWidget);
+
+    if (widget.isActive != oldWidget.isActive) {
+      if (widget.isActive) {
+        _controller.forward();
+      } else {
+        _controller.reverse();
+      }
+    }
+  }
+
+  @override
+  void dispose() {
+    _controller.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return SizeTransition(
+      sizeFactor: _animation,
+      axisAlignment: -1, // Align animation from the top
+      child: Container(
+        padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
+        child: widget.isActive
+            ? Row(
+                mainAxisAlignment: MainAxisAlignment.center,
+                crossAxisAlignment: CrossAxisAlignment.center,
+                children: [
+                  const SizedBox(
+                    height: 16,
+                    width: 16,
+                    child: CircularProgressIndicator(strokeWidth: 2.5),
+                  ),
+                  const Gap(16),
+                  Text('loading').tr(),
+                ],
+              )
+            : const SizedBox.shrink(),
+      ),
+    );
+  }
+}
diff --git a/lib/widgets/navigation/app_destinations.dart b/lib/widgets/navigation/app_destinations.dart
index f137daf..01e371b 100644
--- a/lib/widgets/navigation/app_destinations.dart
+++ b/lib/widgets/navigation/app_destinations.dart
@@ -16,17 +16,17 @@ class AppNavDestination {
 
 List<AppNavDestination> appDestinations = [
   AppNavDestination(
-    icon: Icon(Symbols.home),
+    icon: Icon(Symbols.home, weight: 400, opticalSize: 20),
     screen: 'home',
     label: tr('screenHome'),
   ),
   AppNavDestination(
-    icon: Icon(Symbols.explore),
+    icon: Icon(Symbols.explore, weight: 400, opticalSize: 20),
     screen: 'explore',
     label: tr('screenExplore'),
   ),
   AppNavDestination(
-    icon: Icon(Symbols.account_circle),
+    icon: Icon(Symbols.account_circle, weight: 400, opticalSize: 20),
     screen: 'account',
     label: tr('screenAccount'),
   ),