🐛 Fix some issues on Android and Web

This commit is contained in:
LittleSheep 2025-03-23 16:24:53 +08:00
parent 21a1d4a2ad
commit 8dd6435a30
10 changed files with 174 additions and 167 deletions

BIN
assets/icon/kanban-1st.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

View File

@ -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<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) {
@ -322,19 +294,21 @@ 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>();
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>();
@ -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<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);
@ -416,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;
@ -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,
);
},
),

View File

@ -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;

View File

@ -306,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();
}
});
},

View File

@ -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;

View File

@ -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(

View File

@ -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;

View File

@ -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();

View File

@ -67,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,

View File

@ -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