♻️ Refactored large screen user experience
This commit is contained in:
		@@ -70,6 +70,11 @@ class NavigationProvider extends ChangeNotifier {
 | 
			
		||||
      screen: 'news',
 | 
			
		||||
      label: 'screenNews',
 | 
			
		||||
    ),
 | 
			
		||||
    AppNavDestination(
 | 
			
		||||
      icon: Icon(Symbols.settings, weight: 400, opticalSize: 20),
 | 
			
		||||
      screen: 'settings',
 | 
			
		||||
      label: 'screenSettings',
 | 
			
		||||
    ),
 | 
			
		||||
  ];
 | 
			
		||||
  static const List<String> kDefaultPinnedDestination = [
 | 
			
		||||
    'home',
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										329
									
								
								lib/router.dart
									
									
									
									
									
								
							
							
						
						
									
										329
									
								
								lib/router.dart
									
									
									
									
									
								
							@@ -70,38 +70,6 @@ final _appRoutes = [
 | 
			
		||||
    name: 'home',
 | 
			
		||||
    builder: (context, state) => const HomeScreen(),
 | 
			
		||||
  ),
 | 
			
		||||
  ShellRoute(
 | 
			
		||||
    builder: (context, state, child) => ResponsiveScaffold(
 | 
			
		||||
      asideFlex: 2,
 | 
			
		||||
      contentFlex: 3,
 | 
			
		||||
      aside: const ExploreScreen(),
 | 
			
		||||
      child: child,
 | 
			
		||||
    ),
 | 
			
		||||
    routes: [
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/explore',
 | 
			
		||||
        name: 'explore',
 | 
			
		||||
        builder: (context, state) => const ResponsiveScaffoldLanding(
 | 
			
		||||
          child: ExploreScreen(),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/posts/:slug',
 | 
			
		||||
        name: 'postDetail',
 | 
			
		||||
        builder: (context, state) => PostDetailScreen(
 | 
			
		||||
          key: ValueKey(state.pathParameters['slug']!),
 | 
			
		||||
          slug: state.pathParameters['slug']!,
 | 
			
		||||
          preload: state.extra as SnPost?,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/publishers/:name',
 | 
			
		||||
        name: 'postPublisher',
 | 
			
		||||
        builder: (context, state) =>
 | 
			
		||||
            PostPublisherScreen(name: state.pathParameters['name']!),
 | 
			
		||||
      ),
 | 
			
		||||
    ],
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/posts',
 | 
			
		||||
    name: 'posts',
 | 
			
		||||
@@ -145,6 +113,38 @@ final _appRoutes = [
 | 
			
		||||
      ),
 | 
			
		||||
    ],
 | 
			
		||||
  ),
 | 
			
		||||
  ShellRoute(
 | 
			
		||||
    builder: (context, state, child) => ResponsiveScaffold(
 | 
			
		||||
      asideFlex: 2,
 | 
			
		||||
      contentFlex: 3,
 | 
			
		||||
      aside: const ExploreScreen(),
 | 
			
		||||
      child: child,
 | 
			
		||||
    ),
 | 
			
		||||
    routes: [
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/explore',
 | 
			
		||||
        name: 'explore',
 | 
			
		||||
        builder: (context, state) => const ResponsiveScaffoldLanding(
 | 
			
		||||
          child: ExploreScreen(),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/posts/:slug',
 | 
			
		||||
        name: 'postDetail',
 | 
			
		||||
        builder: (context, state) => PostDetailScreen(
 | 
			
		||||
          key: ValueKey(state.pathParameters['slug']!),
 | 
			
		||||
          slug: state.pathParameters['slug']!,
 | 
			
		||||
          preload: state.extra as SnPost?,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/publishers/:name',
 | 
			
		||||
        name: 'postPublisher',
 | 
			
		||||
        builder: (context, state) =>
 | 
			
		||||
            PostPublisherScreen(name: state.pathParameters['name']!),
 | 
			
		||||
      ),
 | 
			
		||||
    ],
 | 
			
		||||
  ),
 | 
			
		||||
  ShellRoute(
 | 
			
		||||
    builder: (context, state, child) => ResponsiveScaffold(
 | 
			
		||||
      aside: const AccountScreen(),
 | 
			
		||||
@@ -152,142 +152,153 @@ final _appRoutes = [
 | 
			
		||||
    ),
 | 
			
		||||
    routes: [
 | 
			
		||||
      GoRoute(
 | 
			
		||||
          path: '/account',
 | 
			
		||||
          name: 'account',
 | 
			
		||||
          builder: (context, state) =>
 | 
			
		||||
              const ResponsiveScaffoldLanding(child: AccountScreen()),
 | 
			
		||||
          routes: [
 | 
			
		||||
            GoRoute(
 | 
			
		||||
              path: '/punishments',
 | 
			
		||||
              name: 'accountPunishments',
 | 
			
		||||
              builder: (context, state) => const PunishmentsScreen(),
 | 
			
		||||
            ),
 | 
			
		||||
            GoRoute(
 | 
			
		||||
              path: '/programs',
 | 
			
		||||
              name: 'accountProgram',
 | 
			
		||||
              builder: (context, state) => const AccountProgramScreen(),
 | 
			
		||||
            ),
 | 
			
		||||
            GoRoute(
 | 
			
		||||
              path: '/contacts',
 | 
			
		||||
              name: 'accountContactMethods',
 | 
			
		||||
              builder: (context, state) => const AccountContactMethod(),
 | 
			
		||||
            ),
 | 
			
		||||
            GoRoute(
 | 
			
		||||
              path: '/events',
 | 
			
		||||
              name: 'accountActionEvents',
 | 
			
		||||
              builder: (context, state) => const ActionEventScreen(),
 | 
			
		||||
            ),
 | 
			
		||||
            GoRoute(
 | 
			
		||||
              path: '/tickets',
 | 
			
		||||
              name: 'accountAuthTickets',
 | 
			
		||||
              builder: (context, state) => const AccountAuthTicket(),
 | 
			
		||||
            ),
 | 
			
		||||
            GoRoute(
 | 
			
		||||
              path: '/badges',
 | 
			
		||||
              name: 'accountBadges',
 | 
			
		||||
              builder: (context, state) => const AccountBadgesScreen(),
 | 
			
		||||
            ),
 | 
			
		||||
            GoRoute(
 | 
			
		||||
              path: '/wallet',
 | 
			
		||||
              name: 'accountWallet',
 | 
			
		||||
              builder: (context, state) => const WalletScreen(),
 | 
			
		||||
            ),
 | 
			
		||||
            GoRoute(
 | 
			
		||||
              path: '/keypairs',
 | 
			
		||||
              name: 'accountKeyPairs',
 | 
			
		||||
              builder: (context, state) => const KeyPairScreen(),
 | 
			
		||||
            ),
 | 
			
		||||
            GoRoute(
 | 
			
		||||
              path: '/settings',
 | 
			
		||||
              name: 'accountSettings',
 | 
			
		||||
              builder: (context, state) => AccountSettingsScreen(),
 | 
			
		||||
              routes: [
 | 
			
		||||
                GoRoute(
 | 
			
		||||
                  path: '/notify',
 | 
			
		||||
                  name: 'accountSettingsNotify',
 | 
			
		||||
                  builder: (context, state) => const AccountNotifyPrefsScreen(),
 | 
			
		||||
                ),
 | 
			
		||||
                GoRoute(
 | 
			
		||||
                  path: '/auth',
 | 
			
		||||
                  name: 'accountSettingsSecurity',
 | 
			
		||||
                  builder: (context, state) =>
 | 
			
		||||
                      const AccountSecurityPrefsScreen(),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
            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']!,
 | 
			
		||||
        path: '/account',
 | 
			
		||||
        name: 'account',
 | 
			
		||||
        builder: (context, state) =>
 | 
			
		||||
            const ResponsiveScaffoldLanding(child: AccountScreen()),
 | 
			
		||||
        routes: [
 | 
			
		||||
          GoRoute(
 | 
			
		||||
            path: '/punishments',
 | 
			
		||||
            name: 'accountPunishments',
 | 
			
		||||
            builder: (context, state) => const PunishmentsScreen(),
 | 
			
		||||
          ),
 | 
			
		||||
          GoRoute(
 | 
			
		||||
            path: '/programs',
 | 
			
		||||
            name: 'accountProgram',
 | 
			
		||||
            builder: (context, state) => const AccountProgramScreen(),
 | 
			
		||||
          ),
 | 
			
		||||
          GoRoute(
 | 
			
		||||
            path: '/contacts',
 | 
			
		||||
            name: 'accountContactMethods',
 | 
			
		||||
            builder: (context, state) => const AccountContactMethod(),
 | 
			
		||||
          ),
 | 
			
		||||
          GoRoute(
 | 
			
		||||
            path: '/events',
 | 
			
		||||
            name: 'accountActionEvents',
 | 
			
		||||
            builder: (context, state) => const ActionEventScreen(),
 | 
			
		||||
          ),
 | 
			
		||||
          GoRoute(
 | 
			
		||||
            path: '/tickets',
 | 
			
		||||
            name: 'accountAuthTickets',
 | 
			
		||||
            builder: (context, state) => const AccountAuthTicket(),
 | 
			
		||||
          ),
 | 
			
		||||
          GoRoute(
 | 
			
		||||
            path: '/badges',
 | 
			
		||||
            name: 'accountBadges',
 | 
			
		||||
            builder: (context, state) => const AccountBadgesScreen(),
 | 
			
		||||
          ),
 | 
			
		||||
          GoRoute(
 | 
			
		||||
            path: '/wallet',
 | 
			
		||||
            name: 'accountWallet',
 | 
			
		||||
            builder: (context, state) => const WalletScreen(),
 | 
			
		||||
          ),
 | 
			
		||||
          GoRoute(
 | 
			
		||||
            path: '/keypairs',
 | 
			
		||||
            name: 'accountKeyPairs',
 | 
			
		||||
            builder: (context, state) => const KeyPairScreen(),
 | 
			
		||||
          ),
 | 
			
		||||
          GoRoute(
 | 
			
		||||
            path: '/settings',
 | 
			
		||||
            name: 'accountSettings',
 | 
			
		||||
            builder: (context, state) => AccountSettingsScreen(),
 | 
			
		||||
            routes: [
 | 
			
		||||
              GoRoute(
 | 
			
		||||
                path: '/notify',
 | 
			
		||||
                name: 'accountSettingsNotify',
 | 
			
		||||
                builder: (context, state) => const AccountNotifyPrefsScreen(),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            GoRoute(
 | 
			
		||||
              path: '/profile/:name',
 | 
			
		||||
              name: 'accountProfilePage',
 | 
			
		||||
              pageBuilder: (context, state) => NoTransitionPage(
 | 
			
		||||
                child: UserScreen(name: state.pathParameters['name']!),
 | 
			
		||||
              GoRoute(
 | 
			
		||||
                path: '/auth',
 | 
			
		||||
                name: 'accountSettingsSecurity',
 | 
			
		||||
                builder: (context, state) => const AccountSecurityPrefsScreen(),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
          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: '/chat',
 | 
			
		||||
    name: 'chat',
 | 
			
		||||
    builder: (context, state) => const ChatScreen(),
 | 
			
		||||
    path: '/accounts/:name',
 | 
			
		||||
    name: 'accountProfilePage',
 | 
			
		||||
    pageBuilder: (context, state) => NoTransitionPage(
 | 
			
		||||
      child: UserScreen(name: state.pathParameters['name']!),
 | 
			
		||||
    ),
 | 
			
		||||
  ),
 | 
			
		||||
  ShellRoute(
 | 
			
		||||
    builder: (context, state, child) =>
 | 
			
		||||
        ResponsiveScaffold(aside: const ChatScreen(), child: child),
 | 
			
		||||
    routes: [
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/:scope/:alias',
 | 
			
		||||
        name: 'chatRoom',
 | 
			
		||||
        builder: (context, state) => ChatRoomScreen(
 | 
			
		||||
          scope: state.pathParameters['scope']!,
 | 
			
		||||
          alias: state.pathParameters['alias']!,
 | 
			
		||||
          extra: state.extra as ChatRoomScreenExtra?,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/:scope/:alias/call',
 | 
			
		||||
        name: 'chatCallRoom',
 | 
			
		||||
        builder: (context, state) => CallRoomScreen(
 | 
			
		||||
          scope: state.pathParameters['scope']!,
 | 
			
		||||
          alias: state.pathParameters['alias']!,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/:scope/:alias/detail',
 | 
			
		||||
        name: 'channelDetail',
 | 
			
		||||
        builder: (context, state) => ChannelDetailScreen(
 | 
			
		||||
          scope: state.pathParameters['scope']!,
 | 
			
		||||
          alias: state.pathParameters['alias']!,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/manage',
 | 
			
		||||
        name: 'chatManage',
 | 
			
		||||
        builder: (context, state) => ChatManageScreen(
 | 
			
		||||
          editingChannelAlias: state.uri.queryParameters['editing'],
 | 
			
		||||
        path: '/chat',
 | 
			
		||||
        name: 'chat',
 | 
			
		||||
        builder: (context, state) => const ResponsiveScaffoldLanding(
 | 
			
		||||
          child: ChatScreen(),
 | 
			
		||||
        ),
 | 
			
		||||
        routes: [
 | 
			
		||||
          GoRoute(
 | 
			
		||||
            path: '/:scope/:alias',
 | 
			
		||||
            name: 'chatRoom',
 | 
			
		||||
            builder: (context, state) => ChatRoomScreen(
 | 
			
		||||
              key: ValueKey(
 | 
			
		||||
                '${state.pathParameters['scope']!}:${state.pathParameters['alias']!}',
 | 
			
		||||
              ),
 | 
			
		||||
              scope: state.pathParameters['scope']!,
 | 
			
		||||
              alias: state.pathParameters['alias']!,
 | 
			
		||||
              extra: state.extra as ChatRoomScreenExtra?,
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          GoRoute(
 | 
			
		||||
            path: '/:scope/:alias/call',
 | 
			
		||||
            name: 'chatCallRoom',
 | 
			
		||||
            builder: (context, state) => CallRoomScreen(
 | 
			
		||||
              scope: state.pathParameters['scope']!,
 | 
			
		||||
              alias: state.pathParameters['alias']!,
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          GoRoute(
 | 
			
		||||
            path: '/:scope/:alias/detail',
 | 
			
		||||
            name: 'channelDetail',
 | 
			
		||||
            builder: (context, state) => ChannelDetailScreen(
 | 
			
		||||
              scope: state.pathParameters['scope']!,
 | 
			
		||||
              alias: state.pathParameters['alias']!,
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          GoRoute(
 | 
			
		||||
            path: '/manage',
 | 
			
		||||
            name: 'chatManage',
 | 
			
		||||
            builder: (context, state) => ChatManageScreen(
 | 
			
		||||
              editingChannelAlias: state.uri.queryParameters['editing'],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    ],
 | 
			
		||||
  ),
 | 
			
		||||
 
 | 
			
		||||
@@ -110,6 +110,7 @@ class AccountScreen extends StatelessWidget {
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: AutoAppBarLeading(),
 | 
			
		||||
        title: Text("screenAccount").tr(),
 | 
			
		||||
 
 | 
			
		||||
@@ -59,6 +59,7 @@ class _ActionEventScreenState extends State<ActionEventScreen> {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: const PageBackButton(),
 | 
			
		||||
        title: Text('accountActionEvent').tr(),
 | 
			
		||||
 
 | 
			
		||||
@@ -91,6 +91,7 @@ class _AccountAuthTicketState extends State<AccountAuthTicket> {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: const PageBackButton(),
 | 
			
		||||
        title: Text('accountAuthTickets').tr(),
 | 
			
		||||
 
 | 
			
		||||
@@ -70,6 +70,7 @@ class _AccountBadgesScreenState extends State<AccountBadgesScreen> {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: Text('screenAccountBadges').tr(),
 | 
			
		||||
      ),
 | 
			
		||||
 
 | 
			
		||||
@@ -69,6 +69,7 @@ class _AccountContactMethodState extends State<AccountContactMethod> {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: const PageBackButton(),
 | 
			
		||||
        title: Text('accountContactMethods').tr(),
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,11 @@ 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),
 | 
			
		||||
  3: (
 | 
			
		||||
    'authFactorInAppNotify',
 | 
			
		||||
    'authFactorInAppNotifyDescription',
 | 
			
		||||
    Symbols.notifications_active
 | 
			
		||||
  ),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class FactorSettingsScreen extends StatefulWidget {
 | 
			
		||||
@@ -36,7 +40,10 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
 | 
			
		||||
      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() ?? [],
 | 
			
		||||
        resp.data
 | 
			
		||||
                ?.map((e) => SnAuthFactor.fromJson(e as Map<String, dynamic>))
 | 
			
		||||
                .toList() ??
 | 
			
		||||
            [],
 | 
			
		||||
      );
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
@@ -55,6 +62,7 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: PageBackButton(),
 | 
			
		||||
        title: Text('screenFactorSettings').tr(),
 | 
			
		||||
@@ -96,7 +104,8 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
 | 
			
		||||
                    return ListTile(
 | 
			
		||||
                      title: Text(kFactorTypes[ele.type]!.$1).tr(),
 | 
			
		||||
                      subtitle: Text(kFactorTypes[ele.type]!.$2).tr(),
 | 
			
		||||
                      contentPadding: const EdgeInsets.only(left: 24, right: 12),
 | 
			
		||||
                      contentPadding:
 | 
			
		||||
                          const EdgeInsets.only(left: 24, right: 12),
 | 
			
		||||
                      leading: Icon(kFactorTypes[ele.type]!.$3),
 | 
			
		||||
                      trailing: IconButton(
 | 
			
		||||
                        icon: const Icon(Symbols.close),
 | 
			
		||||
@@ -105,14 +114,17 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
 | 
			
		||||
                                context
 | 
			
		||||
                                    .showConfirmDialog(
 | 
			
		||||
                                  'authFactorDelete'.tr(),
 | 
			
		||||
                                  'authFactorDeleteDescription'.tr(args: [kFactorTypes[ele.type]!.$1.tr()]),
 | 
			
		||||
                                  'authFactorDeleteDescription'.tr(
 | 
			
		||||
                                      args: [kFactorTypes[ele.type]!.$1.tr()]),
 | 
			
		||||
                                )
 | 
			
		||||
                                    .then((val) async {
 | 
			
		||||
                                  if (!val) return;
 | 
			
		||||
                                  try {
 | 
			
		||||
                                    if (!context.mounted) return;
 | 
			
		||||
                                    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
                                    await sn.client.delete('/cgi/id/users/me/factors/${ele.id}');
 | 
			
		||||
                                    final sn =
 | 
			
		||||
                                        context.read<SnNetworkProvider>();
 | 
			
		||||
                                    await sn.client.delete(
 | 
			
		||||
                                        '/cgi/id/users/me/factors/${ele.id}');
 | 
			
		||||
                                    _fetchFactors();
 | 
			
		||||
                                  } catch (err) {
 | 
			
		||||
                                    if (!context.mounted) return;
 | 
			
		||||
@@ -191,7 +203,9 @@ class _FactorNewDialogState extends State<_FactorNewDialog> {
 | 
			
		||||
              value: _factorType,
 | 
			
		||||
              items: kFactorTypes.entries.map(
 | 
			
		||||
                (ele) {
 | 
			
		||||
                  final contains = widget.currentlyHave.map((ele) => ele.type).contains(ele.key);
 | 
			
		||||
                  final contains = widget.currentlyHave
 | 
			
		||||
                      .map((ele) => ele.type)
 | 
			
		||||
                      .contains(ele.key);
 | 
			
		||||
                  return DropdownMenuItem<int>(
 | 
			
		||||
                    enabled: !contains,
 | 
			
		||||
                    value: ele.key,
 | 
			
		||||
 
 | 
			
		||||
@@ -37,6 +37,7 @@ class _KeyPairScreenState extends State<KeyPairScreen> {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: Text('screenKeyPairs').tr(),
 | 
			
		||||
      ),
 | 
			
		||||
 
 | 
			
		||||
@@ -75,6 +75,7 @@ class _AccountNotifyPrefsScreenState extends State<AccountNotifyPrefsScreen> {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: const PageBackButton(),
 | 
			
		||||
        title: Text('accountSettingsNotify').tr(),
 | 
			
		||||
 
 | 
			
		||||
@@ -70,6 +70,7 @@ class _AccountSecurityPrefsScreenState
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: const PageBackButton(),
 | 
			
		||||
        title: Text('accountSettingsSecurity').tr(),
 | 
			
		||||
 
 | 
			
		||||
@@ -66,37 +66,40 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
    _locationController.text = prof.profile!.location;
 | 
			
		||||
    _avatar = prof.avatar;
 | 
			
		||||
    _banner = prof.banner;
 | 
			
		||||
    _links = prof.profile!.links.entries.map((ele) => (ele.key, ele.value)).toList();
 | 
			
		||||
    _links =
 | 
			
		||||
        prof.profile!.links.entries.map((ele) => (ele.key, ele.value)).toList();
 | 
			
		||||
    _birthday = prof.profile!.birthday?.toLocal();
 | 
			
		||||
    if (_birthday != null) {
 | 
			
		||||
      _birthdayController.text = DateFormat(_kDateFormat).format(prof.profile!.birthday!.toLocal());
 | 
			
		||||
      _birthdayController.text =
 | 
			
		||||
          DateFormat(_kDateFormat).format(prof.profile!.birthday!.toLocal());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _selectBirthday() async {
 | 
			
		||||
    await showCupertinoModalPopup<DateTime?>(
 | 
			
		||||
      context: context,
 | 
			
		||||
      builder:
 | 
			
		||||
          (BuildContext context) => Container(
 | 
			
		||||
            height: 216,
 | 
			
		||||
            padding: const EdgeInsets.only(top: 6.0),
 | 
			
		||||
            margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
 | 
			
		||||
            color: Theme.of(context).colorScheme.surface,
 | 
			
		||||
            child: SafeArea(
 | 
			
		||||
              top: false,
 | 
			
		||||
              child: CupertinoDatePicker(
 | 
			
		||||
                initialDateTime: _birthday?.toLocal(),
 | 
			
		||||
                mode: CupertinoDatePickerMode.date,
 | 
			
		||||
                use24hFormat: true,
 | 
			
		||||
                onDateTimeChanged: (DateTime newDate) {
 | 
			
		||||
                  setState(() {
 | 
			
		||||
                    _birthday = newDate;
 | 
			
		||||
                    _birthdayController.text = DateFormat(_kDateFormat).format(_birthday!);
 | 
			
		||||
                  });
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
      builder: (BuildContext context) => Container(
 | 
			
		||||
        height: 216,
 | 
			
		||||
        padding: const EdgeInsets.only(top: 6.0),
 | 
			
		||||
        margin:
 | 
			
		||||
            EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
 | 
			
		||||
        color: Theme.of(context).colorScheme.surface,
 | 
			
		||||
        child: SafeArea(
 | 
			
		||||
          top: false,
 | 
			
		||||
          child: CupertinoDatePicker(
 | 
			
		||||
            initialDateTime: _birthday?.toLocal(),
 | 
			
		||||
            mode: CupertinoDatePickerMode.date,
 | 
			
		||||
            use24hFormat: true,
 | 
			
		||||
            onDateTimeChanged: (DateTime newDate) {
 | 
			
		||||
              setState(() {
 | 
			
		||||
                _birthday = newDate;
 | 
			
		||||
                _birthdayController.text =
 | 
			
		||||
                    DateFormat(_kDateFormat).format(_birthday!);
 | 
			
		||||
              });
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -109,29 +112,32 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
 | 
			
		||||
    Uint8List? rawBytes;
 | 
			
		||||
    if (!skipCrop) {
 | 
			
		||||
      final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
 | 
			
		||||
      final aspectRatios =
 | 
			
		||||
          place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
 | 
			
		||||
      final result =
 | 
			
		||||
          (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
 | 
			
		||||
              ? await showCupertinoImageCropper(
 | 
			
		||||
                // ignore: use_build_context_synchronously
 | 
			
		||||
                context,
 | 
			
		||||
                allowedAspectRatios: aspectRatios,
 | 
			
		||||
                imageProvider: imageProvider,
 | 
			
		||||
              )
 | 
			
		||||
              : await showMaterialImageCropper(
 | 
			
		||||
                // ignore: use_build_context_synchronously
 | 
			
		||||
                context,
 | 
			
		||||
                allowedAspectRatios: aspectRatios,
 | 
			
		||||
                imageProvider: imageProvider,
 | 
			
		||||
              );
 | 
			
		||||
      final ImageProvider imageProvider =
 | 
			
		||||
          kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
 | 
			
		||||
      final aspectRatios = place == 'banner'
 | 
			
		||||
          ? [CropAspectRatio(width: 16, height: 7)]
 | 
			
		||||
          : [CropAspectRatio(width: 1, height: 1)];
 | 
			
		||||
      final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
 | 
			
		||||
          ? await showCupertinoImageCropper(
 | 
			
		||||
              // ignore: use_build_context_synchronously
 | 
			
		||||
              context,
 | 
			
		||||
              allowedAspectRatios: aspectRatios,
 | 
			
		||||
              imageProvider: imageProvider,
 | 
			
		||||
            )
 | 
			
		||||
          : await showMaterialImageCropper(
 | 
			
		||||
              // ignore: use_build_context_synchronously
 | 
			
		||||
              context,
 | 
			
		||||
              allowedAspectRatios: aspectRatios,
 | 
			
		||||
              imageProvider: imageProvider,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
      if (result == null) return;
 | 
			
		||||
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      setState(() => _isBusy = true);
 | 
			
		||||
      rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
 | 
			
		||||
      rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!
 | 
			
		||||
          .buffer
 | 
			
		||||
          .asUint8List();
 | 
			
		||||
    } else {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      setState(() => _isBusy = true);
 | 
			
		||||
@@ -152,7 +158,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
      await sn.client.put('/cgi/id/users/me/$place', data: {'attachment': attachment.rid});
 | 
			
		||||
      await sn.client
 | 
			
		||||
          .put('/cgi/id/users/me/$place', data: {'attachment': attachment.rid});
 | 
			
		||||
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      final ua = context.read<UserProvider>();
 | 
			
		||||
@@ -188,7 +195,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
          'location': _locationController.value.text,
 | 
			
		||||
          'birthday': _birthday?.toUtc().toIso8601String(),
 | 
			
		||||
          'links': {
 | 
			
		||||
            for (final link in _links!.where((ele) => ele.$1.isNotEmpty && ele.$2.isNotEmpty)) link.$1: link.$2,
 | 
			
		||||
            for (final link in _links!
 | 
			
		||||
                .where((ele) => ele.$1.isNotEmpty && ele.$2.isNotEmpty))
 | 
			
		||||
              link.$1: link.$2,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
@@ -235,7 +244,10 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(leading: const PageBackButton(), title: Text('screenAccountProfileEdit').tr()),
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
          leading: const PageBackButton(),
 | 
			
		||||
          title: Text('screenAccountProfileEdit').tr()),
 | 
			
		||||
      body: SingleChildScrollView(
 | 
			
		||||
        child: Column(
 | 
			
		||||
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
@@ -253,11 +265,14 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
                      child: AspectRatio(
 | 
			
		||||
                        aspectRatio: 16 / 9,
 | 
			
		||||
                        child: Container(
 | 
			
		||||
                          color: Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
			
		||||
                          child:
 | 
			
		||||
                              _banner != null
 | 
			
		||||
                                  ? AutoResizeUniversalImage(sn.getAttachmentUrl(_banner!), fit: BoxFit.cover)
 | 
			
		||||
                                  : const SizedBox.shrink(),
 | 
			
		||||
                          color: Theme.of(context)
 | 
			
		||||
                              .colorScheme
 | 
			
		||||
                              .surfaceContainerHigh,
 | 
			
		||||
                          child: _banner != null
 | 
			
		||||
                              ? AutoResizeUniversalImage(
 | 
			
		||||
                                  sn.getAttachmentUrl(_banner!),
 | 
			
		||||
                                  fit: BoxFit.cover)
 | 
			
		||||
                              : const SizedBox.shrink(),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
@@ -294,12 +309,16 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
                    labelText: 'fieldUsername'.tr(),
 | 
			
		||||
                    helperText: 'fieldUsernameCannotEditHint'.tr(),
 | 
			
		||||
                  ),
 | 
			
		||||
                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                  onTapOutside: (_) =>
 | 
			
		||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                ),
 | 
			
		||||
                TextField(
 | 
			
		||||
                  controller: _nicknameController,
 | 
			
		||||
                  decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldNickname'.tr()),
 | 
			
		||||
                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                  decoration: InputDecoration(
 | 
			
		||||
                      border: const UnderlineInputBorder(),
 | 
			
		||||
                      labelText: 'fieldNickname'.tr()),
 | 
			
		||||
                  onTapOutside: (_) =>
 | 
			
		||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                ),
 | 
			
		||||
                Row(
 | 
			
		||||
                  children: [
 | 
			
		||||
@@ -311,7 +330,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
                          border: const UnderlineInputBorder(),
 | 
			
		||||
                          labelText: 'fieldFirstName'.tr(),
 | 
			
		||||
                        ),
 | 
			
		||||
                        onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                        onTapOutside: (_) =>
 | 
			
		||||
                            FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    const Gap(8),
 | 
			
		||||
@@ -323,7 +343,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
                          border: const UnderlineInputBorder(),
 | 
			
		||||
                          labelText: 'fieldLastName'.tr(),
 | 
			
		||||
                        ),
 | 
			
		||||
                        onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                        onTapOutside: (_) =>
 | 
			
		||||
                            FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ],
 | 
			
		||||
@@ -338,7 +359,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
                          border: const UnderlineInputBorder(),
 | 
			
		||||
                          labelText: 'fieldGender'.tr(),
 | 
			
		||||
                        ),
 | 
			
		||||
                        onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                        onTapOutside: (_) =>
 | 
			
		||||
                            FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    const Gap(4),
 | 
			
		||||
@@ -350,7 +372,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
                          border: const UnderlineInputBorder(),
 | 
			
		||||
                          labelText: 'fieldPronouns'.tr(),
 | 
			
		||||
                        ),
 | 
			
		||||
                        onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                        onTapOutside: (_) =>
 | 
			
		||||
                            FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ],
 | 
			
		||||
@@ -360,8 +383,11 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
                  keyboardType: TextInputType.multiline,
 | 
			
		||||
                  maxLines: null,
 | 
			
		||||
                  minLines: 3,
 | 
			
		||||
                  decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldDescription'.tr()),
 | 
			
		||||
                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                  decoration: InputDecoration(
 | 
			
		||||
                      border: const UnderlineInputBorder(),
 | 
			
		||||
                      labelText: 'fieldDescription'.tr()),
 | 
			
		||||
                  onTapOutside: (_) =>
 | 
			
		||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                ),
 | 
			
		||||
                Row(
 | 
			
		||||
                  crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
@@ -373,18 +399,21 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
                          border: const UnderlineInputBorder(),
 | 
			
		||||
                          labelText: 'fieldTimeZone'.tr(),
 | 
			
		||||
                        ),
 | 
			
		||||
                        onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                        onTapOutside: (_) =>
 | 
			
		||||
                            FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    const Gap(4),
 | 
			
		||||
                    StyledWidget(
 | 
			
		||||
                      IconButton(
 | 
			
		||||
                        icon: const Icon(Symbols.calendar_month),
 | 
			
		||||
                        visualDensity: VisualDensity(horizontal: -4, vertical: -4),
 | 
			
		||||
                        visualDensity:
 | 
			
		||||
                            VisualDensity(horizontal: -4, vertical: -4),
 | 
			
		||||
                        padding: EdgeInsets.zero,
 | 
			
		||||
                        constraints: const BoxConstraints(),
 | 
			
		||||
                        onPressed: () async {
 | 
			
		||||
                          _timezoneController.text = await FlutterTimezone.getLocalTimezone();
 | 
			
		||||
                          _timezoneController.text =
 | 
			
		||||
                              await FlutterTimezone.getLocalTimezone();
 | 
			
		||||
                        },
 | 
			
		||||
                      ),
 | 
			
		||||
                    ).padding(top: 6),
 | 
			
		||||
@@ -392,7 +421,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
                    StyledWidget(
 | 
			
		||||
                      IconButton(
 | 
			
		||||
                        icon: const Icon(Symbols.clear),
 | 
			
		||||
                        visualDensity: VisualDensity(horizontal: -4, vertical: -4),
 | 
			
		||||
                        visualDensity:
 | 
			
		||||
                            VisualDensity(horizontal: -4, vertical: -4),
 | 
			
		||||
                        padding: EdgeInsets.zero,
 | 
			
		||||
                        constraints: const BoxConstraints(),
 | 
			
		||||
                        onPressed: () {
 | 
			
		||||
@@ -404,13 +434,18 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
                ),
 | 
			
		||||
                TextField(
 | 
			
		||||
                  controller: _locationController,
 | 
			
		||||
                  decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldLocation'.tr()),
 | 
			
		||||
                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                  decoration: InputDecoration(
 | 
			
		||||
                      border: const UnderlineInputBorder(),
 | 
			
		||||
                      labelText: 'fieldLocation'.tr()),
 | 
			
		||||
                  onTapOutside: (_) =>
 | 
			
		||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                ),
 | 
			
		||||
                TextField(
 | 
			
		||||
                  controller: _birthdayController,
 | 
			
		||||
                  readOnly: true,
 | 
			
		||||
                  decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldBirthday'.tr()),
 | 
			
		||||
                  decoration: InputDecoration(
 | 
			
		||||
                      border: const UnderlineInputBorder(),
 | 
			
		||||
                      labelText: 'fieldBirthday'.tr()),
 | 
			
		||||
                  onTap: () => _selectBirthday(),
 | 
			
		||||
                ),
 | 
			
		||||
                if (_links != null)
 | 
			
		||||
@@ -418,7 +453,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
                    margin: const EdgeInsets.only(top: 16, bottom: 4),
 | 
			
		||||
                    child: Container(
 | 
			
		||||
                      width: double.infinity,
 | 
			
		||||
                      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
 | 
			
		||||
                      padding: const EdgeInsets.symmetric(
 | 
			
		||||
                          horizontal: 16, vertical: 8),
 | 
			
		||||
                      child: Column(
 | 
			
		||||
                        crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                        children: [
 | 
			
		||||
@@ -427,13 +463,17 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
                              Expanded(
 | 
			
		||||
                                child: Text(
 | 
			
		||||
                                  'fieldLinks'.tr(),
 | 
			
		||||
                                  style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 17),
 | 
			
		||||
                                  style: Theme.of(context)
 | 
			
		||||
                                      .textTheme
 | 
			
		||||
                                      .titleMedium!
 | 
			
		||||
                                      .copyWith(fontSize: 17),
 | 
			
		||||
                                ),
 | 
			
		||||
                              ),
 | 
			
		||||
                              IconButton(
 | 
			
		||||
                                padding: EdgeInsets.zero,
 | 
			
		||||
                                constraints: const BoxConstraints(),
 | 
			
		||||
                                visualDensity: VisualDensity(horizontal: -4, vertical: -4),
 | 
			
		||||
                                visualDensity:
 | 
			
		||||
                                    VisualDensity(horizontal: -4, vertical: -4),
 | 
			
		||||
                                icon: const Icon(Symbols.add),
 | 
			
		||||
                                onPressed: () {
 | 
			
		||||
                                  setState(() => _links!.add(('', '')));
 | 
			
		||||
@@ -457,7 +497,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
                                    onChanged: (value) {
 | 
			
		||||
                                      _links![idx] = (value, _links![idx].$2);
 | 
			
		||||
                                    },
 | 
			
		||||
                                    onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                                    onTapOutside: (_) => FocusManager
 | 
			
		||||
                                        .instance.primaryFocus
 | 
			
		||||
                                        ?.unfocus(),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                ),
 | 
			
		||||
                                const Gap(8),
 | 
			
		||||
@@ -473,7 +515,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
                                    onChanged: (value) {
 | 
			
		||||
                                      _links![idx] = (_links![idx].$1, value);
 | 
			
		||||
                                    },
 | 
			
		||||
                                    onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                                    onTapOutside: (_) => FocusManager
 | 
			
		||||
                                        .instance.primaryFocus
 | 
			
		||||
                                        ?.unfocus(),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                ),
 | 
			
		||||
                              ],
 | 
			
		||||
 
 | 
			
		||||
@@ -70,6 +70,7 @@ class _AccountProgramScreenState extends State<AccountProgramScreen> {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: Text('accountProgram').tr(),
 | 
			
		||||
      ),
 | 
			
		||||
 
 | 
			
		||||
@@ -27,10 +27,12 @@ class AccountPublisherEditScreen extends StatefulWidget {
 | 
			
		||||
  const AccountPublisherEditScreen({super.key, required this.name});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<AccountPublisherEditScreen> createState() => _AccountPublisherEditScreenState();
 | 
			
		||||
  State<AccountPublisherEditScreen> createState() =>
 | 
			
		||||
      _AccountPublisherEditScreenState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> {
 | 
			
		||||
class _AccountPublisherEditScreenState
 | 
			
		||||
    extends State<AccountPublisherEditScreen> {
 | 
			
		||||
  bool _isBusy = false;
 | 
			
		||||
 | 
			
		||||
  SnPublisher? _publisher;
 | 
			
		||||
@@ -115,29 +117,32 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
 | 
			
		||||
 | 
			
		||||
    Uint8List? rawBytes;
 | 
			
		||||
    if (!skipCrop) {
 | 
			
		||||
      final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
 | 
			
		||||
      final aspectRatios =
 | 
			
		||||
          place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
 | 
			
		||||
      final result =
 | 
			
		||||
          (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
 | 
			
		||||
              ? await showCupertinoImageCropper(
 | 
			
		||||
                // ignore: use_build_context_synchronously
 | 
			
		||||
                context,
 | 
			
		||||
                allowedAspectRatios: aspectRatios,
 | 
			
		||||
                imageProvider: imageProvider,
 | 
			
		||||
              )
 | 
			
		||||
              : await showMaterialImageCropper(
 | 
			
		||||
                // ignore: use_build_context_synchronously
 | 
			
		||||
                context,
 | 
			
		||||
                allowedAspectRatios: aspectRatios,
 | 
			
		||||
                imageProvider: imageProvider,
 | 
			
		||||
              );
 | 
			
		||||
      final ImageProvider imageProvider =
 | 
			
		||||
          kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
 | 
			
		||||
      final aspectRatios = place == 'banner'
 | 
			
		||||
          ? [CropAspectRatio(width: 16, height: 7)]
 | 
			
		||||
          : [CropAspectRatio(width: 1, height: 1)];
 | 
			
		||||
      final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
 | 
			
		||||
          ? await showCupertinoImageCropper(
 | 
			
		||||
              // ignore: use_build_context_synchronously
 | 
			
		||||
              context,
 | 
			
		||||
              allowedAspectRatios: aspectRatios,
 | 
			
		||||
              imageProvider: imageProvider,
 | 
			
		||||
            )
 | 
			
		||||
          : await showMaterialImageCropper(
 | 
			
		||||
              // ignore: use_build_context_synchronously
 | 
			
		||||
              context,
 | 
			
		||||
              allowedAspectRatios: aspectRatios,
 | 
			
		||||
              imageProvider: imageProvider,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
      if (result == null) return;
 | 
			
		||||
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      setState(() => _isBusy = true);
 | 
			
		||||
      rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
 | 
			
		||||
      rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!
 | 
			
		||||
          .buffer
 | 
			
		||||
          .asUint8List();
 | 
			
		||||
    } else {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      setState(() => _isBusy = true);
 | 
			
		||||
@@ -191,7 +196,10 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(leading: PageBackButton(), title: Text('screenAccountPublisherEdit').tr()),
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
          leading: PageBackButton(),
 | 
			
		||||
          title: Text('screenAccountPublisherEdit').tr()),
 | 
			
		||||
      body: SingleChildScrollView(
 | 
			
		||||
        child: Column(
 | 
			
		||||
          children: [
 | 
			
		||||
@@ -208,11 +216,14 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
 | 
			
		||||
                      child: AspectRatio(
 | 
			
		||||
                        aspectRatio: 16 / 9,
 | 
			
		||||
                        child: Container(
 | 
			
		||||
                          color: Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
			
		||||
                          child:
 | 
			
		||||
                              _banner != null
 | 
			
		||||
                                  ? AutoResizeUniversalImage(sn.getAttachmentUrl(_banner!), fit: BoxFit.cover)
 | 
			
		||||
                                  : const SizedBox.shrink(),
 | 
			
		||||
                          color: Theme.of(context)
 | 
			
		||||
                              .colorScheme
 | 
			
		||||
                              .surfaceContainerHigh,
 | 
			
		||||
                          child: _banner != null
 | 
			
		||||
                              ? AutoResizeUniversalImage(
 | 
			
		||||
                                  sn.getAttachmentUrl(_banner!),
 | 
			
		||||
                                  fit: BoxFit.cover)
 | 
			
		||||
                              : const SizedBox.shrink(),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
@@ -245,13 +256,15 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
 | 
			
		||||
                labelText: 'fieldUsername'.tr(),
 | 
			
		||||
                helperText: 'fieldUsernameCannotEditHint'.tr(),
 | 
			
		||||
              ),
 | 
			
		||||
              onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
              onTapOutside: (_) =>
 | 
			
		||||
                  FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
            ),
 | 
			
		||||
            const Gap(4),
 | 
			
		||||
            TextField(
 | 
			
		||||
              controller: _nickController,
 | 
			
		||||
              decoration: InputDecoration(labelText: 'fieldNickname'.tr()),
 | 
			
		||||
              onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
              onTapOutside: (_) =>
 | 
			
		||||
                  FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
            ),
 | 
			
		||||
            const Gap(4),
 | 
			
		||||
            TextField(
 | 
			
		||||
@@ -259,7 +272,8 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
 | 
			
		||||
              maxLines: null,
 | 
			
		||||
              minLines: 3,
 | 
			
		||||
              decoration: InputDecoration(labelText: 'fieldDescription'.tr()),
 | 
			
		||||
              onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
              onTapOutside: (_) =>
 | 
			
		||||
                  FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
            ),
 | 
			
		||||
            const Gap(12),
 | 
			
		||||
            Row(
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,8 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return  AppScaffold(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: const PageBackButton(),
 | 
			
		||||
        title: Text('screenAccountPublisherNew').tr(),
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,8 @@ class _PublisherScreenState extends State<PublisherScreen> {
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      final resp = await sn.client.get('/cgi/co/publishers/me');
 | 
			
		||||
      final List<SnPublisher> out = List<SnPublisher>.from(resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
 | 
			
		||||
      final List<SnPublisher> out = List<SnPublisher>.from(
 | 
			
		||||
          resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
 | 
			
		||||
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
 | 
			
		||||
@@ -81,6 +82,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: const PageBackButton(),
 | 
			
		||||
        title: Text('screenAccountPublishers').tr(),
 | 
			
		||||
@@ -93,7 +95,9 @@ class _PublisherScreenState extends State<PublisherScreen> {
 | 
			
		||||
            contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
            leading: const Icon(Symbols.add_circle),
 | 
			
		||||
            onTap: () {
 | 
			
		||||
              GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
 | 
			
		||||
              GoRouter.of(context)
 | 
			
		||||
                  .pushNamed('accountPublisherNew')
 | 
			
		||||
                  .then((value) {
 | 
			
		||||
                if (value == true) {
 | 
			
		||||
                  _publishers.clear();
 | 
			
		||||
                  _fetchPublishers();
 | 
			
		||||
@@ -119,7 +123,8 @@ class _PublisherScreenState extends State<PublisherScreen> {
 | 
			
		||||
                    return ListTile(
 | 
			
		||||
                      title: Text(publisher.nick),
 | 
			
		||||
                      subtitle: Text('@${publisher.name}'),
 | 
			
		||||
                      contentPadding: const EdgeInsets.symmetric(horizontal: 16),
 | 
			
		||||
                      contentPadding:
 | 
			
		||||
                          const EdgeInsets.symmetric(horizontal: 16),
 | 
			
		||||
                      leading: AccountImage(content: publisher.avatar),
 | 
			
		||||
                      trailing: PopupMenuButton(
 | 
			
		||||
                        itemBuilder: (BuildContext context) => [
 | 
			
		||||
 
 | 
			
		||||
@@ -55,6 +55,7 @@ class _PunishmentsScreenState extends State<PunishmentsScreen> {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: Text('accountPunishments').tr(),
 | 
			
		||||
        leading: PageBackButton(),
 | 
			
		||||
 
 | 
			
		||||
@@ -37,6 +37,7 @@ class AccountSettingsScreen extends StatelessWidget {
 | 
			
		||||
    final ua = context.watch<UserProvider>();
 | 
			
		||||
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: PageBackButton(),
 | 
			
		||||
        title: Text('screenAccountSettings').tr(),
 | 
			
		||||
 
 | 
			
		||||
@@ -6,19 +6,16 @@ import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:google_fonts/google_fonts.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:responsive_framework/responsive_framework.dart';
 | 
			
		||||
import 'package:surface/providers/channel.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/providers/user_directory.dart';
 | 
			
		||||
import 'package:surface/providers/userinfo.dart';
 | 
			
		||||
import 'package:surface/screens/chat/room.dart';
 | 
			
		||||
import 'package:surface/types/chat.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_select.dart';
 | 
			
		||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_background.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:surface/widgets/unauthorized_hint.dart';
 | 
			
		||||
import 'package:uuid/uuid.dart';
 | 
			
		||||
@@ -130,8 +127,6 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  SnChannel? _focusChannel;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
@@ -140,13 +135,8 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _onTapChannel(SnChannel channel) {
 | 
			
		||||
    final doExpand = ResponsiveBreakpoints.of(context).largerOrEqualTo(DESKTOP);
 | 
			
		||||
 | 
			
		||||
    if (doExpand) {
 | 
			
		||||
      setState(() => _focusChannel = channel);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    GoRouter.of(context).pushNamed(
 | 
			
		||||
    setState(() => _unreadCounts?[channel.id] = 0);
 | 
			
		||||
    GoRouter.of(context).pushReplacementNamed(
 | 
			
		||||
      'chatRoom',
 | 
			
		||||
      pathParameters: {
 | 
			
		||||
        'scope': channel.realm?.alias ?? 'global',
 | 
			
		||||
@@ -154,7 +144,6 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
			
		||||
      },
 | 
			
		||||
    ).then((value) {
 | 
			
		||||
      if (mounted) {
 | 
			
		||||
        _unreadCounts?[channel.id] = 0;
 | 
			
		||||
        setState(() => _unreadCounts?[channel.id] = 0);
 | 
			
		||||
        _refreshChannels(noRemote: true);
 | 
			
		||||
      }
 | 
			
		||||
@@ -177,10 +166,8 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final doExpand = ResponsiveBreakpoints.of(context).largerOrEqualTo(DESKTOP);
 | 
			
		||||
 | 
			
		||||
    final chatList = AppScaffold(
 | 
			
		||||
      noBackground: doExpand,
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: AutoAppBarLeading(),
 | 
			
		||||
        title: Text('screenChat').tr(),
 | 
			
		||||
@@ -268,11 +255,6 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
			
		||||
                      lastMessage: lastMessage,
 | 
			
		||||
                      unreadCount: _unreadCounts?[channel.id],
 | 
			
		||||
                      onTap: () {
 | 
			
		||||
                        if (doExpand) {
 | 
			
		||||
                          _unreadCounts?[channel.id] = 0;
 | 
			
		||||
                          setState(() => _focusChannel = channel);
 | 
			
		||||
                          return;
 | 
			
		||||
                        }
 | 
			
		||||
                        _onTapChannel(channel);
 | 
			
		||||
                      },
 | 
			
		||||
                    );
 | 
			
		||||
@@ -284,28 +266,6 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (doExpand) {
 | 
			
		||||
      return AppBackground(
 | 
			
		||||
        isRoot: true,
 | 
			
		||||
        child: Row(
 | 
			
		||||
          children: [
 | 
			
		||||
            SizedBox(width: 340, child: chatList),
 | 
			
		||||
            const VerticalDivider(width: 1),
 | 
			
		||||
            if (_focusChannel != null)
 | 
			
		||||
              Expanded(
 | 
			
		||||
                child: ChatRoomScreen(
 | 
			
		||||
                  key: ValueKey(_focusChannel!.id),
 | 
			
		||||
                  scope: _focusChannel!.realm?.alias ?? 'global',
 | 
			
		||||
                  alias: _focusChannel!.alias,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return chatList;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,8 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
			
		||||
    return Stack(
 | 
			
		||||
      children: [
 | 
			
		||||
        Container(
 | 
			
		||||
          color: Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
 | 
			
		||||
          color:
 | 
			
		||||
              Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
 | 
			
		||||
          child: call.focusTrack != null
 | 
			
		||||
              ? InteractiveParticipantWidget(
 | 
			
		||||
                  isFixedAvatar: false,
 | 
			
		||||
@@ -72,7 +73,8 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
			
		||||
                      color: Theme.of(context).cardColor,
 | 
			
		||||
                      participant: track,
 | 
			
		||||
                      onTap: () {
 | 
			
		||||
                        if (track.participant.sid != call.focusTrack?.participant.sid) {
 | 
			
		||||
                        if (track.participant.sid !=
 | 
			
		||||
                            call.focusTrack?.participant.sid) {
 | 
			
		||||
                          call.setFocusTrack(track);
 | 
			
		||||
                        }
 | 
			
		||||
                      },
 | 
			
		||||
@@ -114,10 +116,14 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
			
		||||
            child: ClipRRect(
 | 
			
		||||
              borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
              child: InteractiveParticipantWidget(
 | 
			
		||||
                color: Theme.of(context).colorScheme.surfaceContainerHigh.withOpacity(0.75),
 | 
			
		||||
                color: Theme.of(context)
 | 
			
		||||
                    .colorScheme
 | 
			
		||||
                    .surfaceContainerHigh
 | 
			
		||||
                    .withOpacity(0.75),
 | 
			
		||||
                participant: track,
 | 
			
		||||
                onTap: () {
 | 
			
		||||
                  if (track.participant.sid != call.focusTrack?.participant.sid) {
 | 
			
		||||
                  if (track.participant.sid !=
 | 
			
		||||
                      call.focusTrack?.participant.sid) {
 | 
			
		||||
                    call.setFocusTrack(track);
 | 
			
		||||
                  }
 | 
			
		||||
                },
 | 
			
		||||
@@ -149,6 +155,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
			
		||||
        listenable: call,
 | 
			
		||||
        builder: (context, _) {
 | 
			
		||||
          return AppScaffold(
 | 
			
		||||
            noBackground: true,
 | 
			
		||||
            appBar: AppBar(
 | 
			
		||||
              title: RichText(
 | 
			
		||||
                textAlign: TextAlign.center,
 | 
			
		||||
@@ -183,7 +190,8 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
			
		||||
                        Builder(builder: (context) {
 | 
			
		||||
                          final call = context.read<ChatCallProvider>();
 | 
			
		||||
                          final connectionQuality =
 | 
			
		||||
                              call.room.localParticipant?.connectionQuality ?? livekit.ConnectionQuality.unknown;
 | 
			
		||||
                              call.room.localParticipant?.connectionQuality ??
 | 
			
		||||
                                  livekit.ConnectionQuality.unknown;
 | 
			
		||||
                          return Expanded(
 | 
			
		||||
                            child: Column(
 | 
			
		||||
                              mainAxisSize: MainAxisSize.min,
 | 
			
		||||
@@ -205,24 +213,35 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
			
		||||
                                  children: [
 | 
			
		||||
                                    Text(
 | 
			
		||||
                                      {
 | 
			
		||||
                                        livekit.ConnectionState.disconnected: 'callStatusDisconnected'.tr(),
 | 
			
		||||
                                        livekit.ConnectionState.connected: 'callStatusConnected'.tr(),
 | 
			
		||||
                                        livekit.ConnectionState.connecting: 'callStatusConnecting'.tr(),
 | 
			
		||||
                                        livekit.ConnectionState.reconnecting: 'callStatusReconnecting'.tr(),
 | 
			
		||||
                                        livekit.ConnectionState.disconnected:
 | 
			
		||||
                                            'callStatusDisconnected'.tr(),
 | 
			
		||||
                                        livekit.ConnectionState.connected:
 | 
			
		||||
                                            'callStatusConnected'.tr(),
 | 
			
		||||
                                        livekit.ConnectionState.connecting:
 | 
			
		||||
                                            'callStatusConnecting'.tr(),
 | 
			
		||||
                                        livekit.ConnectionState.reconnecting:
 | 
			
		||||
                                            'callStatusReconnecting'.tr(),
 | 
			
		||||
                                      }[call.room.connectionState]!,
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    const Gap(6),
 | 
			
		||||
                                    if (connectionQuality != livekit.ConnectionQuality.unknown)
 | 
			
		||||
                                    if (connectionQuality !=
 | 
			
		||||
                                        livekit.ConnectionQuality.unknown)
 | 
			
		||||
                                      Icon(
 | 
			
		||||
                                        {
 | 
			
		||||
                                          livekit.ConnectionQuality.excellent: Icons.signal_cellular_alt,
 | 
			
		||||
                                          livekit.ConnectionQuality.good: Icons.signal_cellular_alt_2_bar,
 | 
			
		||||
                                          livekit.ConnectionQuality.poor: Icons.signal_cellular_alt_1_bar,
 | 
			
		||||
                                          livekit.ConnectionQuality.excellent:
 | 
			
		||||
                                              Icons.signal_cellular_alt,
 | 
			
		||||
                                          livekit.ConnectionQuality.good:
 | 
			
		||||
                                              Icons.signal_cellular_alt_2_bar,
 | 
			
		||||
                                          livekit.ConnectionQuality.poor:
 | 
			
		||||
                                              Icons.signal_cellular_alt_1_bar,
 | 
			
		||||
                                        }[connectionQuality],
 | 
			
		||||
                                        color: {
 | 
			
		||||
                                          livekit.ConnectionQuality.excellent: Colors.green,
 | 
			
		||||
                                          livekit.ConnectionQuality.good: Colors.orange,
 | 
			
		||||
                                          livekit.ConnectionQuality.poor: Colors.red,
 | 
			
		||||
                                          livekit.ConnectionQuality.excellent:
 | 
			
		||||
                                              Colors.green,
 | 
			
		||||
                                          livekit.ConnectionQuality.good:
 | 
			
		||||
                                              Colors.orange,
 | 
			
		||||
                                          livekit.ConnectionQuality.poor:
 | 
			
		||||
                                              Colors.red,
 | 
			
		||||
                                        }[connectionQuality],
 | 
			
		||||
                                        size: 16,
 | 
			
		||||
                                      )
 | 
			
		||||
@@ -244,7 +263,9 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
			
		||||
                        Row(
 | 
			
		||||
                          children: [
 | 
			
		||||
                            IconButton(
 | 
			
		||||
                              icon: _layoutMode == 0 ? const Icon(Icons.view_list) : const Icon(Icons.grid_view),
 | 
			
		||||
                              icon: _layoutMode == 0
 | 
			
		||||
                                  ? const Icon(Icons.view_list)
 | 
			
		||||
                                  : const Icon(Icons.grid_view),
 | 
			
		||||
                              onPressed: () {
 | 
			
		||||
                                _switchLayout();
 | 
			
		||||
                              },
 | 
			
		||||
 
 | 
			
		||||
@@ -220,6 +220,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
 | 
			
		||||
    final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id;
 | 
			
		||||
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: _channel != null ? Text(_channel!.name) : Text('loading').tr(),
 | 
			
		||||
      ),
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
        resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
 | 
			
		||||
      );
 | 
			
		||||
      if (_editingChannel != null) {
 | 
			
		||||
        _belongToRealm = _realms?.firstWhereOrNull((e) => e.id == _editingChannel!.realmId);
 | 
			
		||||
        _belongToRealm =
 | 
			
		||||
            _realms?.firstWhereOrNull((e) => e.id == _editingChannel!.realmId);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (mounted) context.showErrorDialog(err);
 | 
			
		||||
@@ -97,7 +98,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
      'is_community': _isCommunity,
 | 
			
		||||
      if (_editingChannel != null && _belongToRealm == null)
 | 
			
		||||
        'new_belongs_realm': 'global'
 | 
			
		||||
      else if (_editingChannel != null && _belongToRealm?.id != _editingChannel?.realm?.id)
 | 
			
		||||
      else if (_editingChannel != null &&
 | 
			
		||||
          _belongToRealm?.id != _editingChannel?.realm?.id)
 | 
			
		||||
        'new_belongs_realm': _belongToRealm!.alias,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@@ -139,8 +141,11 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: widget.editingChannelAlias != null ? Text('screenChatManage').tr() : Text('screenChatNew').tr(),
 | 
			
		||||
        title: widget.editingChannelAlias != null
 | 
			
		||||
            ? Text('screenChatManage').tr()
 | 
			
		||||
            : Text('screenChatNew').tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      body: SingleChildScrollView(
 | 
			
		||||
        child: Column(
 | 
			
		||||
@@ -152,7 +157,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
                leadingPadding: const EdgeInsets.only(left: 10, right: 20),
 | 
			
		||||
                dividerColor: Colors.transparent,
 | 
			
		||||
                content: Text(
 | 
			
		||||
                  'channelEditingNotice'.tr(args: ['#${_editingChannel!.alias}']),
 | 
			
		||||
                  'channelEditingNotice'
 | 
			
		||||
                      .tr(args: ['#${_editingChannel!.alias}']),
 | 
			
		||||
                ),
 | 
			
		||||
                actions: [
 | 
			
		||||
                  TextButton(
 | 
			
		||||
@@ -192,12 +198,15 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
                                  mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                                  children: [
 | 
			
		||||
                                    Text(item.name).textStyle(Theme.of(context).textTheme.bodyMedium!),
 | 
			
		||||
                                    Text(item.name).textStyle(Theme.of(context)
 | 
			
		||||
                                        .textTheme
 | 
			
		||||
                                        .bodyMedium!),
 | 
			
		||||
                                    Text(
 | 
			
		||||
                                      item.description,
 | 
			
		||||
                                      maxLines: 1,
 | 
			
		||||
                                      overflow: TextOverflow.ellipsis,
 | 
			
		||||
                                    ).textStyle(Theme.of(context).textTheme.bodySmall!),
 | 
			
		||||
                                    ).textStyle(
 | 
			
		||||
                                        Theme.of(context).textTheme.bodySmall!),
 | 
			
		||||
                                  ],
 | 
			
		||||
                                ),
 | 
			
		||||
                              ),
 | 
			
		||||
@@ -213,7 +222,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
                        CircleAvatar(
 | 
			
		||||
                          radius: 16,
 | 
			
		||||
                          backgroundColor: Colors.transparent,
 | 
			
		||||
                          foregroundColor: Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
                          foregroundColor:
 | 
			
		||||
                              Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
                          child: const Icon(Symbols.clear),
 | 
			
		||||
                        ),
 | 
			
		||||
                        const Gap(12),
 | 
			
		||||
@@ -222,7 +232,9 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
                            mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                            crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                            children: [
 | 
			
		||||
                              Text('fieldChatBelongToRealmUnset').tr().textStyle(
 | 
			
		||||
                              Text('fieldChatBelongToRealmUnset')
 | 
			
		||||
                                  .tr()
 | 
			
		||||
                                  .textStyle(
 | 
			
		||||
                                    Theme.of(context).textTheme.bodyMedium!,
 | 
			
		||||
                                  ),
 | 
			
		||||
                            ],
 | 
			
		||||
@@ -257,7 +269,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
                    helperText: 'fieldChatAliasHint'.tr(),
 | 
			
		||||
                    helperMaxLines: 2,
 | 
			
		||||
                  ),
 | 
			
		||||
                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                  onTapOutside: (_) =>
 | 
			
		||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                ),
 | 
			
		||||
                const Gap(4),
 | 
			
		||||
                TextField(
 | 
			
		||||
@@ -266,7 +279,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
                    border: const UnderlineInputBorder(),
 | 
			
		||||
                    labelText: 'fieldChatName'.tr(),
 | 
			
		||||
                  ),
 | 
			
		||||
                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                  onTapOutside: (_) =>
 | 
			
		||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                ),
 | 
			
		||||
                const Gap(4),
 | 
			
		||||
                TextField(
 | 
			
		||||
@@ -277,7 +291,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
                    border: const UnderlineInputBorder(),
 | 
			
		||||
                    labelText: 'fieldChatDescription'.tr(),
 | 
			
		||||
                  ),
 | 
			
		||||
                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                  onTapOutside: (_) =>
 | 
			
		||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                ),
 | 
			
		||||
                const Gap(12),
 | 
			
		||||
                CheckboxListTile(
 | 
			
		||||
 
 | 
			
		||||
@@ -304,6 +304,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
 | 
			
		||||
    final ud = context.read<UserDirectoryProvider>();
 | 
			
		||||
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: Text(
 | 
			
		||||
          _channel?.type == 1
 | 
			
		||||
 
 | 
			
		||||
@@ -157,6 +157,7 @@ class _ExploreScreenState extends State<ExploreScreen>
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final cfg = context.watch<ConfigProvider>();
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      floatingActionButtonLocation: ExpandableFab.location,
 | 
			
		||||
      floatingActionButton: ExpandableFab(
 | 
			
		||||
        key: _fabKey,
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,6 @@ 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_background.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:surface/widgets/post/post_comment_list.dart';
 | 
			
		||||
import 'package:surface/widgets/post/post_item.dart';
 | 
			
		||||
@@ -66,115 +65,111 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
 | 
			
		||||
 | 
			
		||||
    final double maxWidth = _data?.type == 'video' ? double.infinity : 640;
 | 
			
		||||
 | 
			
		||||
    return AppBackground(
 | 
			
		||||
      isRoot: widget.onBack != null,
 | 
			
		||||
      child: AppScaffold(
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
          leading: BackButton(
 | 
			
		||||
            onPressed: () {
 | 
			
		||||
              if (widget.onBack != null) {
 | 
			
		||||
                widget.onBack!.call();
 | 
			
		||||
              }
 | 
			
		||||
              if (GoRouter.of(context).canPop()) {
 | 
			
		||||
                GoRouter.of(context).pop(context);
 | 
			
		||||
                return;
 | 
			
		||||
              }
 | 
			
		||||
              GoRouter.of(context).replaceNamed('explore');
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
          title: _data?.body['title'] != null
 | 
			
		||||
              ? RichText(
 | 
			
		||||
                  textAlign: TextAlign.center,
 | 
			
		||||
                  text: TextSpan(children: [
 | 
			
		||||
                    TextSpan(
 | 
			
		||||
                      text: _data?.body['title'] ?? 'postNoun'.tr(),
 | 
			
		||||
                      style: Theme.of(context).textTheme.titleLarge!.copyWith(
 | 
			
		||||
                            color:
 | 
			
		||||
                                Theme.of(context).appBarTheme.foregroundColor!,
 | 
			
		||||
                          ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    const TextSpan(text: '\n'),
 | 
			
		||||
                    TextSpan(
 | 
			
		||||
                      text: 'postDetail'.tr(),
 | 
			
		||||
                      style: Theme.of(context).textTheme.bodySmall!.copyWith(
 | 
			
		||||
                            color:
 | 
			
		||||
                                Theme.of(context).appBarTheme.foregroundColor!,
 | 
			
		||||
                          ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ]),
 | 
			
		||||
                  maxLines: 2,
 | 
			
		||||
                  overflow: TextOverflow.ellipsis,
 | 
			
		||||
                )
 | 
			
		||||
              : Text('postDetail').tr(),
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: BackButton(
 | 
			
		||||
          onPressed: () {
 | 
			
		||||
            if (widget.onBack != null) {
 | 
			
		||||
              widget.onBack!.call();
 | 
			
		||||
            }
 | 
			
		||||
            if (GoRouter.of(context).canPop()) {
 | 
			
		||||
              GoRouter.of(context).pop(context);
 | 
			
		||||
              return;
 | 
			
		||||
            }
 | 
			
		||||
            GoRouter.of(context).replaceNamed('explore');
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        body: CustomScrollView(
 | 
			
		||||
          slivers: [
 | 
			
		||||
            SliverToBoxAdapter(
 | 
			
		||||
              child: LoadingIndicator(isActive: _isBusy),
 | 
			
		||||
            ),
 | 
			
		||||
            if (_data != null)
 | 
			
		||||
              SliverToBoxAdapter(
 | 
			
		||||
                child: PostItem(
 | 
			
		||||
                  data: _data!,
 | 
			
		||||
                  maxWidth: maxWidth,
 | 
			
		||||
                  showComments: false,
 | 
			
		||||
                  showFullPost: true,
 | 
			
		||||
                  onChanged: (data) {
 | 
			
		||||
                    setState(() => _data = data);
 | 
			
		||||
                  },
 | 
			
		||||
                  onDeleted: () {
 | 
			
		||||
                    Navigator.pop(context);
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            if (_data != null)
 | 
			
		||||
              SliverToBoxAdapter(
 | 
			
		||||
                child: Divider(height: 1).padding(top: 8),
 | 
			
		||||
              ),
 | 
			
		||||
            if (_data != null)
 | 
			
		||||
              SliverToBoxAdapter(
 | 
			
		||||
                child: Container(
 | 
			
		||||
                  constraints: BoxConstraints(maxWidth: maxWidth),
 | 
			
		||||
                  child: Row(
 | 
			
		||||
                    crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                    children: [
 | 
			
		||||
                      const Icon(Symbols.comment, size: 24),
 | 
			
		||||
                      const Gap(16),
 | 
			
		||||
                      Text('postCommentsDetailed')
 | 
			
		||||
                          .plural(_data!.metric.replyCount)
 | 
			
		||||
                          .textStyle(Theme.of(context).textTheme.titleLarge!),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ).padding(horizontal: 20, vertical: 12).center(),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            if (_data != null && ua.isAuthorized)
 | 
			
		||||
              SliverToBoxAdapter(
 | 
			
		||||
                child: PostCommentQuickAction(
 | 
			
		||||
                  parentPost: _data!,
 | 
			
		||||
                  maxWidth: maxWidth,
 | 
			
		||||
                  onPosted: () {
 | 
			
		||||
                    setState(() {
 | 
			
		||||
                      _data = _data!.copyWith(
 | 
			
		||||
                        metric: _data!.metric.copyWith(
 | 
			
		||||
                          replyCount: _data!.metric.replyCount + 1,
 | 
			
		||||
        title: _data?.body['title'] != null
 | 
			
		||||
            ? RichText(
 | 
			
		||||
                textAlign: TextAlign.center,
 | 
			
		||||
                text: TextSpan(children: [
 | 
			
		||||
                  TextSpan(
 | 
			
		||||
                    text: _data?.body['title'] ?? 'postNoun'.tr(),
 | 
			
		||||
                    style: Theme.of(context).textTheme.titleLarge!.copyWith(
 | 
			
		||||
                          color: Theme.of(context).appBarTheme.foregroundColor!,
 | 
			
		||||
                        ),
 | 
			
		||||
                      );
 | 
			
		||||
                    });
 | 
			
		||||
                    _childListKey.currentState!.refresh();
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                  ),
 | 
			
		||||
                  const TextSpan(text: '\n'),
 | 
			
		||||
                  TextSpan(
 | 
			
		||||
                    text: 'postDetail'.tr(),
 | 
			
		||||
                    style: Theme.of(context).textTheme.bodySmall!.copyWith(
 | 
			
		||||
                          color: Theme.of(context).appBarTheme.foregroundColor!,
 | 
			
		||||
                        ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ]),
 | 
			
		||||
                maxLines: 2,
 | 
			
		||||
                overflow: TextOverflow.ellipsis,
 | 
			
		||||
              )
 | 
			
		||||
            : Text('postDetail').tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      body: CustomScrollView(
 | 
			
		||||
        slivers: [
 | 
			
		||||
          SliverToBoxAdapter(
 | 
			
		||||
            child: LoadingIndicator(isActive: _isBusy),
 | 
			
		||||
          ),
 | 
			
		||||
          if (_data != null)
 | 
			
		||||
            SliverToBoxAdapter(
 | 
			
		||||
              child: PostItem(
 | 
			
		||||
                data: _data!,
 | 
			
		||||
                maxWidth: maxWidth,
 | 
			
		||||
                showComments: false,
 | 
			
		||||
                showFullPost: true,
 | 
			
		||||
                onChanged: (data) {
 | 
			
		||||
                  setState(() => _data = data);
 | 
			
		||||
                },
 | 
			
		||||
                onDeleted: () {
 | 
			
		||||
                  Navigator.pop(context);
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
            if (_data != null) SliverGap(8),
 | 
			
		||||
            if (_data != null)
 | 
			
		||||
              PostCommentSliverList(
 | 
			
		||||
                key: _childListKey,
 | 
			
		||||
            ),
 | 
			
		||||
          if (_data != null)
 | 
			
		||||
            SliverToBoxAdapter(
 | 
			
		||||
              child: Divider(height: 1).padding(top: 8),
 | 
			
		||||
            ),
 | 
			
		||||
          if (_data != null)
 | 
			
		||||
            SliverToBoxAdapter(
 | 
			
		||||
              child: Container(
 | 
			
		||||
                constraints: BoxConstraints(maxWidth: maxWidth),
 | 
			
		||||
                child: Row(
 | 
			
		||||
                  crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    const Icon(Symbols.comment, size: 24),
 | 
			
		||||
                    const Gap(16),
 | 
			
		||||
                    Text('postCommentsDetailed')
 | 
			
		||||
                        .plural(_data!.metric.replyCount)
 | 
			
		||||
                        .textStyle(Theme.of(context).textTheme.titleLarge!),
 | 
			
		||||
                  ],
 | 
			
		||||
                ).padding(horizontal: 20, vertical: 12).center(),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          if (_data != null && ua.isAuthorized)
 | 
			
		||||
            SliverToBoxAdapter(
 | 
			
		||||
              child: PostCommentQuickAction(
 | 
			
		||||
                parentPost: _data!,
 | 
			
		||||
                maxWidth: maxWidth,
 | 
			
		||||
                onPosted: () {
 | 
			
		||||
                  setState(() {
 | 
			
		||||
                    _data = _data!.copyWith(
 | 
			
		||||
                      metric: _data!.metric.copyWith(
 | 
			
		||||
                        replyCount: _data!.metric.replyCount + 1,
 | 
			
		||||
                      ),
 | 
			
		||||
                    );
 | 
			
		||||
                  });
 | 
			
		||||
                  _childListKey.currentState!.refresh();
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
            if (_data != null)
 | 
			
		||||
              SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
            ),
 | 
			
		||||
          if (_data != null) SliverGap(8),
 | 
			
		||||
          if (_data != null)
 | 
			
		||||
            PostCommentSliverList(
 | 
			
		||||
              key: _childListKey,
 | 
			
		||||
              parentPost: _data!,
 | 
			
		||||
              maxWidth: maxWidth,
 | 
			
		||||
            ),
 | 
			
		||||
          if (_data != null)
 | 
			
		||||
            SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -286,6 +286,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      body: NestedScrollView(
 | 
			
		||||
        controller: _scrollController,
 | 
			
		||||
        headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,9 @@ class _WalletScreenState extends State<WalletScreen> {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(leading: PageBackButton(), title: Text('screenAccountWallet').tr()),
 | 
			
		||||
      noBackground: true,
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
          leading: PageBackButton(), title: Text('screenAccountWallet').tr()),
 | 
			
		||||
      body: Column(
 | 
			
		||||
        children: [
 | 
			
		||||
          LoadingIndicator(isActive: _isBusy),
 | 
			
		||||
@@ -66,7 +68,9 @@ class _WalletScreenState extends State<WalletScreen> {
 | 
			
		||||
                  SizedBox(width: double.infinity),
 | 
			
		||||
                  Text(
 | 
			
		||||
                    NumberFormat.compactCurrency(
 | 
			
		||||
                      locale: EasyLocalization.of(context)!.currentLocale.toString(),
 | 
			
		||||
                      locale: EasyLocalization.of(context)!
 | 
			
		||||
                          .currentLocale
 | 
			
		||||
                          .toString(),
 | 
			
		||||
                      symbol: '${'walletCurrencyShort'.tr()} ',
 | 
			
		||||
                      decimalDigits: 2,
 | 
			
		||||
                    ).format(double.parse(_wallet!.balance)),
 | 
			
		||||
@@ -76,17 +80,21 @@ class _WalletScreenState extends State<WalletScreen> {
 | 
			
		||||
                  const Gap(16),
 | 
			
		||||
                  Text(
 | 
			
		||||
                    NumberFormat.compactCurrency(
 | 
			
		||||
                      locale: EasyLocalization.of(context)!.currentLocale.toString(),
 | 
			
		||||
                      locale: EasyLocalization.of(context)!
 | 
			
		||||
                          .currentLocale
 | 
			
		||||
                          .toString(),
 | 
			
		||||
                      symbol: '${'walletCurrencyGoldenShort'.tr()} ',
 | 
			
		||||
                      decimalDigits: 2,
 | 
			
		||||
                    ).format(double.parse(_wallet!.goldenBalance)),
 | 
			
		||||
                    style: Theme.of(context).textTheme.titleLarge,
 | 
			
		||||
                  ),
 | 
			
		||||
                  Text('walletCurrencyGolden'.plural(double.parse(_wallet!.goldenBalance))),
 | 
			
		||||
                  Text('walletCurrencyGolden'
 | 
			
		||||
                      .plural(double.parse(_wallet!.goldenBalance))),
 | 
			
		||||
                ],
 | 
			
		||||
              ).padding(horizontal: 20, vertical: 24),
 | 
			
		||||
            ).padding(horizontal: 8, top: 16, bottom: 4),
 | 
			
		||||
          if (_wallet != null) Expanded(child: _WalletTransactionList(myself: _wallet!)),
 | 
			
		||||
          if (_wallet != null)
 | 
			
		||||
            Expanded(child: _WalletTransactionList(myself: _wallet!)),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
@@ -116,7 +124,10 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
 | 
			
		||||
        queryParameters: {'take': 10, 'offset': _transactions.length},
 | 
			
		||||
      );
 | 
			
		||||
      _totalCount = resp.data['count'];
 | 
			
		||||
      _transactions.addAll(resp.data['data']?.map((e) => SnTransaction.fromJson(e)).cast<SnTransaction>() ?? []);
 | 
			
		||||
      _transactions.addAll(resp.data['data']
 | 
			
		||||
              ?.map((e) => SnTransaction.fromJson(e))
 | 
			
		||||
              .cast<SnTransaction>() ??
 | 
			
		||||
          []);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
@@ -141,7 +152,8 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
 | 
			
		||||
        child: InfiniteList(
 | 
			
		||||
          itemCount: _transactions.length,
 | 
			
		||||
          isLoading: _isBusy,
 | 
			
		||||
          hasReachedMax: _totalCount != null && _transactions.length >= _totalCount!,
 | 
			
		||||
          hasReachedMax:
 | 
			
		||||
              _totalCount != null && _transactions.length >= _totalCount!,
 | 
			
		||||
          onFetchData: () {
 | 
			
		||||
            _fetchTransactions();
 | 
			
		||||
          },
 | 
			
		||||
@@ -149,7 +161,9 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
 | 
			
		||||
            final ele = _transactions[idx];
 | 
			
		||||
            final isIncoming = ele.payeeId == widget.myself.id;
 | 
			
		||||
            return ListTile(
 | 
			
		||||
              leading: isIncoming ? const Icon(Symbols.call_received) : const Icon(Symbols.call_made),
 | 
			
		||||
              leading: isIncoming
 | 
			
		||||
                  ? const Icon(Symbols.call_received)
 | 
			
		||||
                  : const Icon(Symbols.call_made),
 | 
			
		||||
              title: Text(
 | 
			
		||||
                '${isIncoming ? '+' : '-'}${ele.amount} ${'walletCurrencyShort'.tr()}',
 | 
			
		||||
                style: TextStyle(color: isIncoming ? Colors.green : Colors.red),
 | 
			
		||||
@@ -162,12 +176,20 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
 | 
			
		||||
                  Row(
 | 
			
		||||
                    children: [
 | 
			
		||||
                      Text(
 | 
			
		||||
                        'walletTransactionType${ele.currency.capitalize()}'.tr(),
 | 
			
		||||
                        'walletTransactionType${ele.currency.capitalize()}'
 | 
			
		||||
                            .tr(),
 | 
			
		||||
                        style: Theme.of(context).textTheme.labelSmall,
 | 
			
		||||
                      ),
 | 
			
		||||
                      Text(' · ').textStyle(Theme.of(context).textTheme.labelSmall!).padding(right: 4),
 | 
			
		||||
                      Text(' · ')
 | 
			
		||||
                          .textStyle(Theme.of(context).textTheme.labelSmall!)
 | 
			
		||||
                          .padding(right: 4),
 | 
			
		||||
                      Text(
 | 
			
		||||
                        DateFormat(null, EasyLocalization.of(context)!.currentLocale.toString()).format(ele.createdAt),
 | 
			
		||||
                        DateFormat(
 | 
			
		||||
                                null,
 | 
			
		||||
                                EasyLocalization.of(context)!
 | 
			
		||||
                                    .currentLocale
 | 
			
		||||
                                    .toString())
 | 
			
		||||
                            .format(ele.createdAt),
 | 
			
		||||
                        style: Theme.of(context).textTheme.labelSmall,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ],
 | 
			
		||||
@@ -199,33 +221,34 @@ class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
 | 
			
		||||
    final TextEditingController passwordController = TextEditingController();
 | 
			
		||||
    final password = await showDialog<String?>(
 | 
			
		||||
      context: context,
 | 
			
		||||
      builder:
 | 
			
		||||
          (ctx) => AlertDialog(
 | 
			
		||||
            title: Text('walletCreate').tr(),
 | 
			
		||||
            content: Column(
 | 
			
		||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
              mainAxisSize: MainAxisSize.min,
 | 
			
		||||
              children: [
 | 
			
		||||
                Text('walletCreatePassword').tr(),
 | 
			
		||||
                const Gap(8),
 | 
			
		||||
                TextField(
 | 
			
		||||
                  autofocus: true,
 | 
			
		||||
                  obscureText: true,
 | 
			
		||||
                  controller: passwordController,
 | 
			
		||||
                  decoration: InputDecoration(labelText: 'fieldPassword'.tr()),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
      builder: (ctx) => AlertDialog(
 | 
			
		||||
        title: Text('walletCreate').tr(),
 | 
			
		||||
        content: Column(
 | 
			
		||||
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
          children: [
 | 
			
		||||
            Text('walletCreatePassword').tr(),
 | 
			
		||||
            const Gap(8),
 | 
			
		||||
            TextField(
 | 
			
		||||
              autofocus: true,
 | 
			
		||||
              obscureText: true,
 | 
			
		||||
              controller: passwordController,
 | 
			
		||||
              decoration: InputDecoration(labelText: 'fieldPassword'.tr()),
 | 
			
		||||
            ),
 | 
			
		||||
            actions: [
 | 
			
		||||
              TextButton(onPressed: () => Navigator.of(ctx).pop(), child: Text('cancel').tr()),
 | 
			
		||||
              TextButton(
 | 
			
		||||
                onPressed: () {
 | 
			
		||||
                  Navigator.of(ctx).pop(passwordController.text);
 | 
			
		||||
                },
 | 
			
		||||
                child: Text('next').tr(),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        actions: [
 | 
			
		||||
          TextButton(
 | 
			
		||||
              onPressed: () => Navigator.of(ctx).pop(),
 | 
			
		||||
              child: Text('cancel').tr()),
 | 
			
		||||
          TextButton(
 | 
			
		||||
            onPressed: () {
 | 
			
		||||
              Navigator.of(ctx).pop(passwordController.text);
 | 
			
		||||
            },
 | 
			
		||||
            child: Text('next').tr(),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
    WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
			
		||||
      passwordController.dispose();
 | 
			
		||||
@@ -257,12 +280,18 @@ class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
 | 
			
		||||
            children: [
 | 
			
		||||
              CircleAvatar(radius: 28, child: Icon(Symbols.add, size: 28)),
 | 
			
		||||
              const Gap(12),
 | 
			
		||||
              Text('walletCreate', style: Theme.of(context).textTheme.titleLarge).tr(),
 | 
			
		||||
              Text('walletCreateSubtitle', style: Theme.of(context).textTheme.bodyMedium).tr(),
 | 
			
		||||
              Text('walletCreate',
 | 
			
		||||
                      style: Theme.of(context).textTheme.titleLarge)
 | 
			
		||||
                  .tr(),
 | 
			
		||||
              Text('walletCreateSubtitle',
 | 
			
		||||
                      style: Theme.of(context).textTheme.bodyMedium)
 | 
			
		||||
                  .tr(),
 | 
			
		||||
              const Gap(8),
 | 
			
		||||
              Align(
 | 
			
		||||
                alignment: Alignment.centerRight,
 | 
			
		||||
                child: TextButton(onPressed: _isBusy ? null : () => _createWallet(), child: Text('next').tr()),
 | 
			
		||||
                child: TextButton(
 | 
			
		||||
                    onPressed: _isBusy ? null : () => _createWallet(),
 | 
			
		||||
                    child: Text('next').tr()),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ).padding(horizontal: 20, vertical: 24),
 | 
			
		||||
 
 | 
			
		||||
@@ -65,7 +65,9 @@ class AppScaffold extends StatelessWidget {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      extendBody: true,
 | 
			
		||||
      extendBodyBehindAppBar: true,
 | 
			
		||||
      backgroundColor: Theme.of(context).scaffoldBackgroundColor,
 | 
			
		||||
      backgroundColor: noBackground
 | 
			
		||||
          ? Colors.transparent
 | 
			
		||||
          : Theme.of(context).scaffoldBackgroundColor,
 | 
			
		||||
      body: SizedBox.expand(
 | 
			
		||||
        child: noBackground
 | 
			
		||||
            ? content
 | 
			
		||||
@@ -238,28 +240,35 @@ class ResponsiveScaffold extends StatelessWidget {
 | 
			
		||||
    this.contentFlex = 2,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  static bool getIsExpand(BuildContext context) {
 | 
			
		||||
    return ResponsiveBreakpoints.of(context).largerOrEqualTo(TABLET);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    if (ResponsiveBreakpoints.of(context).largerOrEqualTo(TABLET)) {
 | 
			
		||||
      return Row(
 | 
			
		||||
        children: [
 | 
			
		||||
          Flexible(
 | 
			
		||||
            flex: asideFlex,
 | 
			
		||||
            child: aside,
 | 
			
		||||
          ),
 | 
			
		||||
          VerticalDivider(width: 1),
 | 
			
		||||
          if (child != null && child != aside)
 | 
			
		||||
            Flexible(flex: contentFlex, child: child!)
 | 
			
		||||
          else
 | 
			
		||||
    if (getIsExpand(context)) {
 | 
			
		||||
      return AppBackground(
 | 
			
		||||
        isRoot: true,
 | 
			
		||||
        child: Row(
 | 
			
		||||
          children: [
 | 
			
		||||
            Flexible(
 | 
			
		||||
              flex: contentFlex,
 | 
			
		||||
              child: ResponsiveScaffoldLanding(child: null),
 | 
			
		||||
              flex: asideFlex,
 | 
			
		||||
              child: aside,
 | 
			
		||||
            ),
 | 
			
		||||
        ],
 | 
			
		||||
            VerticalDivider(width: 1),
 | 
			
		||||
            if (child != null && child != aside)
 | 
			
		||||
              Flexible(flex: contentFlex, child: child!)
 | 
			
		||||
            else
 | 
			
		||||
              Flexible(
 | 
			
		||||
                flex: contentFlex,
 | 
			
		||||
                child: ResponsiveScaffoldLanding(child: null),
 | 
			
		||||
              ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return child ?? aside;
 | 
			
		||||
    return AppBackground(isRoot: true, child: child ?? aside);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -269,9 +278,9 @@ class ResponsiveScaffoldLanding extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    if (ResponsiveBreakpoints.of(context).largerOrEqualTo(TABLET) ||
 | 
			
		||||
        child == null) {
 | 
			
		||||
    if (ResponsiveScaffold.getIsExpand(context) || child == null) {
 | 
			
		||||
      return AppScaffold(
 | 
			
		||||
        noBackground: true,
 | 
			
		||||
        appBar: AppBar(),
 | 
			
		||||
        body: const SizedBox.shrink(),
 | 
			
		||||
      );
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user