diff --git a/assets/icon/kanban-1st.jpg b/assets/icon/kanban-1st.jpg new file mode 100755 index 0000000..edc7cbb Binary files /dev/null and b/assets/icon/kanban-1st.jpg differ diff --git a/lib/main.dart b/lib/main.dart index 856bb66..072815c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -89,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", @@ -114,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; } @@ -132,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(), @@ -212,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!); }, ); } @@ -239,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()) { @@ -261,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(); - config.setUpdate( - remoteVersionString, - resp.data?['body'] ?? 'No changelog', - ); + config.setUpdate(remoteVersionString, resp.data?['body'] ?? 'No changelog'); logging.info("[Update] Update available: $remoteVersionString"); } } catch (e) { @@ -322,19 +294,21 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { _setPhaseText('websocket'); final ws = context.read(); await ws.tryConnect(); - if (!mounted) return; - _setPhaseText('notification'); - final notify = context.read(); - notify.listen(); - await notify.registerPushNotifications(); - if (!mounted) return; - _setPhaseText('keyPair'); - final kp = context.read(); try { + if (!mounted) return; + _setPhaseText('keyPair'); + final kp = context.read(); await kp.reloadActive(); kp.listen(); } catch (_) {} if (ua.isAuthorized) { + if (!mounted) return; + _setPhaseText('notification'); + final notify = context.read(); + notify.listen(); + try { + await notify.registerPushNotifications(); + } catch (_) {} if (!mounted) return; _setPhaseText('stickers'); final sticker = context.read(); @@ -370,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 _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); @@ -416,10 +374,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { Future _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; @@ -430,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(); @@ -532,43 +485,49 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { } }); return SizeChangedLayoutNotifier( - child: _isBusy - ? Material( - key: Key('app-splash-screen-$_isBusy'), - child: Stack( - children: [ - 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, ); }, ), diff --git a/lib/providers/notification.dart b/lib/providers/notification.dart index 7c6e710..9c7b3b5 100644 --- a/lib/providers/notification.dart +++ b/lib/providers/notification.dart @@ -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; diff --git a/lib/screens/account.dart b/lib/screens/account.dart index db7013e..2371257 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -306,9 +306,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget { GoRouter.of(context).pushNamed('authLogin').then((value) { if (value == true && context.mounted) { final ua = context.read(); - context.showSnackbar('loginSuccess'.tr(args: [ - '@${ua.user?.name} (${ua.user?.nick})', - ])); + ua.refreshUser(); } }); }, diff --git a/lib/screens/auth/register.dart b/lib/screens/auth/register.dart index 4c4dbe3..aebabd8 100644 --- a/lib/screens/auth/register.dart +++ b/lib/screens/auth/register.dart @@ -43,7 +43,7 @@ class _RegisterScreenState extends State { final captchaTk = await Navigator.of(context, rootNavigator: true).push( MaterialPageRoute( - builder: (context) => TurnstileScreen(), + builder: (context) => CaptchaScreen(), ), ); if (captchaTk == null) return; diff --git a/lib/screens/captcha.dart b/lib/screens/captcha.dart index 266df22..c1d70f9 100644 --- a/lib/screens/captcha.dart +++ b/lib/screens/captcha.dart @@ -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 createState() => _TurnstileScreenState(); + State createState() => _CaptchaScreenState(); } -class _TurnstileScreenState extends State { +class _CaptchaScreenState extends State { + @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().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(); + + 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( diff --git a/lib/screens/home.dart b/lib/screens/home.dart index c13da17..77865a7 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -511,7 +511,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> { Future _doCheckIn() async { final captchaTk = await Navigator.of(context, rootNavigator: true).push( MaterialPageRoute( - builder: (context) => TurnstileScreen(), + builder: (context) => CaptchaScreen(), ), ); if (captchaTk == null) return; diff --git a/lib/widgets/connection_indicator.dart b/lib/widgets/connection_indicator.dart index 8ce20d6..d411636 100644 --- a/lib/widgets/connection_indicator.dart +++ b/lib/widgets/connection_indicator.dart @@ -16,7 +16,12 @@ class ConnectionIndicator extends StatelessWidget { final ws = context.watch(); final cfg = context.watch(); - 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(); diff --git a/lib/widgets/navigation/app_drawer_navigation.dart b/lib/widgets/navigation/app_drawer_navigation.dart index c7f61f1..1b0df52 100644 --- a/lib/widgets/navigation/app_drawer_navigation.dart +++ b/lib/widgets/navigation/app_drawer_navigation.dart @@ -67,6 +67,7 @@ class _AppNavigationDrawerState extends State { return Drawer( elevation: widget.elevation, backgroundColor: backgroundColor, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(0))), child: Column( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, diff --git a/pubspec.yaml b/pubspec.yaml index 8e18095..71a9429 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -179,6 +179,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