Compare commits
	
		
			2 Commits
		
	
	
		
			2.2.2+57
			...
			3395f3dbd0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 3395f3dbd0 | |||
| d258ba776e | 
@@ -17,6 +17,7 @@
 | 
			
		||||
  "screenAccountProfileEdit": "Edit Profile",
 | 
			
		||||
  "screenAbuseReport": "Abuse Reports",
 | 
			
		||||
  "screenSettings": "Settings",
 | 
			
		||||
  "screenAccountSettings": "Account Settings",
 | 
			
		||||
  "screenNews": "News",
 | 
			
		||||
  "screenAlbum": "Album",
 | 
			
		||||
  "screenChat": "Chat",
 | 
			
		||||
@@ -28,6 +29,7 @@
 | 
			
		||||
  "screenNotification": "Notification",
 | 
			
		||||
  "screenPostSearch": "Search Posts",
 | 
			
		||||
  "screenFriend": "Friends",
 | 
			
		||||
  "screenFactorSettings": "Auth Factors",
 | 
			
		||||
  "dialogOkay": "Okay",
 | 
			
		||||
  "dialogCancel": "Cancel",
 | 
			
		||||
  "dialogConfirm": "Confirm",
 | 
			
		||||
@@ -105,7 +107,15 @@
 | 
			
		||||
  "loginEnterPassword": "Enter the code",
 | 
			
		||||
  "loginSuccess": "Logged in as {}",
 | 
			
		||||
  "authFactorPassword": "Password",
 | 
			
		||||
  "authFactorPasswordDescription": "The password you set when you registered.",
 | 
			
		||||
  "authFactorEmail": "Email verification code",
 | 
			
		||||
  "authFactorEmailDescription": "An one-time code sent to the email address you set when you registered.",
 | 
			
		||||
  "authFactorTOTP": "Time-based OTP",
 | 
			
		||||
  "authFactorTOTPDescription": "A one-time code generated by a TOTP authenticator such as Google Authenticator or Authy.",
 | 
			
		||||
  "authFactorInAppNotify": "In-app notification",
 | 
			
		||||
  "authFactorInAppNotifyDescription": "A one-time code sent via in-app notification.",
 | 
			
		||||
  "authFactorAdd": "Add a factor",
 | 
			
		||||
  "authFactorAddSubtitle": "Provide another way to login your account.",
 | 
			
		||||
  "accountIntroTitle": "Hello there!",
 | 
			
		||||
  "accountIntroSubtitle": "Pick an option below to get started.",
 | 
			
		||||
  "accountLogout": "Logout",
 | 
			
		||||
@@ -114,8 +124,12 @@
 | 
			
		||||
  "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.",
 | 
			
		||||
  "accountSettings": "Account Settings",
 | 
			
		||||
  "accountSettingsSubtitle": "Manage your account and make it yours.",
 | 
			
		||||
  "accountProfileEdit": "Edit your profile",
 | 
			
		||||
  "accountProfileEditSubtitle": "Make your Solarpass account more looks like you.",
 | 
			
		||||
  "factorSettings": "Auth Factors",
 | 
			
		||||
  "factorSettingsSubtitle": "Manage your authentication factors.",
 | 
			
		||||
  "accountProfileEditApplied": "Profile modification applied.",
 | 
			
		||||
  "publishersNew": "New Publisher",
 | 
			
		||||
  "publisherNewSubtitle": "Create a new publisher identity.",
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@
 | 
			
		||||
  "screenAccountProfileEdit": "编辑资料",
 | 
			
		||||
  "screenAbuseReport": "滥用检举",
 | 
			
		||||
  "screenSettings": "设置",
 | 
			
		||||
  "screenAccountSettings": "账号设置",
 | 
			
		||||
  "screenNews": "新闻",
 | 
			
		||||
  "screenAlbum": "相册",
 | 
			
		||||
  "screenChat": "聊天",
 | 
			
		||||
