Compare commits
	
		
			6 Commits
		
	
	
		
			2.4.2+84
			...
			9311bfc3b5
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 9311bfc3b5 | |||
| 8dd6435a30 | |||
| 21a1d4a2ad | |||
| 603875b1af | |||
| 4209a13c84 | |||
| 55b79bfd8f | 
							
								
								
									
										
											BIN
										
									
								
								assets/icon/kanban-1st.jpg
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/icon/kanban-1st.jpg
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 509 KiB  | 
@@ -890,5 +890,12 @@
 | 
			
		||||
  },
 | 
			
		||||
  "settingsHideBottomNav": "Hide Bottom Navigation",
 | 
			
		||||
  "settingsHideBottomNavDescription": "Hide the bottom navigation bar, and show the navigation buttons in the drawer.",
 | 
			
		||||
  "reCaptcha": "reCaptcha"
 | 
			
		||||
  "reCaptcha": "reCaptcha",
 | 
			
		||||
  "friends": "Friends",
 | 
			
		||||
  "friendsDescription": "Manage your friendships.",
 | 
			
		||||
  "album": "Album",
 | 
			
		||||
  "albumDescription": "View albums and manage attachments.",
 | 
			
		||||
  "stickers": "Stickers",
 | 
			
		||||
  "stickersDescription": "View sticker packs and manage stickers.",
 | 
			
		||||
  "navBottomUnauthorizedCaption": "Or create an account"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -888,5 +888,12 @@
 | 
			
		||||
  },
 | 
			
		||||
  "settingsHideBottomNav": "隐藏底部导航栏",
 | 
			
		||||
  "settingsHideBottomNavDescription": "隐藏底部导航栏,在侧边栏抽屉显示导航按钮。",
 | 
			
		||||
  "reCaptcha": "人机验证"
 | 
			
		||||
  "reCaptcha": "人机验证",
 | 
			
		||||
  "friends": "好友",
 | 
			
		||||
  "friendsDescription": "管理好友关系。",
 | 
			
		||||
  "album": "相册",
 | 
			
		||||
  "albumDescription": "查看相册与管理上传附件。",
 | 
			
		||||
  "stickers": "贴图",
 | 
			
		||||
  "stickersDescription": "查看贴图包与管理贴图。",
 | 
			
		||||
  "navBottomUnauthorizedCaption": "或者注册一个账号"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -888,5 +888,12 @@
 | 
			
		||||
  },
 | 
			
		||||
  "settingsHideBottomNav": "隱藏底部導航欄",
 | 
			
		||||
  "settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。",
 | 
			
		||||
  "reCaptcha": "人機驗證"
 | 
			
		||||
  "reCaptcha": "人機驗證",
 | 
			
		||||
  "friends": "好友",
 | 
			
		||||
  "friendsDescription": "管理好友關係。",
 | 
			
		||||
  "album": "相冊",
 | 
			
		||||
  "albumDescription": "查看相冊與管理上傳附件。",
 | 
			
		||||
  "stickers": "貼圖",
 | 
			
		||||
  "stickersDescription": "查看貼圖包與管理貼圖。",
 | 
			
		||||
  "navBottomUnauthorizedCaption": "或者註冊一個賬號"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -888,5 +888,12 @@
 | 
			
		||||
  },
 | 
			
		||||
  "settingsHideBottomNav": "隱藏底部導航欄",
 | 
			
		||||
  "settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。",
 | 
			
		||||
  "reCaptcha": "人機驗證"
 | 
			
		||||
  "reCaptcha": "人機驗證",
 | 
			
		||||
  "friends": "好友",
 | 
			
		||||
  "friendsDescription": "管理好友關係。",
 | 
			
		||||
  "album": "相冊",
 | 
			
		||||
  "albumDescription": "查看相冊與管理上傳附件。",
 | 
			
		||||
  "stickers": "貼圖",
 | 
			
		||||
  "stickersDescription": "查看貼圖包與管理貼圖。",
 | 
			
		||||
  "navBottomUnauthorizedCaption": "或者註冊一個賬號"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										270
									
								
								lib/main.dart
									
									
									
									
									
								
							
							
						
						
									
										270
									
								
								lib/main.dart
									
									
									
									
									
								
							@@ -1,7 +1,6 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
import 'dart:developer';
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
import 'dart:math' hide log;
 | 
			
		||||
import 'dart:ui';
 | 
			
		||||
 | 
			
		||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
 | 
			
		||||