@@ -89,7 +90,15 @@
 | 
			
		||||
  "loginEnterPassword": "验证代码",
 | 
			
		||||
  "loginSuccess": "登录为 {}",
 | 
			
		||||
  "authFactorPassword": "密码",
 | 
			
		||||
  "authFactorPasswordDescription": "注册时选择设置的密码。",
 | 
			
		||||
  "authFactorEmail": "电邮一次性验证码",
 | 
			
		||||
  "authFactorEmailDescription": "由我们生成并发送到绑定的的电子邮箱的一次性验证码。",
 | 
			
		||||
  "authFactorTOTP": "时序验证码",
 | 
			
		||||
  "authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等验证器生成的一次性验证码。",
 | 
			
		||||
  "authFactorInAppNotify": "应用内通知验证码",
 | 
			
		||||
  "authFactorInAppNotifyDescription": "通过站内通知推送的一次性验证码。",
 | 
			
		||||
  "authFactorAdd": "添加新验证因子",
 | 
			
		||||
  "authFactorAddSubtitle": "给你的帐户登陆时提供另一个方案。",
 | 
			
		||||
  "accountIntroTitle": "喜欢您来!",
 | 
			
		||||
  "accountIntroSubtitle": "登陆以探索更广大的世界。",
 | 
			
		||||
  "accountLogout": "退出登录",
 | 
			
		||||
@@ -98,8 +107,12 @@
 | 
			
		||||
  "accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。",
 | 
			
		||||
  "accountPublishers": "你的发布者",
 | 
			
		||||
  "accountPublishersSubtitle": "管理你的公共形象。",
 | 
			
		||||
  "accountSettings": "帐号设置",
 | 
			
		||||
  "accountSettingsSubtitle": "管理你的帐号并让它更好的服务你。",
 | 
			
		||||
  "accountProfileEdit": "编辑资料",
 | 
			
		||||
  "accountProfileEditSubtitle": "使你的 Solarpass 账户更像你。",
 | 
			
		||||
  "factorSettings": "验证因子",
 | 
			
		||||
  "factorSettingsSubtitle": "管理你的登陆验证方式。",
 | 
			
		||||
  "accountProfileEditApplied": "个人资料修改已被应用。",
 | 
			
		||||
  "publishersNew": "新发布者",
 | 
			
		||||
  "publisherNewSubtitle": "创建一个新的公共身份。",
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:surface/screens/abuse_report.dart';
 | 
			
		||||
import 'package:surface/screens/account.dart';
 | 
			
		||||
import 'package:surface/screens/account/account_settings.dart';
 | 
			
		||||
import 'package:surface/screens/account/factor_settings.dart';
 | 
			
		||||
import 'package:surface/screens/account/profile_page.dart';
 | 
			
		||||
import 'package:surface/screens/account/profile_edit.dart';
 | 
			
		||||
import 'package:surface/screens/account/publishers/publisher_edit.dart';
 | 
			
		||||
@@ -96,11 +98,47 @@ final _appRoutes = [
 | 
			
		||||
      ),
 | 
			
		||||
    ],
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/account',
 | 
			
		||||
    name: 'account',
 | 
			
		||||
    builder: (context, state) => const AccountScreen(),
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(path: '/account', name: 'account', builder: (context, state) => const AccountScreen(), routes: [
 | 
			
		||||
    GoRoute(
 | 
			
		||||
      path: '/settings',
 | 
			
		||||
      name: 'accountSettings',
 | 
			
		||||
      builder: (context, state) => AccountSettingsScreen(),
 | 
			
		||||
    ),
 | 
			
		||||
    GoRoute(
 | 
			
		||||
      path: '/settings/factors',
 | 
			
		||||
      name: 'factorSettings',
 | 
			
		||||
      builder: (context, state) => FactorSettingsScreen(),
 | 
			
		||||
    ),
 | 
			
		||||
    GoRoute(
 | 
			
		||||
      path: '/profile/edit',
 | 
			
		||||
      name: 'accountProfileEdit',
 | 
			
		||||
      builder: (context, state) => ProfileEditScreen(),
 | 
			
		||||
    ),
 | 
			
		||||
    GoRoute(
 | 
			
		||||
      path: '/publishers',
 | 
			
		||||
      name: 'accountPublishers',
 | 
			
		||||
      builder: (context, state) => PublisherScreen(),
 | 
			
		||||
    ),
 | 
			
		||||
    GoRoute(
 | 
			
		||||
      path: '/publishers/new',
 | 
			
		||||
      name: 'accountPublisherNew',
 | 
			
		||||
      builder: (context, state) => AccountPublisherNewScreen(),
 | 
			
		||||
    ),
 | 
			
		||||
    GoRoute(
 | 
			
		||||
      path: '/publishers/edit/:name',
 | 
			
		||||
      name: 'accountPublisherEdit',
 | 
			
		||||
      builder: (context, state) => AccountPublisherEditScreen(
 | 
			
		||||
        name: state.pathParameters['name']!,
 | 
			
		||||
      ),
 | 
			
		||||
    ),
 | 
			
		||||
    GoRoute(
 | 
			
		||||
      path: '/:name',
 | 
			
		||||
      name: 'accountProfilePage',
 | 
			
		||||
      pageBuilder: (context, state) => NoTransitionPage(
 | 
			
		||||
        child: UserScreen(name: state.pathParameters['name']!),
 | 
			
		||||
      ),
 | 
			
		||||
    ),
 | 
			
		||||
  ]),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/chat',
 | 
			
		||||
    name: 'chat',
 | 
			
		||||
@@ -161,20 +199,15 @@ final _appRoutes = [
 | 
			
		||||
      ),
 | 
			
		||||
    ],
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/news',
 | 
			
		||||
    name: 'news',
 | 
			
		||||
    builder: (context, state) => const NewsScreen(),
 | 
			
		||||
    routes: [
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/:hash',
 | 
			
		||||
        name: 'newsDetail',
 | 
			
		||||
        builder: (context, state) => NewsDetailScreen(
 | 
			
		||||
          hash: state.pathParameters['hash']!,
 | 
			
		||||
        ),
 | 
			
		||||
  GoRoute(path: '/news', name: 'news', builder: (context, state) => const NewsScreen(), routes: [
 | 
			
		||||
    GoRoute(
 | 
			
		||||
      path: '/:hash',
 | 
			
		||||
      name: 'newsDetail',
 | 
			
		||||
      builder: (context, state) => NewsDetailScreen(
 | 
			
		||||
        hash: state.pathParameters['hash']!,
 | 
			
		||||
      ),
 | 
			
		||||
    ]
 | 
			
		||||
  ),
 | 
			
		||||
    ),
 | 
			
		||||
  ]),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/album',
 | 
			
		||||
    name: 'album',
 | 
			
		||||
@@ -205,35 +238,6 @@ final _appRoutes = [
 | 
			
		||||
    name: 'abuseReport',
 | 
			
		||||
    builder: (context, state) => AbuseReportScreen(),
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/account/profile/edit',
 | 
			
		||||
    name: 'accountProfileEdit',
 | 
			
		||||
    builder: (context, state) => ProfileEditScreen(),
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/account/publishers',
 | 
			
		||||
    name: 'accountPublishers',
 | 
			
		||||
    builder: (context, state) => PublisherScreen(),
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/account/publishers/new',
 | 
			
		||||
    name: 'accountPublisherNew',
 | 
			
		||||
    builder: (context, state) => AccountPublisherNewScreen(),
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/account/publishers/edit/:name',
 | 
			
		||||
    name: 'accountPublisherEdit',
 | 
			
		||||
    builder: (context, state) => AccountPublisherEditScreen(
 | 
			
		||||
      name: state.pathParameters['name']!,
 | 
			
		||||
    ),
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/account/:name',
 | 
			
		||||
    name: 'accountProfilePage',
 | 
			
		||||
    pageBuilder: (context, state) => NoTransitionPage(
 | 
			
		||||
      child: UserScreen(name: state.pathParameters['name']!),
 | 
			
		||||
    ),
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/settings',
 | 
			
		||||
    name: 'settings',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
import 'dart:ui';
 | 
			
		||||
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
@@ -13,6 +15,7 @@ import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:surface/widgets/universal_image.dart';
 | 
			
		||||
 | 
			
		||||
class AccountScreen extends StatelessWidget {
 | 
			
		||||
  const AccountScreen({super.key});
 | 
			
		||||
@@ -20,11 +23,39 @@ class AccountScreen extends StatelessWidget {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final ua = context.watch<UserProvider>();
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: AutoAppBarLeading(),
 | 
			
		||||
        title: Text("screenAccount").tr(),
 | 
			
		||||
        flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
 | 
			
		||||
            ? Stack(
 | 
			
		||||
                fit: StackFit.expand,
 | 
			
		||||
                children: [
 | 
			
		||||
                  AutoResizeUniversalImage(sn.getAttachmentUrl(ua.user!.banner), fit: BoxFit.cover),
 | 
			
		||||
                  Positioned(
 | 
			
		||||
                    top: 0,
 | 
			
		||||
                    left: 0,
 | 
			
		||||
                    right: 0,
 | 
			
		||||
                    height: 56 + MediaQuery.of(context).padding.top,
 | 
			
		||||
                    child: ClipRect(
 | 
			
		||||
                      child: BackdropFilter(
 | 
			
		||||
                        filter: ImageFilter.blur(
 | 
			
		||||
                          sigmaX: 10,
 | 
			
		||||
                          sigmaY: 10,
 | 
			
		||||
                        ),
 | 
			
		||||
                        child: Container(
 | 
			
		||||
                          color: Colors.black.withOpacity(
 | 
			
		||||
                            clampDouble(10 * 0.1, 0, 0.5),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
              )
 | 
			
		||||
            : null,
 | 
			
		||||
        actions: [
 | 
			
		||||
          IconButton(
 | 
			
		||||
            icon: const Icon(Symbols.settings, fill: 1),
 | 
			
		||||
@@ -83,16 +114,6 @@ class _AuthorizedAccountScreen extends StatelessWidget {
 | 
			
		||||
            );
 | 
			
		||||
          }).padding(all: 20),
 | 
			
		||||
        ).padding(horizontal: 8, top: 16, bottom: 4),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text('accountProfileEdit').tr(),
 | 
			
		||||
          subtitle: Text('accountProfileEditSubtitle').tr(),
 | 
			
		||||
          contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
          leading: const Icon(Symbols.contact_page),
 | 
			
		||||
          trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            GoRouter.of(context).pushNamed('accountProfileEdit');
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text('accountPublishers').tr(),
 | 
			
		||||
          subtitle: Text('accountPublishersSubtitle').tr(),
 | 
			
		||||
@@ -113,6 +134,26 @@ class _AuthorizedAccountScreen extends StatelessWidget {
 | 
			
		||||
            GoRouter.of(context).pushNamed('abuseReport');
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text('factorSettings').tr(),
 | 
			
		||||
          subtitle: Text('factorSettingsSubtitle').tr(),
 | 
			
		||||
          contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
          leading: const Icon(Symbols.lock),
 | 
			
		||||
          trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            GoRouter.of(context).pushNamed('factorSettings');
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text('accountSettings').tr(),
 | 
			
		||||
          subtitle: Text('accountSettingsSubtitle').tr(),
 | 
			
		||||
          contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
          leading: const Icon(Symbols.manage_accounts),
 | 
			
		||||
          trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            GoRouter.of(context).pushNamed('accountSettings');
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text('accountLogout').tr(),
 | 
			
		||||
          subtitle: Text('accountLogoutSubtitle').tr(),
 | 
			
		||||
@@ -134,33 +175,6 @@ class _AuthorizedAccountScreen extends StatelessWidget {
 | 
			
		||||
            await Hive.initFlutter();
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text('accountDeletion'.tr()),
 | 
			
		||||
          subtitle: Text('accountDeletionActionDescription'.tr()),
 | 
			
		||||
          contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
          leading: const Icon(Symbols.person_cancel),
 | 
			
		||||
          trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            context
 | 
			
		||||
                .showConfirmDialog(
 | 
			
		||||
              'accountDeletion'.tr(),
 | 
			
		||||
              'accountDeletionDescription'.tr(),
 | 
			
		||||
            )
 | 
			
		||||
                .then((value) {
 | 
			
		||||
              if (!value || !context.mounted) return;
 | 
			
		||||
              final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
              sn.client.post('/cgi/id/users/me/deletion').then((value) {
 | 
			
		||||
                if (context.mounted) {
 | 
			
		||||
                  context.showSnackbar('accountDeletionSubmitted'.tr());
 | 
			
		||||
                }
 | 
			
		||||
              }).catchError((err) {
 | 
			
		||||
                if (context.mounted) {
 | 
			
		||||
                  context.showErrorDialog(err);
 | 
			
		||||
                }
 | 
			
		||||
              });
 | 
			
		||||
            });
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										66
									
								
								lib/screens/account/account_settings.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								lib/screens/account/account_settings.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.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/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
 | 
			
		||||
class AccountSettingsScreen extends StatelessWidget {
 | 
			
		||||
  const AccountSettingsScreen({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: PageBackButton(),
 | 
			
		||||
        title: Text('screenAccountSettings').tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      body: SingleChildScrollView(
 | 
			
		||||
        child: Column(
 | 
			
		||||
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
          children: [
 | 
			
		||||
            ListTile(
 | 
			
		||||
              title: Text('accountProfileEdit').tr(),
 | 
			
		||||
              subtitle: Text('accountProfileEditSubtitle').tr(),
 | 
			
		||||
              contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
              leading: const Icon(Symbols.contact_page),
 | 
			
		||||
              trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
              onTap: () {
 | 
			
		||||
                GoRouter.of(context).pushNamed('accountProfileEdit');
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              title: Text('accountDeletion'.tr()),
 | 
			
		||||
              subtitle: Text('accountDeletionActionDescription'.tr()),
 | 
			
		||||
              contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
              leading: const Icon(Symbols.person_cancel),
 | 
			
		||||
              trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
              onTap: () {
 | 
			
		||||
                context
 | 
			
		||||
                    .showConfirmDialog(
 | 
			
		||||
                  'accountDeletion'.tr(),
 | 
			
		||||
                  'accountDeletionDescription'.tr(),
 | 
			
		||||
                )
 | 
			
		||||
                    .then((value) {
 | 
			
		||||
                  if (!value || !context.mounted) return;
 | 
			
		||||
                  final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
                  sn.client.post('/cgi/id/users/me/deletion').then((value) {
 | 
			
		||||
                    if (context.mounted) {
 | 
			
		||||
                      context.showSnackbar('accountDeletionSubmitted'.tr());
 | 
			
		||||
                    }
 | 
			
		||||
                  }).catchError((err) {
 | 
			
		||||
                    if (context.mounted) {
 | 
			
		||||
                      context.showErrorDialog(err);
 | 
			
		||||
                    }
 | 
			
		||||
                  });
 | 
			
		||||
                });
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										201
									
								
								lib/screens/account/factor_settings.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								lib/screens/account/factor_settings.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,201 @@
 | 
			
		||||
import 'package:dropdown_button2/dropdown_button2.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.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/types/auth.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
 | 
			
		||||
final Map<int, (String, String, IconData)> _kFactorTypes = {
 | 
			
		||||
  0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password),
 | 
			
		||||
  1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email),
 | 
			
		||||
  2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer),
 | 
			
		||||
  3: ('authFactorInAppNotify', 'authFactorInAppNotifyDescription', Symbols.notifications_active),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class FactorSettingsScreen extends StatefulWidget {
 | 
			
		||||
  const FactorSettingsScreen({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<FactorSettingsScreen> createState() => _FactorSettingsScreenState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
 | 
			
		||||
  List<SnAuthFactor>? _factors;
 | 
			
		||||
 | 
			
		||||
  Future<void> _fetchFactors() async {
 | 
			
		||||
    try {
 | 
			
		||||
      final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
      final resp = await sn.client.get('/cgi/id/users/me/factors');
 | 
			
		||||
      _factors = List<SnAuthFactor>.from(
 | 
			
		||||
        resp.data?.map((e) => SnAuthFactor.fromJson(e as Map<String, dynamic>)).toList() ?? [],
 | 
			
		||||
      );
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
    } finally {
 | 
			
		||||
      setState(() {});
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _fetchFactors();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: PageBackButton(),
 | 
			
		||||
        title: Text('screenFactorSettings').tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      body: Column(
 | 
			
		||||
        crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
        children: [
 | 
			
		||||
          LoadingIndicator(
 | 
			
		||||
            isActive: _factors == null,
 | 
			
		||||
          ),
 | 
			
		||||
          ListTile(
 | 
			
		||||
            title: Text('authFactorAdd').tr(),
 | 
			
		||||
            subtitle: Text('authFactorAddSubtitle').tr(),
 | 
			
		||||
            contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
            leading: const Icon(Symbols.add),
 | 
			
		||||
            trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
            onTap: () {
 | 
			
		||||
              showDialog(
 | 
			
		||||
                context: context,
 | 
			
		||||
                builder: (context) => _FactorNewDialog(
 | 
			
		||||
                  currentlyHave: _factors!,
 | 
			
		||||
                ),
 | 
			
		||||
              ).then((val) {
 | 
			
		||||
                if (val == true) _fetchFactors();
 | 
			
		||||
              });
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
          const Divider(height: 1),
 | 
			
		||||
          Expanded(
 | 
			
		||||
            child: MediaQuery.removePadding(
 | 
			
		||||
              context: context,
 | 
			
		||||
              removeTop: true,
 | 
			
		||||
              child: RefreshIndicator(
 | 
			
		||||
                onRefresh: _fetchFactors,
 | 
			
		||||
                child: ListView.builder(
 | 
			
		||||
                  itemCount: _factors?.length ?? 0,
 | 
			
		||||
                  itemBuilder: (context, idx) {
 | 
			
		||||
                    final ele = _factors![idx];
 | 
			
		||||
                    return ListTile(
 | 
			
		||||
                      title: Text(_kFactorTypes[ele.type]!.$1).tr(),
 | 
			
		||||
                      subtitle: Text(_kFactorTypes[ele.type]!.$2).tr(),
 | 
			
		||||
                      contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
                      leading: Icon(_kFactorTypes[ele.type]!.$3),
 | 
			
		||||
                    );
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _FactorNewDialog extends StatefulWidget {
 | 
			
		||||
  final List<SnAuthFactor> currentlyHave;
 | 
			
		||||
 | 
			
		||||
  const _FactorNewDialog({required this.currentlyHave});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<_FactorNewDialog> createState() => _FactorNewDialogState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _FactorNewDialogState extends State<_FactorNewDialog> {
 | 
			
		||||
  int? _factorType;
 | 
			
		||||
  bool _isBusy = false;
 | 
			
		||||
 | 
			
		||||
  Future<void> _submit() async {
 | 
			
		||||
    try {
 | 
			
		||||
      setState(() => _isBusy = true);
 | 
			
		||||
      final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
      final resp = await sn.client.post('/cgi/id/users/me/factors', data: {
 | 
			
		||||
        'type': _factorType,
 | 
			
		||||
      });
 | 
			
		||||
      // TODO show qrcode when creating totp factor
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      Navigator.of(context).pop(true);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
    } finally {
 | 
			
		||||
      setState(() => _isBusy = false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AlertDialog(
 | 
			
		||||
      title: Text('authFactorAdd').tr(),
 | 
			
		||||
      content: Column(
 | 
			
		||||
        mainAxisSize: MainAxisSize.min,
 | 
			
		||||
        children: [
 | 
			
		||||
          DropdownButtonHideUnderline(
 | 
			
		||||
            child: DropdownButton2<int>(
 | 
			
		||||
              hint: Text(
 | 
			
		||||
                'Select Item',
 | 
			
		||||
                style: TextStyle(
 | 
			
		||||
                  fontSize: 14,
 | 
			
		||||
                ),
 | 
			
		||||
                overflow: TextOverflow.ellipsis,
 | 
			
		||||
              ),
 | 
			
		||||
              value: _factorType,
 | 
			
		||||
              items: _kFactorTypes.entries.map(
 | 
			
		||||
                (ele) {
 | 
			
		||||
                  final contains = widget.currentlyHave.map((ele) => ele.type).contains(ele.key);
 | 
			
		||||
                  return DropdownMenuItem<int>(
 | 
			
		||||
                    enabled: !contains,
 | 
			
		||||
                    value: ele.key,
 | 
			
		||||
                    child: Text(
 | 
			
		||||
                      ele.value.$1.tr(),
 | 
			
		||||
                      style: const TextStyle(
 | 
			
		||||
                        fontSize: 14,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ).opacity(contains ? 0.75 : 1),
 | 
			
		||||
                  );
 | 
			
		||||
                },
 | 
			
		||||
              ).toList(),
 | 
			
		||||
              onChanged: (val) => setState(() {
 | 
			
		||||
                _factorType = val;
 | 
			
		||||
              }),
 | 
			
		||||
              buttonStyleData: ButtonStyleData(
 | 
			
		||||
                height: 50,
 | 
			
		||||
                padding: const EdgeInsets.only(left: 14, right: 14),
 | 
			
		||||
                decoration: BoxDecoration(
 | 
			
		||||
                  borderRadius: BorderRadius.circular(14),
 | 
			
		||||
                  border: Border.all(
 | 
			
		||||
                    color: Theme.of(context).dividerColor,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      actions: [
 | 
			
		||||
        TextButton(
 | 
			
		||||
          onPressed: _isBusy ? null : () => Navigator.of(context).pop(),
 | 
			
		||||
          child: Text('dialogCancel').tr(),
 | 
			
		||||
        ),
 | 
			
		||||
        TextButton(
 | 
			
		||||
          onPressed: _isBusy ? null : () => _submit(),
 | 
			
		||||
          child: Text('dialogConfirm').tr(),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -15,8 +15,8 @@ class SnAccount with _$SnAccount {
 | 
			
		||||
    required DateTime? deletedAt,
 | 
			
		||||
    required DateTime? confirmedAt,
 | 
			
		||||
    required List<SnAccountContact>? contacts,
 | 
			
		||||
    required String avatar,
 | 
			
		||||
    required String banner,
 | 
			
		||||
    @Default("") String avatar,
 | 
			
		||||
    @Default("") String banner,
 | 
			
		||||
    required String description,
 | 
			
		||||
    required String name,
 | 
			
		||||
    required String nick,
 | 
			
		||||
 
 | 
			
		||||
@@ -367,8 +367,8 @@ class _$SnAccountImpl extends _SnAccount {
 | 
			
		||||
      required this.deletedAt,
 | 
			
		||||
      required this.confirmedAt,
 | 
			
		||||
      required final List<SnAccountContact>? contacts,
 | 
			
		||||
      required this.avatar,
 | 
			
		||||
      required this.banner,
 | 
			
		||||
      this.avatar = "",
 | 
			
		||||
      this.banner = "",
 | 
			
		||||
      required this.description,
 | 
			
		||||
      required this.name,
 | 
			
		||||
      required this.nick,
 | 
			
		||||
@@ -410,8 +410,10 @@ class _$SnAccountImpl extends _SnAccount {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  @JsonKey()
 | 
			
		||||
  final String avatar;
 | 
			
		||||
  @override
 | 
			
		||||
  @JsonKey()
 | 
			
		||||
  final String banner;
 | 
			
		||||
  @override
 | 
			
		||||
  final String description;
 | 
			
		||||
@@ -540,8 +542,8 @@ abstract class _SnAccount extends SnAccount {
 | 
			
		||||
      required final DateTime? deletedAt,
 | 
			
		||||
      required final DateTime? confirmedAt,
 | 
			
		||||
      required final List<SnAccountContact>? contacts,
 | 
			
		||||
      required final String avatar,
 | 
			
		||||
      required final String banner,
 | 
			
		||||
      final String avatar,
 | 
			
		||||
      final String banner,
 | 
			
		||||
      required final String description,
 | 
			
		||||
      required final String name,
 | 
			
		||||
      required final String nick,
 | 
			
		||||
 
 | 
			
		||||
@@ -20,8 +20,8 @@ _$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) =>
 | 
			
		||||
      contacts: (json['contacts'] as List<dynamic>?)
 | 
			
		||||
          ?.map((e) => SnAccountContact.fromJson(e as Map<String, dynamic>))
 | 
			
		||||
          .toList(),
 | 
			
		||||
      avatar: json['avatar'] as String,
 | 
			
		||||
      banner: json['banner'] as String,
 | 
			
		||||
      avatar: json['avatar'] as String? ?? "",
 | 
			
		||||
      banner: json['banner'] as String? ?? "",
 | 
			
		||||
      description: json['description'] as String,
 | 
			
		||||
      name: json['name'] as String,
 | 
			
		||||
      nick: json['nick'] as String,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user