@@ -90,19 +89,14 @@ void main() async {
 | 
			
		||||
  await EasyLocalization.ensureInitialized();
 | 
			
		||||
 | 
			
		||||
  if (!kIsWeb && !Platform.isLinux) {
 | 
			
		||||
    await Firebase.initializeApp(
 | 
			
		||||
      options: DefaultFirebaseOptions.currentPlatform,
 | 
			
		||||
    );
 | 
			
		||||
    await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  GoRouter.optionURLReflectsImperativeAPIs = true;
 | 
			
		||||
  usePathUrlStrategy();
 | 
			
		||||
 | 
			
		||||
  if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
 | 
			
		||||
    Workmanager().initialize(
 | 
			
		||||
      appBackgroundDispatcher,
 | 
			
		||||
      isInDebugMode: kDebugMode,
 | 
			
		||||
    );
 | 
			
		||||
    Workmanager().initialize(appBackgroundDispatcher, isInDebugMode: kDebugMode);
 | 
			
		||||
    if (Platform.isAndroid) {
 | 
			
		||||
      Workmanager().registerPeriodicTask(
 | 
			
		||||
        "widget-update-random-post",
 | 
			
		||||
@@ -115,8 +109,7 @@ void main() async {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!kIsWeb && Platform.isAndroid) {
 | 
			
		||||
    final ImagePickerPlatform imagePickerImplementation =
 | 
			
		||||
        ImagePickerPlatform.instance;
 | 
			
		||||
    final ImagePickerPlatform imagePickerImplementation = ImagePickerPlatform.instance;
 | 
			
		||||
    if (imagePickerImplementation is ImagePickerAndroid) {
 | 
			
		||||
      imagePickerImplementation.useAndroidPhotoPicker = true;
 | 
			
		||||
    }
 | 
			
		||||
@@ -133,12 +126,7 @@ class SolianApp extends StatelessWidget {
 | 
			
		||||
    return ResponsiveBreakpoints.builder(
 | 
			
		||||
      child: EasyLocalization(
 | 
			
		||||
        path: 'assets/translations',
 | 
			
		||||
        supportedLocales: [
 | 
			
		||||
          Locale('en', 'US'),
 | 
			
		||||
          Locale('zh', 'CN'),
 | 
			
		||||
          Locale('zh', 'TW'),
 | 
			
		||||
          Locale('zh', 'HK'),
 | 
			
		||||
        ],
 | 
			
		||||
        supportedLocales: [Locale('en', 'US'), Locale('zh', 'CN'), Locale('zh', 'TW'), Locale('zh', 'HK')],
 | 
			
		||||
        fallbackLocale: Locale('en', 'US'),
 | 
			
		||||
        useFallbackTranslations: true,
 | 
			
		||||
        assetLoader: JsonAssetLoader(),
 | 
			
		||||
@@ -161,7 +149,7 @@ class SolianApp extends StatelessWidget {
 | 
			
		||||
            Provider(create: (ctx) => SnNetworkProvider(ctx)),
 | 
			
		||||
            Provider(create: (ctx) => UserDirectoryProvider(ctx)),
 | 
			
		||||
            Provider(create: (ctx) => SnAttachmentProvider(ctx)),
 | 
			
		||||
            Provider(create: (ctx) => SnRealmProvider(ctx)),
 | 
			
		||||
            ChangeNotifierProvider(create: (ctx) => SnRealmProvider(ctx)),
 | 
			
		||||
            Provider(create: (ctx) => SnPostContentProvider(ctx)),
 | 
			
		||||
            Provider(create: (ctx) => SnRelationshipProvider(ctx)),
 | 
			
		||||
            Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
 | 
			
		||||
@@ -213,10 +201,7 @@ class _AppDelegate extends StatelessWidget {
 | 
			
		||||
      ],
 | 
			
		||||
      routerConfig: appRouter,
 | 
			
		||||
      builder: (context, child) {
 | 
			
		||||
        return _AppSplashScreen(
 | 
			
		||||
          key: const Key('global-splash-screen'),
 | 
			
		||||
          child: child!,
 | 
			
		||||
        );
 | 
			
		||||
        return _AppSplashScreen(key: const Key('global-splash-screen'), child: child!);
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
@@ -240,8 +225,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
    if (prefs.containsKey('first_boot_time')) {
 | 
			
		||||
      final rawTime = prefs.getString('first_boot_time');
 | 
			
		||||
      final time = DateTime.tryParse(rawTime ?? '');
 | 
			
		||||
      if (time != null &&
 | 
			
		||||
          time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) {
 | 
			
		||||
      if (time != null && time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) {
 | 
			
		||||
        final inAppReview = InAppReview.instance;
 | 
			
		||||
        if (prefs.getBool('rating_requested') == true) return;
 | 
			
		||||
        if (await inAppReview.isAvailable()) {
 | 
			
		||||
@@ -262,30 +246,17 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
      final info = await PackageInfo.fromPlatform();
 | 
			
		||||
      final localVersionString = '${info.version}+${info.buildNumber}';
 | 
			
		||||
      final resp = await Dio(
 | 
			
		||||
        BaseOptions(
 | 
			
		||||
          sendTimeout: const Duration(seconds: 60),
 | 
			
		||||
          receiveTimeout: const Duration(seconds: 60),
 | 
			
		||||
        ),
 | 
			
		||||
      ).get(
 | 
			
		||||
        'https://api.github.com/repos/Solsynth/HyperNet.Surface/releases/latest',
 | 
			
		||||
      );
 | 
			
		||||
        BaseOptions(sendTimeout: const Duration(seconds: 60), receiveTimeout: const Duration(seconds: 60)),
 | 
			
		||||
      ).get('https://api.github.com/repos/Solsynth/HyperNet.Surface/releases/latest');
 | 
			
		||||
      final remoteVersionString = resp.data?['tag_name'] ?? '0.0.0+0';
 | 
			
		||||
      final remoteVersion = Version.parse(remoteVersionString.split('+').first);
 | 
			
		||||
      final localVersion = Version.parse(localVersionString.split('+').first);
 | 
			
		||||
      final remoteBuildNumber =
 | 
			
		||||
          int.tryParse(remoteVersionString.split('+').last) ?? 0;
 | 
			
		||||
      final localBuildNumber =
 | 
			
		||||
          int.tryParse(localVersionString.split('+').last) ?? 0;
 | 
			
		||||
      logging.info(
 | 
			
		||||
          "[Update] Local: $localVersionString, Remote: $remoteVersionString");
 | 
			
		||||
      if ((remoteVersion > localVersion ||
 | 
			
		||||
              remoteBuildNumber > localBuildNumber) &&
 | 
			
		||||
          mounted) {
 | 
			
		||||
      final remoteBuildNumber = int.tryParse(remoteVersionString.split('+').last) ?? 0;
 | 
			
		||||
      final localBuildNumber = int.tryParse(localVersionString.split('+').last) ?? 0;
 | 
			
		||||
      logging.info("[Update] Local: $localVersionString, Remote: $remoteVersionString");
 | 
			
		||||
      if ((remoteVersion > localVersion || remoteBuildNumber > localBuildNumber) && mounted) {
 | 
			
		||||
        final config = context.read<ConfigProvider>();
 | 
			
		||||
        config.setUpdate(
 | 
			
		||||
          remoteVersionString,
 | 
			
		||||
          resp.data?['body'] ?? 'No changelog',
 | 
			
		||||
        );
 | 
			
		||||
        config.setUpdate(remoteVersionString, resp.data?['body'] ?? 'No changelog');
 | 
			
		||||
        logging.info("[Update] Update available: $remoteVersionString");
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
@@ -323,33 +294,39 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
      _setPhaseText('websocket');
 | 
			
		||||
      final ws = context.read<WebSocketProvider>();
 | 
			
		||||
      await ws.tryConnect();
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      _setPhaseText('notification');
 | 
			
		||||
      final notify = context.read<NotificationProvider>();
 | 
			
		||||
      notify.listen();
 | 
			
		||||
      await notify.registerPushNotifications();
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      _setPhaseText('keyPair');
 | 
			
		||||
      final kp = context.read<KeyPairProvider>();
 | 
			
		||||
      await kp.reloadActive();
 | 
			
		||||
      kp.listen();
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      _setPhaseText('stickers');
 | 
			
		||||
      final sticker = context.read<SnStickerProvider>();
 | 
			
		||||
      await sticker.listSticker();
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      _setPhaseText('userDirectory');
 | 
			
		||||
      final ud = context.read<UserDirectoryProvider>();
 | 
			
		||||
      await ud.loadAccountCache();
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      _setPhaseText('realm');
 | 
			
		||||
      final rm = context.read<SnRealmProvider>();
 | 
			
		||||
      await rm.refreshAvailableRealms();
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      _setPhaseText('chat');
 | 
			
		||||
      final ct = context.read<ChatChannelProvider>();
 | 
			
		||||
      await ct.refreshAvailableChannels();
 | 
			
		||||
      _setPhaseText('done');
 | 
			
		||||
      try {
 | 
			
		||||
        if (!mounted) return;
 | 
			
		||||
        _setPhaseText('keyPair');
 | 
			
		||||
        final kp = context.read<KeyPairProvider>();
 | 
			
		||||
        await kp.reloadActive();
 | 
			
		||||
        kp.listen();
 | 
			
		||||
      } catch (_) {}
 | 
			
		||||
      if (ua.isAuthorized) {
 | 
			
		||||
        if (!mounted) return;
 | 
			
		||||
        _setPhaseText('notification');
 | 
			
		||||
        final notify = context.read<NotificationProvider>();
 | 
			
		||||
        notify.listen();
 | 
			
		||||
        try {
 | 
			
		||||
          await notify.registerPushNotifications();
 | 
			
		||||
        } catch (_) {}
 | 
			
		||||
        if (!mounted) return;
 | 
			
		||||
        _setPhaseText('stickers');
 | 
			
		||||
        final sticker = context.read<SnStickerProvider>();
 | 
			
		||||
        await sticker.listSticker();
 | 
			
		||||
        if (!mounted) return;
 | 
			
		||||
        _setPhaseText('userDirectory');
 | 
			
		||||
        final ud = context.read<UserDirectoryProvider>();
 | 
			
		||||
        await ud.loadAccountCache();
 | 
			
		||||
        if (!mounted) return;
 | 
			
		||||
        _setPhaseText('realm');
 | 
			
		||||
        final rm = context.read<SnRealmProvider>();
 | 
			
		||||
        await rm.refreshAvailableRealms();
 | 
			
		||||
        if (!mounted) return;
 | 
			
		||||
        _setPhaseText('chat');
 | 
			
		||||
        final ct = context.read<ChatChannelProvider>();
 | 
			
		||||
        await ct.refreshAvailableChannels();
 | 
			
		||||
        _setPhaseText('done');
 | 
			
		||||
      }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      await context.showErrorDialog(err);
 | 
			
		||||
@@ -367,35 +344,19 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
 | 
			
		||||
  final Menu _appTrayMenu = Menu(
 | 
			
		||||
    items: [
 | 
			
		||||
      MenuItem(
 | 
			
		||||
        key: 'version_label',
 | 
			
		||||
        label: 'Solian',
 | 
			
		||||
        disabled: true,
 | 
			
		||||
      ),
 | 
			
		||||
      MenuItem(key: 'version_label', label: 'Solian', disabled: true),
 | 
			
		||||
      MenuItem.separator(),
 | 
			
		||||
      MenuItem.checkbox(
 | 
			
		||||
        checked: false,
 | 
			
		||||
        key: 'mute_notification',
 | 
			
		||||
        label: 'trayMenuMuteNotification'.tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      MenuItem.checkbox(checked: false, key: 'mute_notification', label: 'trayMenuMuteNotification'.tr()),
 | 
			
		||||
      MenuItem.separator(),
 | 
			
		||||
      MenuItem(
 | 
			
		||||
        key: 'window_show',
 | 
			
		||||
        label: 'trayMenuShow'.tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      MenuItem(
 | 
			
		||||
        key: 'exit',
 | 
			
		||||
        label: 'trayMenuExit'.tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      MenuItem(key: 'window_show', label: 'trayMenuShow'.tr()),
 | 
			
		||||
      MenuItem(key: 'exit', label: 'trayMenuExit'.tr()),
 | 
			
		||||
    ],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  Future<void> _trayInitialization() async {
 | 
			
		||||
    if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
 | 
			
		||||
 | 
			
		||||
    final icon = Platform.isWindows
 | 
			
		||||
        ? 'assets/icon/tray-icon.ico'
 | 
			
		||||
        : 'assets/icon/tray-icon.png';
 | 
			
		||||
    final icon = Platform.isWindows ? 'assets/icon/tray-icon.ico' : 'assets/icon/tray-icon.png';
 | 
			
		||||
    final appVersion = await PackageInfo.fromPlatform();
 | 
			
		||||
 | 
			
		||||
    trayManager.addListener(this);
 | 
			
		||||
@@ -413,10 +374,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
  Future<void> _notifyInitialization() async {
 | 
			
		||||
    if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
 | 
			
		||||
 | 
			
		||||
    await localNotifier.setup(
 | 
			
		||||
      appName: 'Solian',
 | 
			
		||||
      shortcutPolicy: ShortcutPolicy.requireCreate,
 | 
			
		||||
    );
 | 
			
		||||
    await localNotifier.setup(appName: 'Solian', shortcutPolicy: ShortcutPolicy.requireCreate);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  AppLifecycleListener? _appLifecycleListener;
 | 
			
		||||
@@ -427,9 +385,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
 | 
			
		||||
    _isBusy = true;
 | 
			
		||||
    if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) {
 | 
			
		||||
      _appLifecycleListener = AppLifecycleListener(
 | 
			
		||||
        onExitRequested: _onExitRequested,
 | 
			
		||||
      );
 | 
			
		||||
      _appLifecycleListener = AppLifecycleListener(onExitRequested: _onExitRequested);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _trayInitialization();
 | 
			
		||||
@@ -529,44 +485,49 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
              }
 | 
			
		||||
            });
 | 
			
		||||
            return SizeChangedLayoutNotifier(
 | 
			
		||||
              child: _isBusy
 | 
			
		||||
                  ? Material(
 | 
			
		||||
                      key: Key('app-splash-screen-$_isBusy'),
 | 
			
		||||
                      child: Stack(
 | 
			
		||||
                        children: [
 | 
			
		||||
                          CustomPaint(painter: GraphPainter()),
 | 
			
		||||
                          Center(
 | 
			
		||||
                            child: Container(
 | 
			
		||||
                              constraints: const BoxConstraints(
 | 
			
		||||
                                maxWidth: 240,
 | 
			
		||||
                              ),
 | 
			
		||||
                              child: Column(
 | 
			
		||||
                                mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                                children: [
 | 
			
		||||
                                  Image.asset(
 | 
			
		||||
                                    'assets/icon/icon.png',
 | 
			
		||||
                                    width: 64,
 | 
			
		||||
                                    height: 64,
 | 
			
		||||
                                    color:
 | 
			
		||||
                                        Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  Text('Solar Network').bold(),
 | 
			
		||||
                                  AppVersionLabel(),
 | 
			
		||||
                                  Gap(8),
 | 
			
		||||
                                  Text(
 | 
			
		||||
                                    _phaseText,
 | 
			
		||||
                                    textAlign: TextAlign.center,
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  Gap(16),
 | 
			
		||||
                                  const LinearProgressIndicator(),
 | 
			
		||||
                                ],
 | 
			
		||||
              child:
 | 
			
		||||
                  _isBusy
 | 
			
		||||
                      ? Material(
 | 
			
		||||
                        key: Key('app-splash-screen-$_isBusy'),
 | 
			
		||||
                        child: Stack(
 | 
			
		||||
                          children: [
 | 
			
		||||
                            Container(
 | 
			
		||||
                              decoration: BoxDecoration(
 | 
			
		||||
                                image: DecorationImage(
 | 
			
		||||
                                  image: AssetImage('assets/icon/kanban-1st.jpg'),
 | 
			
		||||
                                  fit: BoxFit.cover,
 | 
			
		||||
                                  opacity: 0.1,
 | 
			
		||||
                                ),
 | 
			
		||||
                                color: Theme.of(context).colorScheme.surface,
 | 
			
		||||
                                backgroundBlendMode: BlendMode.darken,
 | 
			
		||||
                              ),
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ),
 | 
			
		||||
                    )
 | 
			
		||||
                  : widget.child,
 | 
			
		||||
                            Center(
 | 
			
		||||
                              child: Container(
 | 
			
		||||
                                constraints: const BoxConstraints(maxWidth: 240),
 | 
			
		||||
                                child: Column(
 | 
			
		||||
                                  mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                                  children: [
 | 
			
		||||
                                    Image.asset(
 | 
			
		||||
                                      'assets/icon/icon.png',
 | 
			
		||||
                                      width: 64,
 | 
			
		||||
                                      height: 64,
 | 
			
		||||
                                      color: Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    Text('Solar Network').bold(),
 | 
			
		||||
                                    AppVersionLabel(),
 | 
			
		||||
                                    Gap(8),
 | 
			
		||||
                                    Text(_phaseText, textAlign: TextAlign.center),
 | 
			
		||||
                                    Gap(16),
 | 
			
		||||
                                    const LinearProgressIndicator(),
 | 
			
		||||
                                  ],
 | 
			
		||||
                                ),
 | 
			
		||||
                              ),
 | 
			
		||||
                            ),
 | 
			
		||||
                          ],
 | 
			
		||||
                        ),
 | 
			
		||||
                      )
 | 
			
		||||
                      : widget.child,
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
@@ -574,44 +535,3 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class GraphPainter extends CustomPainter {
 | 
			
		||||
  final Random random = Random();
 | 
			
		||||
  final int numNodes = 20;
 | 
			
		||||
  final double maxDistance = 100; // Max distance to draw a line
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void paint(Canvas canvas, Size size) {
 | 
			
		||||
    final paintNode = Paint()..color = Colors.white;
 | 
			
		||||
    final paintEdge = Paint()
 | 
			
		||||
      ..color = Colors.white.withOpacity(0.3)
 | 
			
		||||
      ..strokeWidth = 1;
 | 
			
		||||
 | 
			
		||||
    // Generate random points
 | 
			
		||||
    List<Offset> nodes = List.generate(
 | 
			
		||||
      numNodes,
 | 
			
		||||
      (_) => Offset(
 | 
			
		||||
        random.nextDouble() * size.width,
 | 
			
		||||
        random.nextDouble() * size.height,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Draw edges between close nodes
 | 
			
		||||
    for (var i = 0; i < nodes.length; i++) {
 | 
			
		||||
      for (var j = i + 1; j < nodes.length; j++) {
 | 
			
		||||
        double distance = (nodes[i] - nodes[j]).distance;
 | 
			
		||||
        if (distance < maxDistance) {
 | 
			
		||||
          canvas.drawLine(nodes[i], nodes[j], paintEdge);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Draw nodes
 | 
			
		||||
    for (var node in nodes) {
 | 
			
		||||
      canvas.drawCircle(node, 4, paintNode);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  bool shouldRepaint(CustomPainter oldDelegate) => false;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -41,6 +41,11 @@ class ChatChannelProvider extends ChangeNotifier {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void addAvailableChannel(SnChannel channel) {
 | 
			
		||||
    _availableChannels.add(channel);
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _saveChannelToLocal(Iterable<SnChannel> channels) async {
 | 
			
		||||
    await Future.wait(
 | 
			
		||||
      channels.map(
 | 
			
		||||
 
 | 
			
		||||
@@ -61,26 +61,6 @@ class NavigationProvider extends ChangeNotifier {
 | 
			
		||||
      screen: 'news',
 | 
			
		||||
      label: 'screenNews',
 | 
			
		||||
    ),
 | 
			
		||||
    AppNavDestination(
 | 
			
		||||
      icon: Icon(Symbols.emoji_emotions, weight: 400, opticalSize: 20),
 | 
			
		||||
      screen: 'stickers',
 | 
			
		||||
      label: 'screenStickers',
 | 
			
		||||
    ),
 | 
			
		||||
    AppNavDestination(
 | 
			
		||||
      icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
 | 
			
		||||
      screen: 'album',
 | 
			
		||||
      label: 'screenAlbum',
 | 
			
		||||
    ),
 | 
			
		||||
    AppNavDestination(
 | 
			
		||||
      icon: Icon(Symbols.diversity_4, weight: 400, opticalSize: 20),
 | 
			
		||||
      screen: 'friend',
 | 
			
		||||
      label: 'screenFriend',
 | 
			
		||||
    ),
 | 
			
		||||
    AppNavDestination(
 | 
			
		||||
      icon: Icon(Symbols.notifications, weight: 400, opticalSize: 20),
 | 
			
		||||
      screen: 'notification',
 | 
			
		||||
      label: 'screenNotification',
 | 
			
		||||
    ),
 | 
			
		||||
  ];
 | 
			
		||||
  static const List<String> kDefaultPinnedDestination = [
 | 
			
		||||
    'home',
 | 
			
		||||
 
 | 
			
		||||
@@ -48,13 +48,11 @@ class NotificationProvider extends ChangeNotifier {
 | 
			
		||||
    var deviceUuid = await FlutterUdid.consistentUdid;
 | 
			
		||||
 | 
			
		||||
    if (deviceUuid.isEmpty) {
 | 
			
		||||
      logging.warning(
 | 
			
		||||
          '[Push Notification] Unable to active push notifications, couldn\'t get device uuid');
 | 
			
		||||
      logging.warning('[Push Notification] Unable to active push notifications, couldn\'t get device uuid');
 | 
			
		||||
      return;
 | 
			
		||||
    } else {
 | 
			
		||||
      logging.info('[Push Notification] Device UUID is $deviceUuid');
 | 
			
		||||
      logging
 | 
			
		||||
          .info('[Push Notification] Registering device push notifications...');
 | 
			
		||||
      logging.info('[Push Notification] Registering device push notifications...');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (Platform.isIOS || Platform.isMacOS) {
 | 
			
		||||
@@ -66,14 +64,14 @@ class NotificationProvider extends ChangeNotifier {
 | 
			
		||||
    }
 | 
			
		||||
    logging.info('[Push Notification] Device Push Token is $token');
 | 
			
		||||
 | 
			
		||||
    await _sn.client.post(
 | 
			
		||||
      '/cgi/id/notifications/subscription',
 | 
			
		||||
      data: {
 | 
			
		||||
        'provider': provider,
 | 
			
		||||
        'device_token': token,
 | 
			
		||||
        'device_id': deviceUuid,
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
    try {
 | 
			
		||||
      await _sn.client.post(
 | 
			
		||||
        '/cgi/id/notifications/subscription',
 | 
			
		||||
        data: {'provider': provider, 'device_token': token, 'device_id': deviceUuid},
 | 
			
		||||
      );
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      logging.error('[Push Notification] Unable to register push notifications: $err');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int showingCount = 0;
 | 
			
		||||
@@ -91,8 +89,7 @@ class NotificationProvider extends ChangeNotifier {
 | 
			
		||||
        final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
 | 
			
		||||
        if (doHaptic) HapticFeedback.mediumImpact();
 | 
			
		||||
 | 
			
		||||
        if (notification.topic == 'messaging.message' &&
 | 
			
		||||
            skippableNotifyChannel != null) {
 | 
			
		||||
        if (notification.topic == 'messaging.message' && skippableNotifyChannel != null) {
 | 
			
		||||
          if (notification.metadata['channel_id'] != null &&
 | 
			
		||||
              notification.metadata['channel_id'] == skippableNotifyChannel) {
 | 
			
		||||
            return;
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ import 'package:surface/providers/database.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/types/realm.dart';
 | 
			
		||||
 | 
			
		||||
class SnRealmProvider {
 | 
			
		||||
class SnRealmProvider extends ChangeNotifier {
 | 
			
		||||
  late final SnNetworkProvider _sn;
 | 
			
		||||
  late final DatabaseProvider _dt;
 | 
			
		||||
 | 
			
		||||
@@ -39,6 +39,11 @@ class SnRealmProvider {
 | 
			
		||||
    return out;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void addAvailableRealm(SnRealm realm) {
 | 
			
		||||
    _availableRealms.add(realm);
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<SnRealm> getRealm(dynamic aliasOrId) async {
 | 
			
		||||
    if (_cache.containsKey(aliasOrId.toString())) {
 | 
			
		||||
      return _cache[aliasOrId.toString()]!;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,7 @@ import 'package:crypto/crypto.dart';
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:surface/logger.dart';
 | 
			
		||||
 | 
			
		||||
// TODO self host translate api
 | 
			
		||||
const kTranslateApiBaseUrl = 'https://translate.disroot.org';
 | 
			
		||||
const kTranslateApiBaseUrl = 'https://translate.solsynth.dev';
 | 
			
		||||
 | 
			
		||||
class SnTranslator {
 | 
			
		||||
  final Dio client = Dio(
 | 
			
		||||
 
 | 
			
		||||
@@ -64,6 +64,7 @@ class UserProvider extends ChangeNotifier {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<SnAccount?> refreshUser() async {
 | 
			
		||||
    if (!isAuthorized) return null;
 | 
			
		||||
    final resp = await _sn.client.get('/cgi/id/users/me');
 | 
			
		||||
    final out = SnAccount.fromJson(resp.data);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -30,19 +30,7 @@ class AccountScreen extends StatelessWidget {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: AutoAppBarLeading(),
 | 
			
		||||
        title: Text(
 | 
			
		||||
          "screenAccount",
 | 
			
		||||
          style: TextStyle(
 | 
			
		||||
            color: Colors.white,
 | 
			
		||||
            shadows: [
 | 
			
		||||
              Shadow(
 | 
			
		||||
                offset: Offset(1, 1),
 | 
			
		||||
                blurRadius: 5.0,
 | 
			
		||||
                color: Color.fromARGB(255, 0, 0, 0),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ).tr(),
 | 
			
		||||
        title: Text("screenAccount").tr(),
 | 
			
		||||
        flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
 | 
			
		||||
            ? Stack(
 | 
			
		||||
                fit: StackFit.expand,
 | 
			
		||||
@@ -158,23 +146,33 @@ class _AuthorizedAccountScreen extends StatelessWidget {
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text('abuseReport').tr(),
 | 
			
		||||
          subtitle: Text('abuseReportActionDescription').tr(),
 | 
			
		||||
          title: Text('friends').tr(),
 | 
			
		||||
          subtitle: Text('friendsDescription').tr(),
 | 
			
		||||
          contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
          leading: const Icon(Symbols.flag),
 | 
			
		||||
          leading: const Icon(Symbols.person),
 | 
			
		||||
          trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            GoRouter.of(context).pushNamed('abuseReport');
 | 
			
		||||
            GoRouter.of(context).pushNamed('friend');
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text('factorSettings').tr(),
 | 
			
		||||
          subtitle: Text('factorSettingsSubtitle').tr(),
 | 
			
		||||
          title: Text('album').tr(),
 | 
			
		||||
          subtitle: Text('albumDescription').tr(),
 | 
			
		||||
          contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
          leading: const Icon(Symbols.lock),
 | 
			
		||||
          leading: const Icon(Symbols.photo_library),
 | 
			
		||||
          trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            GoRouter.of(context).pushNamed('factorSettings');
 | 
			
		||||
            GoRouter.of(context).pushNamed('album');
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text('stickers').tr(),
 | 
			
		||||
          subtitle: Text('stickersDescription').tr(),
 | 
			
		||||
          contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
          leading: const Icon(Symbols.emoji_emotions),
 | 
			
		||||
          trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            GoRouter.of(context).pushNamed('stickers');
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
@@ -237,6 +235,16 @@ class _AuthorizedAccountScreen extends StatelessWidget {
 | 
			
		||||
            GoRouter.of(context).pushNamed('accountSettings');
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text('abuseReport').tr(),
 | 
			
		||||
          subtitle: Text('abuseReportActionDescription').tr(),
 | 
			
		||||
          contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
          leading: const Icon(Symbols.flag),
 | 
			
		||||
          trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            GoRouter.of(context).pushNamed('abuseReport');
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text('accountLogout').tr(),
 | 
			
		||||
          subtitle: Text('accountLogoutSubtitle').tr(),
 | 
			
		||||
@@ -298,9 +306,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
 | 
			
		||||
            GoRouter.of(context).pushNamed('authLogin').then((value) {
 | 
			
		||||
              if (value == true && context.mounted) {
 | 
			
		||||
                final ua = context.read<UserProvider>();
 | 
			
		||||
                context.showSnackbar('loginSuccess'.tr(args: [
 | 
			
		||||
                  '@${ua.user?.name} (${ua.user?.nick})',
 | 
			
		||||
                ]));
 | 
			
		||||
                ua.refreshUser();
 | 
			
		||||
              }
 | 
			
		||||
            });
 | 
			
		||||
          },
 | 
			
		||||
 
 | 
			
		||||
@@ -117,6 +117,16 @@ class AccountSettingsScreen extends StatelessWidget {
 | 
			
		||||
                GoRouter.of(context).pushNamed('accountSettingsSecurity');
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              title: Text('factorSettings').tr(),
 | 
			
		||||
              subtitle: Text('factorSettingsSubtitle').tr(),
 | 
			
		||||
              contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
              leading: const Icon(Symbols.lock),
 | 
			
		||||
              trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
              onTap: () {
 | 
			
		||||
                GoRouter.of(context).pushNamed('factorSettings');
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              title: Text('accountProfileEdit').tr(),
 | 
			
		||||
              subtitle: Text('accountProfileEditSubtitle').tr(),
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,6 @@ import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/providers/user_directory.dart';
 | 
			
		||||
import 'package:surface/types/attachment.dart';
 | 
			
		||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
			
		||||
import 'package:surface/widgets/attachment/attachment_zoom.dart';
 | 
			
		||||
import 'package:surface/widgets/attachment/attachment_item.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
@@ -106,7 +105,7 @@ class _AlbumScreenState extends State<AlbumScreen> {
 | 
			
		||||
        controller: _scrollController,
 | 
			
		||||
        slivers: [
 | 
			
		||||
          SliverAppBar(
 | 
			
		||||
            leading: AutoAppBarLeading(),
 | 
			
		||||
            leading: PageBackButton(),
 | 
			
		||||
            title: Text('screenAlbum').tr(),
 | 
			
		||||
          ),
 | 
			
		||||
          SliverToBoxAdapter(
 | 
			
		||||
@@ -119,7 +118,8 @@ class _AlbumScreenState extends State<AlbumScreen> {
 | 
			
		||||
                    child: CircularProgressIndicator(
 | 
			
		||||
                      value: _billing?.includedRatio ?? 0,
 | 
			
		||||
                      strokeWidth: 8,
 | 
			
		||||
                      backgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
			
		||||
                      backgroundColor:
 | 
			
		||||
                          Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
			
		||||
                    ),
 | 
			
		||||
                  ).padding(all: 12),
 | 
			
		||||
                  const Gap(24),
 | 
			
		||||
@@ -129,7 +129,8 @@ class _AlbumScreenState extends State<AlbumScreen> {
 | 
			
		||||
                      children: [
 | 
			
		||||
                        Text('attachmentBillingUploaded').tr().bold(),
 | 
			
		||||
                        Text(
 | 
			
		||||
                          (_billing?.currentBytes ?? 0).formatBytes(decimals: 4),
 | 
			
		||||
                          (_billing?.currentBytes ?? 0)
 | 
			
		||||
                              .formatBytes(decimals: 4),
 | 
			
		||||
                          style: GoogleFonts.robotoMono(),
 | 
			
		||||
                        ),
 | 
			
		||||
                        Text('attachmentBillingDiscount').tr().bold(),
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
 | 
			
		||||
 | 
			
		||||
    final captchaTk = await Navigator.of(context, rootNavigator: true).push(
 | 
			
		||||
      MaterialPageRoute(
 | 
			
		||||
        builder: (context) => TurnstileScreen(),
 | 
			
		||||
        builder: (context) => CaptchaScreen(),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
    if (captchaTk == null) return;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,67 @@
 | 
			
		||||
import 'dart:html' as html;
 | 
			
		||||
import 'dart:ui_web' as ui;
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart' show kIsWeb;
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:surface/providers/config.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
 | 
			
		||||
class TurnstileScreen extends StatefulWidget {
 | 
			
		||||
  const TurnstileScreen({
 | 
			
		||||
class CaptchaScreen extends StatefulWidget {
 | 
			
		||||
  const CaptchaScreen({
 | 
			
		||||
    super.key,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<TurnstileScreen> createState() => _TurnstileScreenState();
 | 
			
		||||
  State<CaptchaScreen> createState() => _CaptchaScreenState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _TurnstileScreenState extends State<TurnstileScreen> {
 | 
			
		||||
class _CaptchaScreenState extends State<CaptchaScreen> {
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    if (kIsWeb) {
 | 
			
		||||
      _setupWebListener();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _setupWebListener() {
 | 
			
		||||
    html.window.onMessage.listen((event) {
 | 
			
		||||
      if (event.data != null && event.data is String) {
 | 
			
		||||
        final message = event.data as String;
 | 
			
		||||
        if (message.startsWith("captcha_tk=")) {
 | 
			
		||||
          String token = message.replaceFirst("captcha_tk=", "");
 | 
			
		||||
          Navigator.pop(context, token);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Create an iframe for the captcha page
 | 
			
		||||
    final iframe = html.IFrameElement()
 | 
			
		||||
      ..src = '${context.read<ConfigProvider>().serverUrl}/captcha?redirect_uri=solink://captcha'
 | 
			
		||||
      ..style.border = 'none'
 | 
			
		||||
      ..width = '100%'
 | 
			
		||||
      ..height = '100%';
 | 
			
		||||
 | 
			
		||||
    html.document.body!.append(iframe);
 | 
			
		||||
    ui.platformViewRegistry.registerViewFactory(
 | 
			
		||||
      'captcha-iframe',
 | 
			
		||||
      (int viewId) => iframe,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final cfg = context.read<ConfigProvider>();
 | 
			
		||||
 | 
			
		||||
    if (kIsWeb) {
 | 
			
		||||
      return AppScaffold(
 | 
			
		||||
        appBar: AppBar(title: Text("reCaptcha").tr()),
 | 
			
		||||
        body: HtmlElementView(viewType: 'captcha-iframe'),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(title: Text("reCaptcha").tr()),
 | 
			
		||||
      body: InAppWebView(
 | 
			
		||||
 
 | 
			
		||||
@@ -201,7 +201,7 @@ class _FriendScreenState extends State<FriendScreen> {
 | 
			
		||||
    if (!ua.isAuthorized) {
 | 
			
		||||
      return AppScaffold(
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
          leading: AutoAppBarLeading(),
 | 
			
		||||
          leading: PageBackButton(),
 | 
			
		||||
          title: Text('screenFriend').tr(),
 | 
			
		||||
        ),
 | 
			
		||||
        body: Center(
 | 
			
		||||
@@ -254,7 +254,8 @@ class _FriendScreenState extends State<FriendScreen> {
 | 
			
		||||
              trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
              onTap: _showBlocks,
 | 
			
		||||
            ),
 | 
			
		||||
          if (_requests.isNotEmpty || _blocks.isNotEmpty) const Divider(height: 1),
 | 
			
		||||
          if (_requests.isNotEmpty || _blocks.isNotEmpty)
 | 
			
		||||
            const Divider(height: 1),
 | 
			
		||||
          Expanded(
 | 
			
		||||
            child: MediaQuery.removePadding(
 | 
			
		||||
              context: context,
 | 
			
		||||
@@ -270,7 +271,8 @@ class _FriendScreenState extends State<FriendScreen> {
 | 
			
		||||
                    final relation = _relations[index];
 | 
			
		||||
                    final other = relation.related;
 | 
			
		||||
                    return ListTile(
 | 
			
		||||
                      contentPadding: const EdgeInsets.only(right: 24, left: 16),
 | 
			
		||||
                      contentPadding:
 | 
			
		||||
                          const EdgeInsets.only(right: 24, left: 16),
 | 
			
		||||
                      leading: AccountImage(content: other?.avatar),
 | 
			
		||||
                      title: Text(other?.nick ?? 'unknown'),
 | 
			
		||||
                      subtitle: Text(other?.nick ?? 'unknown'),
 | 
			
		||||
@@ -286,12 +288,16 @@ class _FriendScreenState extends State<FriendScreen> {
 | 
			
		||||
                              mainAxisAlignment: MainAxisAlignment.end,
 | 
			
		||||
                              children: [
 | 
			
		||||
                                InkWell(
 | 
			
		||||
                                  onTap: _isUpdating ? null : () => _changeRelation(relation, 2),
 | 
			
		||||
                                  onTap: _isUpdating
 | 
			
		||||
                                      ? null
 | 
			
		||||
                                      : () => _changeRelation(relation, 2),
 | 
			
		||||
                                  child: Text('friendBlock').tr(),
 | 
			
		||||
                                ),
 | 
			
		||||
                                const Gap(8),
 | 
			
		||||
                                InkWell(
 | 
			
		||||
                                  onTap: _isUpdating ? null : () => _deleteRelation(relation),
 | 
			
		||||
                                  onTap: _isUpdating
 | 
			
		||||
                                      ? null
 | 
			
		||||
                                      : () => _deleteRelation(relation),
 | 
			
		||||
                                  child: Text('friendDeleteAction').tr(),
 | 
			
		||||
                                ),
 | 
			
		||||
                              ],
 | 
			
		||||
@@ -420,7 +426,9 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
 | 
			
		||||
              mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
              crossAxisAlignment: CrossAxisAlignment.end,
 | 
			
		||||
              children: [
 | 
			
		||||
                Text(kFriendStatus[relation.status] ?? 'unknown').tr().opacity(0.75),
 | 
			
		||||
                Text(kFriendStatus[relation.status] ?? 'unknown')
 | 
			
		||||
                    .tr()
 | 
			
		||||
                    .opacity(0.75),
 | 
			
		||||
                if (relation.status == 0)
 | 
			
		||||
                  Row(
 | 
			
		||||
                    mainAxisAlignment: MainAxisAlignment.end,
 | 
			
		||||
@@ -441,7 +449,8 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
 | 
			
		||||
                    mainAxisAlignment: MainAxisAlignment.end,
 | 
			
		||||
                    children: [
 | 
			
		||||
                      InkWell(
 | 
			
		||||
                        onTap: _isBusy ? null : () => _changeRelation(relation, 1),
 | 
			
		||||
                        onTap:
 | 
			
		||||
                            _isBusy ? null : () => _changeRelation(relation, 1),
 | 
			
		||||
                        child: Text('friendUnblock').tr(),
 | 
			
		||||
                      ),
 | 
			
		||||
                      const Gap(8),
 | 
			
		||||
 
 | 
			
		||||
@@ -511,7 +511,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
 | 
			
		||||
  Future<void> _doCheckIn() async {
 | 
			
		||||
    final captchaTk = await Navigator.of(context, rootNavigator: true).push(
 | 
			
		||||
      MaterialPageRoute(
 | 
			
		||||
        builder: (context) => TurnstileScreen(),
 | 
			
		||||
        builder: (context) => CaptchaScreen(),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
    if (captchaTk == null) return;
 | 
			
		||||
@@ -806,7 +806,7 @@ class _HomeDashNotificationWidgetState
 | 
			
		||||
              child: IconButton(
 | 
			
		||||
                icon: const Icon(Symbols.arrow_right_alt),
 | 
			
		||||
                onPressed: () {
 | 
			
		||||
                  GoRouter.of(context).goNamed('notification');
 | 
			
		||||
                  GoRouter.of(context).pushNamed('notification');
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
 
 | 
			
		||||
@@ -149,8 +149,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
 | 
			
		||||
    if (!ua.isAuthorized) {
 | 
			
		||||
      return AppScaffold(
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
            leading: AutoAppBarLeading(),
 | 
			
		||||
            title: Text('screenNotification').tr()),
 | 
			
		||||
          leading: PageBackButton(),
 | 
			
		||||
          title: Text('screenNotification').tr(),
 | 
			
		||||
        ),
 | 
			
		||||
        body: Center(child: UnauthorizedHint()),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,10 @@ import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/channel.dart';
 | 
			
		||||
import 'package:surface/providers/config.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/providers/sn_realm.dart';
 | 
			
		||||
import 'package:surface/providers/userinfo.dart';
 | 
			
		||||
import 'package:surface/types/chat.dart';
 | 
			
		||||
import 'package:surface/types/realm.dart';
 | 
			
		||||
@@ -57,7 +59,9 @@ class _RealmDiscoveryScreenState extends State<RealmDiscoveryScreen> {
 | 
			
		||||
        title: Text('screenRealmDiscovery').tr(),
 | 
			
		||||
        actions: [
 | 
			
		||||
          IconButton(
 | 
			
		||||
            icon: _isCompactView ? const Icon(Symbols.view_list) : const Icon(Symbols.view_module),
 | 
			
		||||
            icon: _isCompactView
 | 
			
		||||
                ? const Icon(Symbols.view_list)
 | 
			
		||||
                : const Icon(Symbols.view_module),
 | 
			
		||||
            onPressed: () {
 | 
			
		||||
              setState(() => _isCompactView = !_isCompactView);
 | 
			
		||||
              context.read<ConfigProvider>().realmCompactView = _isCompactView;
 | 
			
		||||
@@ -117,7 +121,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
 | 
			
		||||
    try {
 | 
			
		||||
      setState(() => _isBusy = true);
 | 
			
		||||
      final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
      final resp = await sn.client.get('/cgi/im/channels/${widget.realm.alias}/public');
 | 
			
		||||
      final resp =
 | 
			
		||||
          await sn.client.get('/cgi/im/channels/${widget.realm.alias}/public');
 | 
			
		||||
      final out = List<SnChannel>.from(
 | 
			
		||||
        resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
 | 
			
		||||
      );
 | 
			
		||||
@@ -135,10 +140,13 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
 | 
			
		||||
      setState(() => _isJoining = true);
 | 
			
		||||
      final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
      final ua = context.read<UserProvider>();
 | 
			
		||||
      await sn.client.post('/cgi/id/realms/${widget.realm.alias}/members', data: {
 | 
			
		||||
      final rel = context.read<SnRealmProvider>();
 | 
			
		||||
      await sn.client
 | 
			
		||||
          .post('/cgi/id/realms/${widget.realm.alias}/members', data: {
 | 
			
		||||
        'related': ua.user?.name,
 | 
			
		||||
      });
 | 
			
		||||
      await _joinSelectedChannels();
 | 
			
		||||
      rel.addAvailableRealm(widget.realm);
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showSnackbar('realmJoined'.tr(args: [widget.realm.name]));
 | 
			
		||||
      Navigator.pop(context);
 | 
			
		||||
@@ -156,13 +164,20 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
 | 
			
		||||
      try {
 | 
			
		||||
        final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
        final ua = context.read<UserProvider>();
 | 
			
		||||
        await sn.client.post('/cgi/im/channels/${widget.realm.alias}/$channel/members', data: {
 | 
			
		||||
          'related': ua.user?.name,
 | 
			
		||||
        });
 | 
			
		||||
        await sn.client.post(
 | 
			
		||||
            '/cgi/im/channels/${widget.realm.alias}/$channel/members',
 | 
			
		||||
            data: {
 | 
			
		||||
              'related': ua.user?.name,
 | 
			
		||||
            });
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        if (!mounted) return;
 | 
			
		||||
        context.showErrorDialog(err);
 | 
			
		||||
      }
 | 
			
		||||
      final ct = context.read<ChatChannelProvider>();
 | 
			
		||||
      for (final channel
 | 
			
		||||
          in _channels!.where((ele) => _planJoinChannels.contains(ele.alias))) {
 | 
			
		||||
        ct.addAvailableChannel(channel);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -182,7 +197,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
 | 
			
		||||
          children: [
 | 
			
		||||
            const Icon(Symbols.group_add, size: 24),
 | 
			
		||||
            const Gap(16),
 | 
			
		||||
            Text('realmJoin', style: Theme.of(context).textTheme.titleLarge).tr(),
 | 
			
		||||
            Text('realmJoin', style: Theme.of(context).textTheme.titleLarge)
 | 
			
		||||
                .tr(),
 | 
			
		||||
          ],
 | 
			
		||||
        ).padding(horizontal: 20, top: 16, bottom: 12),
 | 
			
		||||
        Row(
 | 
			
		||||
@@ -216,7 +232,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
 | 
			
		||||
        Container(
 | 
			
		||||
          width: double.infinity,
 | 
			
		||||
          color: Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
			
		||||
          child: Text('realmCommunityPublicChannelsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium)
 | 
			
		||||
          child: Text('realmCommunityPublicChannelsHint'.tr(),
 | 
			
		||||
                  style: Theme.of(context).textTheme.bodyMedium)
 | 
			
		||||
              .padding(horizontal: 24, vertical: 8),
 | 
			
		||||
        ),
 | 
			
		||||
        Expanded(
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,6 @@ import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/providers/sn_sticker.dart';
 | 
			
		||||
import 'package:surface/providers/userinfo.dart';
 | 
			
		||||
import 'package:surface/types/attachment.dart';
 | 
			
		||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
			
		||||
import 'package:surface/widgets/attachment/attachment_item.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
@@ -134,7 +133,7 @@ class _StickerScreenState extends State<StickerScreen>
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: AutoAppBarLeading(),
 | 
			
		||||
        leading: PageBackButton(),
 | 
			
		||||
        title: Text('screenStickers').tr(),
 | 
			
		||||
        actions: [
 | 
			
		||||
          IconButton(
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,12 @@ class ConnectionIndicator extends StatelessWidget {
 | 
			
		||||
    final ws = context.watch<WebSocketProvider>();
 | 
			
		||||
    final cfg = context.watch<ConfigProvider>();
 | 
			
		||||
 | 
			
		||||
    final marginLeft = cfg.drawerIsCollapsed ? 0.0 : cfg.drawerIsExpanded ? 304.0 : 80.0;
 | 
			
		||||
    final marginLeft =
 | 
			
		||||
        cfg.drawerIsCollapsed
 | 
			
		||||
            ? 0.0
 | 
			
		||||
            : cfg.drawerIsExpanded
 | 
			
		||||
            ? 304.0
 | 
			
		||||
            : 80.0;
 | 
			
		||||
 | 
			
		||||
    return ListenableBuilder(
 | 
			
		||||
      listenable: ws,
 | 
			
		||||
@@ -32,37 +37,39 @@ class ConnectionIndicator extends StatelessWidget {
 | 
			
		||||
                elevation: 2,
 | 
			
		||||
                shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
 | 
			
		||||
                color: Theme.of(context).colorScheme.secondaryContainer,
 | 
			
		||||
                child: ua.isAuthorized
 | 
			
		||||
                    ? Row(
 | 
			
		||||
                        mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                        mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
                        crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                        children: [
 | 
			
		||||
                          if (ws.isBusy)
 | 
			
		||||
                            Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
 | 
			
		||||
                          else if (!ws.isConnected)
 | 
			
		||||
                            Text('serverDisconnected')
 | 
			
		||||
                                .tr()
 | 
			
		||||
                                .textColor(Theme.of(context).colorScheme.onSecondaryContainer)
 | 
			
		||||
                          else
 | 
			
		||||
                            Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
 | 
			
		||||
                          const Gap(8),
 | 
			
		||||
                          if (ws.isBusy)
 | 
			
		||||
                            const CircularProgressIndicator(strokeWidth: 2.5)
 | 
			
		||||
                                .width(12)
 | 
			
		||||
                                .height(12)
 | 
			
		||||
                                .padding(horizontal: 4, right: 4)
 | 
			
		||||
                          else if (!ws.isConnected)
 | 
			
		||||
                            const Icon(Symbols.power_off, size: 18)
 | 
			
		||||
                          else
 | 
			
		||||
                            const Icon(Symbols.power, size: 18),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ).padding(horizontal: 8, vertical: 4)
 | 
			
		||||
                    : const SizedBox.shrink(),
 | 
			
		||||
              ).opacity(show ? 1 : 0, animate: true).animate(
 | 
			
		||||
                    const Duration(milliseconds: 300),
 | 
			
		||||
                    Curves.easeInOut,
 | 
			
		||||
                  ),
 | 
			
		||||
                child:
 | 
			
		||||
                    ua.isAuthorized
 | 
			
		||||
                        ? Row(
 | 
			
		||||
                          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                          mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
                          crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                          children: [
 | 
			
		||||
                            if (ws.isBusy)
 | 
			
		||||
                              Text(
 | 
			
		||||
                                'serverConnecting',
 | 
			
		||||
                              ).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
 | 
			
		||||
                            else if (!ws.isConnected)
 | 
			
		||||
                              Text(
 | 
			
		||||
                                'serverDisconnected',
 | 
			
		||||
                              ).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
 | 
			
		||||
                            else
 | 
			
		||||
                              Text(
 | 
			
		||||
                                'serverConnected',
 | 
			
		||||
                              ).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
 | 
			
		||||
                            const Gap(8),
 | 
			
		||||
                            if (ws.isBusy)
 | 
			
		||||
                              const CircularProgressIndicator(
 | 
			
		||||
                                strokeWidth: 2.5,
 | 
			
		||||
                                padding: EdgeInsets.zero,
 | 
			
		||||
                              ).width(12).height(12).padding(horizontal: 4, right: 4)
 | 
			
		||||
                            else if (!ws.isConnected)
 | 
			
		||||
                              const Icon(Symbols.power_off, size: 18)
 | 
			
		||||
                            else
 | 
			
		||||
                              const Icon(Symbols.power, size: 18),
 | 
			
		||||
                          ],
 | 
			
		||||
                        ).padding(horizontal: 8, vertical: 4)
 | 
			
		||||
                        : const SizedBox.shrink(),
 | 
			
		||||
              ).opacity(show ? 1 : 0, animate: true).animate(const Duration(milliseconds: 300), Curves.easeInOut),
 | 
			
		||||
              onTap: () {
 | 
			
		||||
                if (!ws.isConnected && !ws.isBusy) {
 | 
			
		||||
                  ws.connect();
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import 'dart:io';
 | 
			
		||||
 | 
			
		||||
import 'package:animations/animations.dart';
 | 
			
		||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
 | 
			
		||||
import 'package:collection/collection.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
@@ -9,6 +10,7 @@ import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:responsive_framework/responsive_framework.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/channel.dart';
 | 
			
		||||
import 'package:surface/providers/config.dart';
 | 
			
		||||
@@ -46,6 +48,17 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
 | 
			
		||||
    final nav = context.watch<NavigationProvider>();
 | 
			
		||||
    final cfg = context.watch<ConfigProvider>();
 | 
			
		||||
 | 
			
		||||
    final routeName = GoRouter.of(context)
 | 
			
		||||
        .routerDelegate
 | 
			
		||||
        .currentConfiguration
 | 
			
		||||
        .last
 | 
			
		||||
        .route
 | 
			
		||||
        .name;
 | 
			
		||||
    final showNavButtons = cfg.hideBottomNav ||
 | 
			
		||||
        !(nav.showBottomNavScreen.contains(routeName)
 | 
			
		||||
            ? ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
 | 
			
		||||
            : false);
 | 
			
		||||
 | 
			
		||||
    final backgroundColor = cfg.drawerIsExpanded ? Colors.transparent : null;
 | 
			
		||||
 | 
			
		||||
    return ListenableBuilder(
 | 
			
		||||
@@ -54,6 +67,7 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
 | 
			
		||||
        return Drawer(
 | 
			
		||||
          elevation: widget.elevation,
 | 
			
		||||
          backgroundColor: backgroundColor,
 | 
			
		||||
          shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(0))),
 | 
			
		||||
          child: Column(
 | 
			
		||||
            mainAxisSize: MainAxisSize.max,
 | 
			
		||||
            crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
@@ -78,49 +92,70 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
 | 
			
		||||
              Expanded(
 | 
			
		||||
                child: _DrawerContentList(),
 | 
			
		||||
              ),
 | 
			
		||||
              if (cfg.hideBottomNav)
 | 
			
		||||
              if (showNavButtons)
 | 
			
		||||
                Row(
 | 
			
		||||
                  spacing: 8,
 | 
			
		||||
                  children: nav.destinations.where((ele) => ele.isPinned).map(
 | 
			
		||||
                    (ele) {
 | 
			
		||||
                  children:
 | 
			
		||||
                      nav.destinations.where((ele) => ele.isPinned).mapIndexed(
 | 
			
		||||
                    (idx, ele) {
 | 
			
		||||
                      return Expanded(
 | 
			
		||||
                        child: Tooltip(
 | 
			
		||||
                          message: ele.label.tr(),
 | 
			
		||||
                          child: IconButton.filledTonal(
 | 
			
		||||
                          child: IconButton(
 | 
			
		||||
                            icon: ele.icon,
 | 
			
		||||
                            color: Theme.of(context)
 | 
			
		||||
                                .colorScheme
 | 
			
		||||
                                .onPrimaryContainer,
 | 
			
		||||
                            color: nav.currentIndex == idx
 | 
			
		||||
                                ? Theme.of(context)
 | 
			
		||||
                                    .colorScheme
 | 
			
		||||
                                    .onPrimaryContainer
 | 
			
		||||
                                : Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
                            style: ButtonStyle(
 | 
			
		||||
                              backgroundColor: WidgetStatePropertyAll(
 | 
			
		||||
                                nav.currentIndex == idx
 | 
			
		||||
                                    ? Theme.of(context)
 | 
			
		||||
                                        .colorScheme
 | 
			
		||||
                                        .primaryContainer
 | 
			
		||||
                                    : Colors.transparent,
 | 
			
		||||
                              ),
 | 
			
		||||
                            ),
 | 
			
		||||
                            onPressed: () {
 | 
			
		||||
                              GoRouter.of(context).goNamed(ele.screen);
 | 
			
		||||
                              Scaffold.of(context).closeDrawer();
 | 
			
		||||
                              nav.setIndex(idx);
 | 
			
		||||
                            },
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      );
 | 
			
		||||
                    },
 | 
			
		||||
                  ).toList(),
 | 
			
		||||
                ).padding(horizontal: 16),
 | 
			
		||||
                ).padding(horizontal: 16, bottom: 8),
 | 
			
		||||
              Align(
 | 
			
		||||
                alignment: Alignment.bottomCenter,
 | 
			
		||||
                child: ListTile(
 | 
			
		||||
                  contentPadding: EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
                  leading: AccountImage(content: ua.user?.avatar),
 | 
			
		||||
                  title: Text(ua.user?.nick ?? 'unknown'.tr()).fontSize(15),
 | 
			
		||||
                  subtitle:
 | 
			
		||||
                      Text('@${ua.user?.name ?? 'unknown'.tr()}').fontSize(13),
 | 
			
		||||
                  leading: AccountImage(
 | 
			
		||||
                    content: ua.user?.avatar,
 | 
			
		||||
                    fallbackWidget:
 | 
			
		||||
                        ua.isAuthorized ? null : const Icon(Symbols.login),
 | 
			
		||||
                  ),
 | 
			
		||||
                  title: ua.isAuthorized
 | 
			
		||||
                      ? Text(ua.user?.nick ?? 'unknown'.tr()).fontSize(15)
 | 
			
		||||
                      : Text('screenAuthLogin').tr(),
 | 
			
		||||
                  subtitle: ua.isAuthorized
 | 
			
		||||
                      ? Text('@${ua.user?.name ?? 'unknown'.tr()}').fontSize(13)
 | 
			
		||||
                      : Text('navBottomUnauthorizedCaption').fontSize(13).tr(),
 | 
			
		||||
                  trailing: Row(
 | 
			
		||||
                    mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                    children: [
 | 
			
		||||
                      IconButton(
 | 
			
		||||
                        icon: const Icon(Symbols.notifications, fill: 1),
 | 
			
		||||
                        padding: EdgeInsets.zero,
 | 
			
		||||
                        visualDensity: VisualDensity.compact,
 | 
			
		||||
                        onPressed: () {
 | 
			
		||||
                          GoRouter.of(context).pushNamed('notification');
 | 
			
		||||
                          Scaffold.of(context).closeDrawer();
 | 
			
		||||
                        },
 | 
			
		||||
                      ),
 | 
			
		||||
                      if (ua.isAuthorized)
 | 
			
		||||
                        IconButton(
 | 
			
		||||
                          icon: const Icon(Symbols.notifications, fill: 1),
 | 
			
		||||
                          padding: EdgeInsets.zero,
 | 
			
		||||
                          visualDensity: VisualDensity.compact,
 | 
			
		||||
                          onPressed: () {
 | 
			
		||||
                            GoRouter.of(context).pushNamed('notification');
 | 
			
		||||
                            Scaffold.of(context).closeDrawer();
 | 
			
		||||
                          },
 | 
			
		||||
                        ),
 | 
			
		||||
                      IconButton(
 | 
			
		||||
                        icon: const Icon(Symbols.settings, fill: 1),
 | 
			
		||||
                        padding: EdgeInsets.zero,
 | 
			
		||||
@@ -138,7 +173,7 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              Gap(MediaQuery.of(context).padding.bottom),
 | 
			
		||||
              Gap(MediaQuery.of(context).padding.bottom + 8),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
@@ -155,7 +190,7 @@ class _DrawerContentList extends StatelessWidget {
 | 
			
		||||
    final ct = context.read<ChatChannelProvider>();
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
    final nav = context.watch<NavigationProvider>();
 | 
			
		||||
    final rel = context.read<SnRealmProvider>();
 | 
			
		||||
    final rel = context.watch<SnRealmProvider>();
 | 
			
		||||
 | 
			
		||||
    return PageTransitionSwitcher(
 | 
			
		||||
      duration: const Duration(milliseconds: 300),
 | 
			
		||||
@@ -185,16 +220,6 @@ class _DrawerContentList extends StatelessWidget {
 | 
			
		||||
                  horizontal: 32,
 | 
			
		||||
                  vertical: 12,
 | 
			
		||||
                ),
 | 
			
		||||
                ListTile(
 | 
			
		||||
                  minTileHeight: 48,
 | 
			
		||||
                  contentPadding: EdgeInsets.only(left: 28, right: 16),
 | 
			
		||||
                  leading: const Icon(Symbols.home),
 | 
			
		||||
                  title: Text('screenHome').tr(),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    GoRouter.of(context).goNamed('home');
 | 
			
		||||
                    Scaffold.of(context).closeDrawer();
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                ...rel.availableRealms.map((ele) {
 | 
			
		||||
                  return ListTile(
 | 
			
		||||
                    minTileHeight: 48,
 | 
			
		||||
@@ -209,6 +234,16 @@ class _DrawerContentList extends StatelessWidget {
 | 
			
		||||
                    },
 | 
			
		||||
                  );
 | 
			
		||||
                }),
 | 
			
		||||
                ListTile(
 | 
			
		||||
                  minTileHeight: 48,
 | 
			
		||||
                  contentPadding: EdgeInsets.only(left: 28, right: 16),
 | 
			
		||||
                  leading: const Icon(Symbols.globe).padding(right: 4),
 | 
			
		||||
                  title: Text('screenRealmDiscovery').tr(),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    GoRouter.of(context).pushNamed('realmDiscovery');
 | 
			
		||||
                    Scaffold.of(context).closeDrawer();
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            )
 | 
			
		||||
          : ListView(
 | 
			
		||||
@@ -247,7 +282,7 @@ class _DrawerContentList extends StatelessWidget {
 | 
			
		||||
                  ),
 | 
			
		||||
                  title: Text(nav.focusedRealm!.name),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    GoRouter.of(context).pushNamed(
 | 
			
		||||
                    GoRouter.of(context).goNamed(
 | 
			
		||||
                      'realmDetail',
 | 
			
		||||
                      pathParameters: {
 | 
			
		||||
                        'alias': nav.focusedRealm!.alias,
 | 
			
		||||
@@ -265,7 +300,7 @@ class _DrawerContentList extends StatelessWidget {
 | 
			
		||||
                  leading: const Icon(Symbols.globe),
 | 
			
		||||
                  title: Text('community').tr(),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    GoRouter.of(context).pushNamed(
 | 
			
		||||
                    GoRouter.of(context).goNamed(
 | 
			
		||||
                      'realmCommunity',
 | 
			
		||||
                      pathParameters: {
 | 
			
		||||
                        'alias': nav.focusedRealm!.alias,
 | 
			
		||||
@@ -290,7 +325,7 @@ class _DrawerContentList extends StatelessWidget {
 | 
			
		||||
                    leading: const Icon(Symbols.tag),
 | 
			
		||||
                    title: Text(ele.name),
 | 
			
		||||
                    onTap: () {
 | 
			
		||||
                      GoRouter.of(context).pushNamed(
 | 
			
		||||
                      GoRouter.of(context).goNamed(
 | 
			
		||||
                        'chatRoom',
 | 
			
		||||
                        pathParameters: {
 | 
			
		||||
                          'scope': ele.realm?.alias ?? 'global',
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,7 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
 | 
			
		||||
      final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
      await sn.client
 | 
			
		||||
          .put('/cgi/co/questions/${widget.parentPost.id}/answer', data: {
 | 
			
		||||
        'publisher': answer.publisherId,
 | 
			
		||||
        'publisher': widget.parentPost.publisherId,
 | 
			
		||||
        'answer_id': answer.id,
 | 
			
		||||
      });
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
 
 | 
			
		||||
@@ -279,6 +279,8 @@ class _PostItemState extends State<PostItem> {
 | 
			
		||||
    final ua = context.read<UserProvider>();
 | 
			
		||||
    final isAuthor =
 | 
			
		||||
        ua.isAuthorized && widget.data.publisher.accountId == ua.user?.id;
 | 
			
		||||
    final isParentAuthor = ua.isAuthorized &&
 | 
			
		||||
        widget.data.replyTo?.publisher.accountId == ua.user?.id;
 | 
			
		||||
 | 
			
		||||
    final displayableAttachments = widget.data.preload?.attachments
 | 
			
		||||
        ?.where((ele) =>
 | 
			
		||||
@@ -333,6 +335,7 @@ class _PostItemState extends State<PostItem> {
 | 
			
		||||
                    _PostActionPopup(
 | 
			
		||||
                      data: widget.data,
 | 
			
		||||
                      isAuthor: isAuthor,
 | 
			
		||||
                      isParentAuthor: isParentAuthor,
 | 
			
		||||
                      onShare: () => _doShare(context),
 | 
			
		||||
                      onShareImage: () => _doShareViaPicture(context),
 | 
			
		||||
                      onSelectAnswer: widget.onSelectAnswer,
 | 
			
		||||
@@ -577,6 +580,7 @@ class _PostItemState extends State<PostItem> {
 | 
			
		||||
                            _PostActionPopup(
 | 
			
		||||
                              data: widget.data,
 | 
			
		||||
                              isAuthor: isAuthor,
 | 
			
		||||
                              isParentAuthor: isParentAuthor,
 | 
			
		||||
                              onShare: () => _doShare(context),
 | 
			
		||||
                              onShareImage: () => _doShareViaPicture(context),
 | 
			
		||||
                              onSelectAnswer: widget.onSelectAnswer,
 | 
			
		||||
@@ -1317,6 +1321,7 @@ class _PostAvatar extends StatelessWidget {
 | 
			
		||||
class _PostActionPopup extends StatelessWidget {
 | 
			
		||||
  final SnPost data;
 | 
			
		||||
  final bool isAuthor;
 | 
			
		||||
  final bool isParentAuthor;
 | 
			
		||||
  final Function onDeleted;
 | 
			
		||||
  final Function() onShare, onShareImage;
 | 
			
		||||
  final Function()? onSelectAnswer;
 | 
			
		||||
@@ -1324,6 +1329,7 @@ class _PostActionPopup extends StatelessWidget {
 | 
			
		||||
  const _PostActionPopup({
 | 
			
		||||
    required this.data,
 | 
			
		||||
    required this.isAuthor,
 | 
			
		||||
    required this.isParentAuthor,
 | 
			
		||||
    required this.onDeleted,
 | 
			
		||||
    required this.onShare,
 | 
			
		||||
    required this.onShareImage,
 | 
			
		||||
@@ -1397,7 +1403,7 @@ class _PostActionPopup extends StatelessWidget {
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
          if (onTranslate != null) PopupMenuDivider(),
 | 
			
		||||
          if (isAuthor && onSelectAnswer != null)
 | 
			
		||||
          if (isParentAuthor && onSelectAnswer != null)
 | 
			
		||||
            PopupMenuItem(
 | 
			
		||||
              child: Row(
 | 
			
		||||
                children: [
 | 
			
		||||
@@ -1410,7 +1416,7 @@ class _PostActionPopup extends StatelessWidget {
 | 
			
		||||
                onSelectAnswer?.call();
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
          if (isAuthor && onSelectAnswer != null) PopupMenuDivider(),
 | 
			
		||||
          if (isParentAuthor && onSelectAnswer != null) PopupMenuDivider(),
 | 
			
		||||
          if (isAuthor)
 | 
			
		||||
            PopupMenuItem(
 | 
			
		||||
              child: Row(
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -2238,10 +2238,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: tray_manager
 | 
			
		||||
      sha256: "80be6c508159a6f3c57983de795209ac13453e9832fd574143b06dceee188ed2"
 | 
			
		||||
      sha256: c2da0f0f1ddb455e721cf68d05d1281fec75cf5df0a1d3cb67b6ca0bdfd5709d
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.3.2"
 | 
			
		||||
    version: "0.4.0"
 | 
			
		||||
  tuple:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -2525,11 +2525,10 @@ packages:
 | 
			
		||||
  workmanager:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      path: workmanager
 | 
			
		||||
      ref: main
 | 
			
		||||
      resolved-ref: "4ce065135dc1b91fee918f81596b42a56850391d"
 | 
			
		||||
      url: "https://github.com/fluttercommunity/flutter_workmanager.git"
 | 
			
		||||
    source: git
 | 
			
		||||
      name: workmanager
 | 
			
		||||
      sha256: ed13530cccd28c5c9959ad42d657cd0666274ca74c56dea0ca183ddd527d3a00
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.5.2"
 | 
			
		||||
  xdg_directories:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								pubspec.yaml
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								pubspec.yaml
									
									
									
									
									
								
							@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
 | 
			
		||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
			
		||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
			
		||||
# of the product and file versions while build-number is used as the build suffix.
 | 
			
		||||
version: 2.4.2+83
 | 
			
		||||
version: 2.4.2+84
 | 
			
		||||
 | 
			
		||||
environment:
 | 
			
		||||
  sdk: ^3.5.4
 | 
			
		||||
@@ -103,11 +103,7 @@ dependencies:
 | 
			
		||||
  flutter_svg: ^2.0.16
 | 
			
		||||
  home_widget: ^0.7.0
 | 
			
		||||
  receive_sharing_intent: ^1.8.1
 | 
			
		||||
  workmanager:
 | 
			
		||||
    git:
 | 
			
		||||
      url: https://github.com/fluttercommunity/flutter_workmanager.git
 | 
			
		||||
      path: workmanager
 | 
			
		||||
      ref: main
 | 
			
		||||
  workmanager: ^0.5.2
 | 
			
		||||
  flutter_app_update: ^3.2.2
 | 
			
		||||
  in_app_review: ^2.0.10
 | 
			
		||||
  version: ^3.0.2
 | 
			
		||||
@@ -120,7 +116,7 @@ dependencies:
 | 
			
		||||
  flutter_inappwebview: ^6.1.5
 | 
			
		||||
  html: ^0.15.5
 | 
			
		||||
  xml: ^6.5.0
 | 
			
		||||
  tray_manager: ^0.3.2
 | 
			
		||||
  tray_manager: ^0.4.0
 | 
			
		||||
  hotkey_manager: ^0.2.3
 | 
			
		||||
  image_picker_android: ^0.8.12+20
 | 
			
		||||
  cached_network_image_platform_interface: ^4.1.1
 | 
			
		||||
@@ -179,6 +175,7 @@ flutter:
 | 
			
		||||
    - assets/icon/icon-light-radius.png
 | 
			
		||||
    - assets/icon/tray-icon.ico
 | 
			
		||||
    - assets/icon/tray-icon.png
 | 
			
		||||
    - assets/icon/kanban-1st.jpg
 | 
			
		||||
    - assets/translations/
 | 
			
		||||
 | 
			
		||||
  # An image asset can refer to one or more resolution-specific "variants", see
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user