Compare commits

..

6 Commits

Author SHA1 Message Date
9311bfc3b5 ⬆️ Upgrade deps & replace to own translation api 2025-03-23 16:26:41 +08:00
8dd6435a30 🐛 Fix some issues on Android and Web 2025-03-23 16:24:53 +08:00
21a1d4a2ad 🐛 Fix unable select answer 2025-03-23 00:01:48 +08:00
603875b1af 🐛 Fix styling issue 2025-03-22 23:07:13 +08:00
4209a13c84 🐛 Fix no nav to use 2025-03-22 22:51:50 +08:00
55b79bfd8f 🐛 Finish bug fixes 2025-03-22 21:50:01 +08:00
28 changed files with 421 additions and 355 deletions

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

View File

@ -890,5 +890,12 @@
}, },
"settingsHideBottomNav": "Hide Bottom Navigation", "settingsHideBottomNav": "Hide Bottom Navigation",
"settingsHideBottomNavDescription": "Hide the bottom navigation bar, and show the navigation buttons in the drawer.", "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"
} }

View File

@ -888,5 +888,12 @@
}, },
"settingsHideBottomNav": "隐藏底部导航栏", "settingsHideBottomNav": "隐藏底部导航栏",
"settingsHideBottomNavDescription": "隐藏底部导航栏,在侧边栏抽屉显示导航按钮。", "settingsHideBottomNavDescription": "隐藏底部导航栏,在侧边栏抽屉显示导航按钮。",
"reCaptcha": "人机验证" "reCaptcha": "人机验证",
"friends": "好友",
"friendsDescription": "管理好友关系。",
"album": "相册",
"albumDescription": "查看相册与管理上传附件。",
"stickers": "贴图",
"stickersDescription": "查看贴图包与管理贴图。",
"navBottomUnauthorizedCaption": "或者注册一个账号"
} }

View File

@ -888,5 +888,12 @@
}, },
"settingsHideBottomNav": "隱藏底部導航欄", "settingsHideBottomNav": "隱藏底部導航欄",
"settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。", "settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。",
"reCaptcha": "人機驗證" "reCaptcha": "人機驗證",
"friends": "好友",
"friendsDescription": "管理好友關係。",
"album": "相冊",
"albumDescription": "查看相冊與管理上傳附件。",
"stickers": "貼圖",
"stickersDescription": "查看貼圖包與管理貼圖。",
"navBottomUnauthorizedCaption": "或者註冊一個賬號"
} }

View File

@ -888,5 +888,12 @@
}, },
"settingsHideBottomNav": "隱藏底部導航欄", "settingsHideBottomNav": "隱藏底部導航欄",
"settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。", "settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。",
"reCaptcha": "人機驗證" "reCaptcha": "人機驗證",
"friends": "好友",
"friendsDescription": "管理好友關係。",
"album": "相冊",
"albumDescription": "查看相冊與管理上傳附件。",
"stickers": "貼圖",
"stickersDescription": "查看貼圖包與管理貼圖。",
"navBottomUnauthorizedCaption": "或者註冊一個賬號"
} }

View File

@ -1,7 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'dart:math' hide log;
import 'dart:ui'; import 'dart:ui';
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart';
@ -90,19 +89,14 @@ void main() async {
await EasyLocalization.ensureInitialized(); await EasyLocalization.ensureInitialized();
if (!kIsWeb && !Platform.isLinux) { if (!kIsWeb && !Platform.isLinux) {
await Firebase.initializeApp( await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
options: DefaultFirebaseOptions.currentPlatform,
);
} }
GoRouter.optionURLReflectsImperativeAPIs = true; GoRouter.optionURLReflectsImperativeAPIs = true;
usePathUrlStrategy(); usePathUrlStrategy();
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
Workmanager().initialize( Workmanager().initialize(appBackgroundDispatcher, isInDebugMode: kDebugMode);
appBackgroundDispatcher,
isInDebugMode: kDebugMode,
);
if (Platform.isAndroid) { if (Platform.isAndroid) {
Workmanager().registerPeriodicTask( Workmanager().registerPeriodicTask(
"widget-update-random-post", "widget-update-random-post",
@ -115,8 +109,7 @@ void main() async {
} }
if (!kIsWeb && Platform.isAndroid) { if (!kIsWeb && Platform.isAndroid) {
final ImagePickerPlatform imagePickerImplementation = final ImagePickerPlatform imagePickerImplementation = ImagePickerPlatform.instance;
ImagePickerPlatform.instance;
if (imagePickerImplementation is ImagePickerAndroid) { if (imagePickerImplementation is ImagePickerAndroid) {
imagePickerImplementation.useAndroidPhotoPicker = true; imagePickerImplementation.useAndroidPhotoPicker = true;
} }
@ -133,12 +126,7 @@ class SolianApp extends StatelessWidget {
return ResponsiveBreakpoints.builder( return ResponsiveBreakpoints.builder(
child: EasyLocalization( child: EasyLocalization(
path: 'assets/translations', path: 'assets/translations',
supportedLocales: [ supportedLocales: [Locale('en', 'US'), Locale('zh', 'CN'), Locale('zh', 'TW'), Locale('zh', 'HK')],
Locale('en', 'US'),
Locale('zh', 'CN'),
Locale('zh', 'TW'),
Locale('zh', 'HK'),
],
fallbackLocale: Locale('en', 'US'), fallbackLocale: Locale('en', 'US'),
useFallbackTranslations: true, useFallbackTranslations: true,
assetLoader: JsonAssetLoader(), assetLoader: JsonAssetLoader(),
@ -161,7 +149,7 @@ class SolianApp extends StatelessWidget {
Provider(create: (ctx) => SnNetworkProvider(ctx)), Provider(create: (ctx) => SnNetworkProvider(ctx)),
Provider(create: (ctx) => UserDirectoryProvider(ctx)), Provider(create: (ctx) => UserDirectoryProvider(ctx)),
Provider(create: (ctx) => SnAttachmentProvider(ctx)), Provider(create: (ctx) => SnAttachmentProvider(ctx)),
Provider(create: (ctx) => SnRealmProvider(ctx)), ChangeNotifierProvider(create: (ctx) => SnRealmProvider(ctx)),
Provider(create: (ctx) => SnPostContentProvider(ctx)), Provider(create: (ctx) => SnPostContentProvider(ctx)),
Provider(create: (ctx) => SnRelationshipProvider(ctx)), Provider(create: (ctx) => SnRelationshipProvider(ctx)),
Provider(create: (ctx) => SnLinkPreviewProvider(ctx)), Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
@ -213,10 +201,7 @@ class _AppDelegate extends StatelessWidget {
], ],
routerConfig: appRouter, routerConfig: appRouter,
builder: (context, child) { builder: (context, child) {
return _AppSplashScreen( return _AppSplashScreen(key: const Key('global-splash-screen'), child: child!);
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')) { if (prefs.containsKey('first_boot_time')) {
final rawTime = prefs.getString('first_boot_time'); final rawTime = prefs.getString('first_boot_time');
final time = DateTime.tryParse(rawTime ?? ''); final time = DateTime.tryParse(rawTime ?? '');
if (time != null && if (time != null && time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) {
time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) {
final inAppReview = InAppReview.instance; final inAppReview = InAppReview.instance;
if (prefs.getBool('rating_requested') == true) return; if (prefs.getBool('rating_requested') == true) return;
if (await inAppReview.isAvailable()) { if (await inAppReview.isAvailable()) {
@ -262,30 +246,17 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
final info = await PackageInfo.fromPlatform(); final info = await PackageInfo.fromPlatform();
final localVersionString = '${info.version}+${info.buildNumber}'; final localVersionString = '${info.version}+${info.buildNumber}';
final resp = await Dio( final resp = await Dio(
BaseOptions( BaseOptions(sendTimeout: const Duration(seconds: 60), receiveTimeout: const Duration(seconds: 60)),
sendTimeout: const Duration(seconds: 60), ).get('https://api.github.com/repos/Solsynth/HyperNet.Surface/releases/latest');
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 remoteVersionString = resp.data?['tag_name'] ?? '0.0.0+0';
final remoteVersion = Version.parse(remoteVersionString.split('+').first); final remoteVersion = Version.parse(remoteVersionString.split('+').first);
final localVersion = Version.parse(localVersionString.split('+').first); final localVersion = Version.parse(localVersionString.split('+').first);
final remoteBuildNumber = final remoteBuildNumber = int.tryParse(remoteVersionString.split('+').last) ?? 0;
int.tryParse(remoteVersionString.split('+').last) ?? 0; final localBuildNumber = int.tryParse(localVersionString.split('+').last) ?? 0;
final localBuildNumber = logging.info("[Update] Local: $localVersionString, Remote: $remoteVersionString");
int.tryParse(localVersionString.split('+').last) ?? 0; if ((remoteVersion > localVersion || remoteBuildNumber > localBuildNumber) && mounted) {
logging.info(
"[Update] Local: $localVersionString, Remote: $remoteVersionString");
if ((remoteVersion > localVersion ||
remoteBuildNumber > localBuildNumber) &&
mounted) {
final config = context.read<ConfigProvider>(); final config = context.read<ConfigProvider>();
config.setUpdate( config.setUpdate(remoteVersionString, resp.data?['body'] ?? 'No changelog');
remoteVersionString,
resp.data?['body'] ?? 'No changelog',
);
logging.info("[Update] Update available: $remoteVersionString"); logging.info("[Update] Update available: $remoteVersionString");
} }
} catch (e) { } catch (e) {
@ -323,33 +294,39 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
_setPhaseText('websocket'); _setPhaseText('websocket');
final ws = context.read<WebSocketProvider>(); final ws = context.read<WebSocketProvider>();
await ws.tryConnect(); await ws.tryConnect();
if (!mounted) return; try {
_setPhaseText('notification'); if (!mounted) return;
final notify = context.read<NotificationProvider>(); _setPhaseText('keyPair');
notify.listen(); final kp = context.read<KeyPairProvider>();
await notify.registerPushNotifications(); await kp.reloadActive();
if (!mounted) return; kp.listen();
_setPhaseText('keyPair'); } catch (_) {}
final kp = context.read<KeyPairProvider>(); if (ua.isAuthorized) {
await kp.reloadActive(); if (!mounted) return;
kp.listen(); _setPhaseText('notification');
if (!mounted) return; final notify = context.read<NotificationProvider>();
_setPhaseText('stickers'); notify.listen();
final sticker = context.read<SnStickerProvider>(); try {
await sticker.listSticker(); await notify.registerPushNotifications();
if (!mounted) return; } catch (_) {}
_setPhaseText('userDirectory'); if (!mounted) return;
final ud = context.read<UserDirectoryProvider>(); _setPhaseText('stickers');
await ud.loadAccountCache(); final sticker = context.read<SnStickerProvider>();
if (!mounted) return; await sticker.listSticker();
_setPhaseText('realm'); if (!mounted) return;
final rm = context.read<SnRealmProvider>(); _setPhaseText('userDirectory');
await rm.refreshAvailableRealms(); final ud = context.read<UserDirectoryProvider>();
if (!mounted) return; await ud.loadAccountCache();
_setPhaseText('chat'); if (!mounted) return;
final ct = context.read<ChatChannelProvider>(); _setPhaseText('realm');
await ct.refreshAvailableChannels(); final rm = context.read<SnRealmProvider>();
_setPhaseText('done'); await rm.refreshAvailableRealms();
if (!mounted) return;
_setPhaseText('chat');
final ct = context.read<ChatChannelProvider>();
await ct.refreshAvailableChannels();
_setPhaseText('done');
}
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
await context.showErrorDialog(err); await context.showErrorDialog(err);
@ -367,35 +344,19 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
final Menu _appTrayMenu = Menu( final Menu _appTrayMenu = Menu(
items: [ items: [
MenuItem( MenuItem(key: 'version_label', label: 'Solian', disabled: true),
key: 'version_label',
label: 'Solian',
disabled: true,
),
MenuItem.separator(), MenuItem.separator(),
MenuItem.checkbox( MenuItem.checkbox(checked: false, key: 'mute_notification', label: 'trayMenuMuteNotification'.tr()),
checked: false,
key: 'mute_notification',
label: 'trayMenuMuteNotification'.tr(),
),
MenuItem.separator(), MenuItem.separator(),
MenuItem( MenuItem(key: 'window_show', label: 'trayMenuShow'.tr()),
key: 'window_show', MenuItem(key: 'exit', label: 'trayMenuExit'.tr()),
label: 'trayMenuShow'.tr(),
),
MenuItem(
key: 'exit',
label: 'trayMenuExit'.tr(),
),
], ],
); );
Future<void> _trayInitialization() async { Future<void> _trayInitialization() async {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return; if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
final icon = Platform.isWindows final icon = Platform.isWindows ? 'assets/icon/tray-icon.ico' : 'assets/icon/tray-icon.png';
? 'assets/icon/tray-icon.ico'
: 'assets/icon/tray-icon.png';
final appVersion = await PackageInfo.fromPlatform(); final appVersion = await PackageInfo.fromPlatform();
trayManager.addListener(this); trayManager.addListener(this);
@ -413,10 +374,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
Future<void> _notifyInitialization() async { Future<void> _notifyInitialization() async {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return; if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
await localNotifier.setup( await localNotifier.setup(appName: 'Solian', shortcutPolicy: ShortcutPolicy.requireCreate);
appName: 'Solian',
shortcutPolicy: ShortcutPolicy.requireCreate,
);
} }
AppLifecycleListener? _appLifecycleListener; AppLifecycleListener? _appLifecycleListener;
@ -427,9 +385,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
_isBusy = true; _isBusy = true;
if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) { if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) {
_appLifecycleListener = AppLifecycleListener( _appLifecycleListener = AppLifecycleListener(onExitRequested: _onExitRequested);
onExitRequested: _onExitRequested,
);
} }
_trayInitialization(); _trayInitialization();
@ -529,44 +485,49 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
} }
}); });
return SizeChangedLayoutNotifier( return SizeChangedLayoutNotifier(
child: _isBusy child:
? Material( _isBusy
key: Key('app-splash-screen-$_isBusy'), ? Material(
child: Stack( key: Key('app-splash-screen-$_isBusy'),
children: [ child: Stack(
CustomPaint(painter: GraphPainter()), children: [
Center( Container(
child: Container( decoration: BoxDecoration(
constraints: const BoxConstraints( image: DecorationImage(
maxWidth: 240, image: AssetImage('assets/icon/kanban-1st.jpg'),
), fit: BoxFit.cover,
child: Column( opacity: 0.1,
mainAxisSize: MainAxisSize.min, ),
children: [ color: Theme.of(context).colorScheme.surface,
Image.asset( backgroundBlendMode: BlendMode.darken,
'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(),
],
), ),
), ),
), Center(
], child: Container(
), constraints: const BoxConstraints(maxWidth: 240),
) child: Column(
: widget.child, 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;
}

View File

@ -41,6 +41,11 @@ class ChatChannelProvider extends ChangeNotifier {
}); });
} }
void addAvailableChannel(SnChannel channel) {
_availableChannels.add(channel);
notifyListeners();
}
Future<void> _saveChannelToLocal(Iterable<SnChannel> channels) async { Future<void> _saveChannelToLocal(Iterable<SnChannel> channels) async {
await Future.wait( await Future.wait(
channels.map( channels.map(

View File

@ -61,26 +61,6 @@ class NavigationProvider extends ChangeNotifier {
screen: 'news', screen: 'news',
label: 'screenNews', 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 = [ static const List<String> kDefaultPinnedDestination = [
'home', 'home',

View File

@ -48,13 +48,11 @@ class NotificationProvider extends ChangeNotifier {
var deviceUuid = await FlutterUdid.consistentUdid; var deviceUuid = await FlutterUdid.consistentUdid;
if (deviceUuid.isEmpty) { if (deviceUuid.isEmpty) {
logging.warning( logging.warning('[Push Notification] Unable to active push notifications, couldn\'t get device uuid');
'[Push Notification] Unable to active push notifications, couldn\'t get device uuid');
return; return;
} else { } else {
logging.info('[Push Notification] Device UUID is $deviceUuid'); logging.info('[Push Notification] Device UUID is $deviceUuid');
logging logging.info('[Push Notification] Registering device push notifications...');
.info('[Push Notification] Registering device push notifications...');
} }
if (Platform.isIOS || Platform.isMacOS) { if (Platform.isIOS || Platform.isMacOS) {
@ -66,14 +64,14 @@ class NotificationProvider extends ChangeNotifier {
} }
logging.info('[Push Notification] Device Push Token is $token'); logging.info('[Push Notification] Device Push Token is $token');
await _sn.client.post( try {
'/cgi/id/notifications/subscription', await _sn.client.post(
data: { '/cgi/id/notifications/subscription',
'provider': provider, data: {'provider': provider, 'device_token': token, 'device_id': deviceUuid},
'device_token': token, );
'device_id': deviceUuid, } catch (err) {
}, logging.error('[Push Notification] Unable to register push notifications: $err');
); }
} }
int showingCount = 0; int showingCount = 0;
@ -91,8 +89,7 @@ class NotificationProvider extends ChangeNotifier {
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true; final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
if (doHaptic) HapticFeedback.mediumImpact(); if (doHaptic) HapticFeedback.mediumImpact();
if (notification.topic == 'messaging.message' && if (notification.topic == 'messaging.message' && skippableNotifyChannel != null) {
skippableNotifyChannel != null) {
if (notification.metadata['channel_id'] != null && if (notification.metadata['channel_id'] != null &&
notification.metadata['channel_id'] == skippableNotifyChannel) { notification.metadata['channel_id'] == skippableNotifyChannel) {
return; return;

View File

@ -8,7 +8,7 @@ import 'package:surface/providers/database.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/realm.dart'; import 'package:surface/types/realm.dart';
class SnRealmProvider { class SnRealmProvider extends ChangeNotifier {
late final SnNetworkProvider _sn; late final SnNetworkProvider _sn;
late final DatabaseProvider _dt; late final DatabaseProvider _dt;
@ -39,6 +39,11 @@ class SnRealmProvider {
return out; return out;
} }
void addAvailableRealm(SnRealm realm) {
_availableRealms.add(realm);
notifyListeners();
}
Future<SnRealm> getRealm(dynamic aliasOrId) async { Future<SnRealm> getRealm(dynamic aliasOrId) async {
if (_cache.containsKey(aliasOrId.toString())) { if (_cache.containsKey(aliasOrId.toString())) {
return _cache[aliasOrId.toString()]!; return _cache[aliasOrId.toString()]!;

View File

@ -4,8 +4,7 @@ import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:surface/logger.dart'; import 'package:surface/logger.dart';
// TODO self host translate api const kTranslateApiBaseUrl = 'https://translate.solsynth.dev';
const kTranslateApiBaseUrl = 'https://translate.disroot.org';
class SnTranslator { class SnTranslator {
final Dio client = Dio( final Dio client = Dio(

View File

@ -64,6 +64,7 @@ class UserProvider extends ChangeNotifier {
} }
Future<SnAccount?> refreshUser() async { Future<SnAccount?> refreshUser() async {
if (!isAuthorized) return null;
final resp = await _sn.client.get('/cgi/id/users/me'); final resp = await _sn.client.get('/cgi/id/users/me');
final out = SnAccount.fromJson(resp.data); final out = SnAccount.fromJson(resp.data);

View File

@ -30,19 +30,7 @@ class AccountScreen extends StatelessWidget {
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text( title: Text("screenAccount").tr(),
"screenAccount",
style: TextStyle(
color: Colors.white,
shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 5.0,
color: Color.fromARGB(255, 0, 0, 0),
),
],
),
).tr(),
flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
? Stack( ? Stack(
fit: StackFit.expand, fit: StackFit.expand,
@ -158,23 +146,33 @@ class _AuthorizedAccountScreen extends StatelessWidget {
}, },
), ),
ListTile( ListTile(
title: Text('abuseReport').tr(), title: Text('friends').tr(),
subtitle: Text('abuseReportActionDescription').tr(), subtitle: Text('friendsDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.flag), leading: const Icon(Symbols.person),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed('abuseReport'); GoRouter.of(context).pushNamed('friend');
}, },
), ),
ListTile( ListTile(
title: Text('factorSettings').tr(), title: Text('album').tr(),
subtitle: Text('factorSettingsSubtitle').tr(), subtitle: Text('albumDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.lock), leading: const Icon(Symbols.photo_library),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
onTap: () { 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( ListTile(
@ -237,6 +235,16 @@ class _AuthorizedAccountScreen extends StatelessWidget {
GoRouter.of(context).pushNamed('accountSettings'); 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( ListTile(
title: Text('accountLogout').tr(), title: Text('accountLogout').tr(),
subtitle: Text('accountLogoutSubtitle').tr(), subtitle: Text('accountLogoutSubtitle').tr(),
@ -298,9 +306,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
GoRouter.of(context).pushNamed('authLogin').then((value) { GoRouter.of(context).pushNamed('authLogin').then((value) {
if (value == true && context.mounted) { if (value == true && context.mounted) {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
context.showSnackbar('loginSuccess'.tr(args: [ ua.refreshUser();
'@${ua.user?.name} (${ua.user?.nick})',
]));
} }
}); });
}, },

View File

@ -117,6 +117,16 @@ class AccountSettingsScreen extends StatelessWidget {
GoRouter.of(context).pushNamed('accountSettingsSecurity'); 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( ListTile(
title: Text('accountProfileEdit').tr(), title: Text('accountProfileEdit').tr(),
subtitle: Text('accountProfileEditSubtitle').tr(), subtitle: Text('accountProfileEditSubtitle').tr(),

View File

@ -10,7 +10,6 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
import 'package:surface/types/attachment.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_zoom.dart';
import 'package:surface/widgets/attachment/attachment_item.dart'; import 'package:surface/widgets/attachment/attachment_item.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
@ -106,7 +105,7 @@ class _AlbumScreenState extends State<AlbumScreen> {
controller: _scrollController, controller: _scrollController,
slivers: [ slivers: [
SliverAppBar( SliverAppBar(
leading: AutoAppBarLeading(), leading: PageBackButton(),
title: Text('screenAlbum').tr(), title: Text('screenAlbum').tr(),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
@ -119,7 +118,8 @@ class _AlbumScreenState extends State<AlbumScreen> {
child: CircularProgressIndicator( child: CircularProgressIndicator(
value: _billing?.includedRatio ?? 0, value: _billing?.includedRatio ?? 0,
strokeWidth: 8, strokeWidth: 8,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh, backgroundColor:
Theme.of(context).colorScheme.surfaceContainerHigh,
), ),
).padding(all: 12), ).padding(all: 12),
const Gap(24), const Gap(24),
@ -129,7 +129,8 @@ class _AlbumScreenState extends State<AlbumScreen> {
children: [ children: [
Text('attachmentBillingUploaded').tr().bold(), Text('attachmentBillingUploaded').tr().bold(),
Text( Text(
(_billing?.currentBytes ?? 0).formatBytes(decimals: 4), (_billing?.currentBytes ?? 0)
.formatBytes(decimals: 4),
style: GoogleFonts.robotoMono(), style: GoogleFonts.robotoMono(),
), ),
Text('attachmentBillingDiscount').tr().bold(), Text('attachmentBillingDiscount').tr().bold(),

View File

@ -43,7 +43,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
final captchaTk = await Navigator.of(context, rootNavigator: true).push( final captchaTk = await Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => TurnstileScreen(), builder: (context) => CaptchaScreen(),
), ),
); );
if (captchaTk == null) return; 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:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
class TurnstileScreen extends StatefulWidget { class CaptchaScreen extends StatefulWidget {
const TurnstileScreen({ const CaptchaScreen({
super.key, super.key,
}); });
@override @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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cfg = context.read<ConfigProvider>(); final cfg = context.read<ConfigProvider>();
if (kIsWeb) {
return AppScaffold(
appBar: AppBar(title: Text("reCaptcha").tr()),
body: HtmlElementView(viewType: 'captcha-iframe'),
);
}
return AppScaffold( return AppScaffold(
appBar: AppBar(title: Text("reCaptcha").tr()), appBar: AppBar(title: Text("reCaptcha").tr()),
body: InAppWebView( body: InAppWebView(

View File

@ -201,7 +201,7 @@ class _FriendScreenState extends State<FriendScreen> {
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: PageBackButton(),
title: Text('screenFriend').tr(), title: Text('screenFriend').tr(),
), ),
body: Center( body: Center(
@ -254,7 +254,8 @@ class _FriendScreenState extends State<FriendScreen> {
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
onTap: _showBlocks, onTap: _showBlocks,
), ),
if (_requests.isNotEmpty || _blocks.isNotEmpty) const Divider(height: 1), if (_requests.isNotEmpty || _blocks.isNotEmpty)
const Divider(height: 1),
Expanded( Expanded(
child: MediaQuery.removePadding( child: MediaQuery.removePadding(
context: context, context: context,
@ -270,7 +271,8 @@ class _FriendScreenState extends State<FriendScreen> {
final relation = _relations[index]; final relation = _relations[index];
final other = relation.related; final other = relation.related;
return ListTile( return ListTile(
contentPadding: const EdgeInsets.only(right: 24, left: 16), contentPadding:
const EdgeInsets.only(right: 24, left: 16),
leading: AccountImage(content: other?.avatar), leading: AccountImage(content: other?.avatar),
title: Text(other?.nick ?? 'unknown'), title: Text(other?.nick ?? 'unknown'),
subtitle: Text(other?.nick ?? 'unknown'), subtitle: Text(other?.nick ?? 'unknown'),
@ -286,12 +288,16 @@ class _FriendScreenState extends State<FriendScreen> {
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
InkWell( InkWell(
onTap: _isUpdating ? null : () => _changeRelation(relation, 2), onTap: _isUpdating
? null
: () => _changeRelation(relation, 2),
child: Text('friendBlock').tr(), child: Text('friendBlock').tr(),
), ),
const Gap(8), const Gap(8),
InkWell( InkWell(
onTap: _isUpdating ? null : () => _deleteRelation(relation), onTap: _isUpdating
? null
: () => _deleteRelation(relation),
child: Text('friendDeleteAction').tr(), child: Text('friendDeleteAction').tr(),
), ),
], ],
@ -420,7 +426,9 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Text(kFriendStatus[relation.status] ?? 'unknown').tr().opacity(0.75), Text(kFriendStatus[relation.status] ?? 'unknown')
.tr()
.opacity(0.75),
if (relation.status == 0) if (relation.status == 0)
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
@ -441,7 +449,8 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
InkWell( InkWell(
onTap: _isBusy ? null : () => _changeRelation(relation, 1), onTap:
_isBusy ? null : () => _changeRelation(relation, 1),
child: Text('friendUnblock').tr(), child: Text('friendUnblock').tr(),
), ),
const Gap(8), const Gap(8),

View File

@ -511,7 +511,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
Future<void> _doCheckIn() async { Future<void> _doCheckIn() async {
final captchaTk = await Navigator.of(context, rootNavigator: true).push( final captchaTk = await Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => TurnstileScreen(), builder: (context) => CaptchaScreen(),
), ),
); );
if (captchaTk == null) return; if (captchaTk == null) return;
@ -806,7 +806,7 @@ class _HomeDashNotificationWidgetState
child: IconButton( child: IconButton(
icon: const Icon(Symbols.arrow_right_alt), icon: const Icon(Symbols.arrow_right_alt),
onPressed: () { onPressed: () {
GoRouter.of(context).goNamed('notification'); GoRouter.of(context).pushNamed('notification');
}, },
), ),
), ),

View File

@ -149,8 +149,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: PageBackButton(),
title: Text('screenNotification').tr()), title: Text('screenNotification').tr(),
),
body: Center(child: UnauthorizedHint()), body: Center(child: UnauthorizedHint()),
); );
} }

View File

@ -4,8 +4,10 @@ import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/channel.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_realm.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/chat.dart'; import 'package:surface/types/chat.dart';
import 'package:surface/types/realm.dart'; import 'package:surface/types/realm.dart';
@ -57,7 +59,9 @@ class _RealmDiscoveryScreenState extends State<RealmDiscoveryScreen> {
title: Text('screenRealmDiscovery').tr(), title: Text('screenRealmDiscovery').tr(),
actions: [ actions: [
IconButton( 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: () { onPressed: () {
setState(() => _isCompactView = !_isCompactView); setState(() => _isCompactView = !_isCompactView);
context.read<ConfigProvider>().realmCompactView = _isCompactView; context.read<ConfigProvider>().realmCompactView = _isCompactView;
@ -117,7 +121,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
try { try {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>(); 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( final out = List<SnChannel>.from(
resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(), resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
); );
@ -135,10 +140,13 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
setState(() => _isJoining = true); setState(() => _isJoining = true);
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final ua = context.read<UserProvider>(); 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, 'related': ua.user?.name,
}); });
await _joinSelectedChannels(); await _joinSelectedChannels();
rel.addAvailableRealm(widget.realm);
if (!mounted) return; if (!mounted) return;
context.showSnackbar('realmJoined'.tr(args: [widget.realm.name])); context.showSnackbar('realmJoined'.tr(args: [widget.realm.name]));
Navigator.pop(context); Navigator.pop(context);
@ -156,13 +164,20 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
await sn.client.post('/cgi/im/channels/${widget.realm.alias}/$channel/members', data: { await sn.client.post(
'related': ua.user?.name, '/cgi/im/channels/${widget.realm.alias}/$channel/members',
}); data: {
'related': ua.user?.name,
});
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); 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: [ children: [
const Icon(Symbols.group_add, size: 24), const Icon(Symbols.group_add, size: 24),
const Gap(16), 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), ).padding(horizontal: 20, top: 16, bottom: 12),
Row( Row(
@ -216,7 +232,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
Container( Container(
width: double.infinity, width: double.infinity,
color: Theme.of(context).colorScheme.surfaceContainerHigh, 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), .padding(horizontal: 24, vertical: 8),
), ),
Expanded( Expanded(

View File

@ -9,7 +9,6 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_sticker.dart'; import 'package:surface/providers/sn_sticker.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/attachment.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/attachment/attachment_item.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
@ -134,7 +133,7 @@ class _StickerScreenState extends State<StickerScreen>
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: PageBackButton(),
title: Text('screenStickers').tr(), title: Text('screenStickers').tr(),
actions: [ actions: [
IconButton( IconButton(

View File

@ -16,7 +16,12 @@ class ConnectionIndicator extends StatelessWidget {
final ws = context.watch<WebSocketProvider>(); final ws = context.watch<WebSocketProvider>();
final cfg = context.watch<ConfigProvider>(); 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( return ListenableBuilder(
listenable: ws, listenable: ws,
@ -32,37 +37,39 @@ class ConnectionIndicator extends StatelessWidget {
elevation: 2, elevation: 2,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
color: Theme.of(context).colorScheme.secondaryContainer, color: Theme.of(context).colorScheme.secondaryContainer,
child: ua.isAuthorized child:
? Row( ua.isAuthorized
mainAxisSize: MainAxisSize.min, ? Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ crossAxisAlignment: CrossAxisAlignment.center,
if (ws.isBusy) children: [
Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer) if (ws.isBusy)
else if (!ws.isConnected) Text(
Text('serverDisconnected') 'serverConnecting',
.tr() ).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
.textColor(Theme.of(context).colorScheme.onSecondaryContainer) else if (!ws.isConnected)
else Text(
Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer), 'serverDisconnected',
const Gap(8), ).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
if (ws.isBusy) else
const CircularProgressIndicator(strokeWidth: 2.5) Text(
.width(12) 'serverConnected',
.height(12) ).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
.padding(horizontal: 4, right: 4) const Gap(8),
else if (!ws.isConnected) if (ws.isBusy)
const Icon(Symbols.power_off, size: 18) const CircularProgressIndicator(
else strokeWidth: 2.5,
const Icon(Symbols.power, size: 18), padding: EdgeInsets.zero,
], ).width(12).height(12).padding(horizontal: 4, right: 4)
).padding(horizontal: 8, vertical: 4) else if (!ws.isConnected)
: const SizedBox.shrink(), const Icon(Symbols.power_off, size: 18)
).opacity(show ? 1 : 0, animate: true).animate( else
const Duration(milliseconds: 300), const Icon(Symbols.power, size: 18),
Curves.easeInOut, ],
), ).padding(horizontal: 8, vertical: 4)
: const SizedBox.shrink(),
).opacity(show ? 1 : 0, animate: true).animate(const Duration(milliseconds: 300), Curves.easeInOut),
onTap: () { onTap: () {
if (!ws.isConnected && !ws.isBusy) { if (!ws.isConnected && !ws.isBusy) {
ws.connect(); ws.connect();

View File

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -9,6 +10,7 @@ import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/channel.dart'; import 'package:surface/providers/channel.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
@ -46,6 +48,17 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
final nav = context.watch<NavigationProvider>(); final nav = context.watch<NavigationProvider>();
final cfg = context.watch<ConfigProvider>(); 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; final backgroundColor = cfg.drawerIsExpanded ? Colors.transparent : null;
return ListenableBuilder( return ListenableBuilder(
@ -54,6 +67,7 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
return Drawer( return Drawer(
elevation: widget.elevation, elevation: widget.elevation,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(0))),
child: Column( child: Column(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -78,49 +92,70 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
Expanded( Expanded(
child: _DrawerContentList(), child: _DrawerContentList(),
), ),
if (cfg.hideBottomNav) if (showNavButtons)
Row( Row(
spacing: 8, spacing: 8,
children: nav.destinations.where((ele) => ele.isPinned).map( children:
(ele) { nav.destinations.where((ele) => ele.isPinned).mapIndexed(
(idx, ele) {
return Expanded( return Expanded(
child: Tooltip( child: Tooltip(
message: ele.label.tr(), message: ele.label.tr(),
child: IconButton.filledTonal( child: IconButton(
icon: ele.icon, icon: ele.icon,
color: Theme.of(context) color: nav.currentIndex == idx
.colorScheme ? Theme.of(context)
.onPrimaryContainer, .colorScheme
.onPrimaryContainer
: Theme.of(context).colorScheme.onSurface,
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
nav.currentIndex == idx
? Theme.of(context)
.colorScheme
.primaryContainer
: Colors.transparent,
),
),
onPressed: () { onPressed: () {
GoRouter.of(context).goNamed(ele.screen); GoRouter.of(context).goNamed(ele.screen);
Scaffold.of(context).closeDrawer(); Scaffold.of(context).closeDrawer();
nav.setIndex(idx);
}, },
), ),
), ),
); );
}, },
).toList(), ).toList(),
).padding(horizontal: 16), ).padding(horizontal: 16, bottom: 8),
Align( Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: ListTile( child: ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 24), contentPadding: EdgeInsets.symmetric(horizontal: 24),
leading: AccountImage(content: ua.user?.avatar), leading: AccountImage(
title: Text(ua.user?.nick ?? 'unknown'.tr()).fontSize(15), content: ua.user?.avatar,
subtitle: fallbackWidget:
Text('@${ua.user?.name ?? 'unknown'.tr()}').fontSize(13), 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( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
IconButton( if (ua.isAuthorized)
icon: const Icon(Symbols.notifications, fill: 1), IconButton(
padding: EdgeInsets.zero, icon: const Icon(Symbols.notifications, fill: 1),
visualDensity: VisualDensity.compact, padding: EdgeInsets.zero,
onPressed: () { visualDensity: VisualDensity.compact,
GoRouter.of(context).pushNamed('notification'); onPressed: () {
Scaffold.of(context).closeDrawer(); GoRouter.of(context).pushNamed('notification');
}, Scaffold.of(context).closeDrawer();
), },
),
IconButton( IconButton(
icon: const Icon(Symbols.settings, fill: 1), icon: const Icon(Symbols.settings, fill: 1),
padding: EdgeInsets.zero, 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 ct = context.read<ChatChannelProvider>();
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final nav = context.watch<NavigationProvider>(); final nav = context.watch<NavigationProvider>();
final rel = context.read<SnRealmProvider>(); final rel = context.watch<SnRealmProvider>();
return PageTransitionSwitcher( return PageTransitionSwitcher(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
@ -185,16 +220,6 @@ class _DrawerContentList extends StatelessWidget {
horizontal: 32, horizontal: 32,
vertical: 12, 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) { ...rel.availableRealms.map((ele) {
return ListTile( return ListTile(
minTileHeight: 48, 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( : ListView(
@ -247,7 +282,7 @@ class _DrawerContentList extends StatelessWidget {
), ),
title: Text(nav.focusedRealm!.name), title: Text(nav.focusedRealm!.name),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).goNamed(
'realmDetail', 'realmDetail',
pathParameters: { pathParameters: {
'alias': nav.focusedRealm!.alias, 'alias': nav.focusedRealm!.alias,
@ -265,7 +300,7 @@ class _DrawerContentList extends StatelessWidget {
leading: const Icon(Symbols.globe), leading: const Icon(Symbols.globe),
title: Text('community').tr(), title: Text('community').tr(),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).goNamed(
'realmCommunity', 'realmCommunity',
pathParameters: { pathParameters: {
'alias': nav.focusedRealm!.alias, 'alias': nav.focusedRealm!.alias,
@ -290,7 +325,7 @@ class _DrawerContentList extends StatelessWidget {
leading: const Icon(Symbols.tag), leading: const Icon(Symbols.tag),
title: Text(ele.name), title: Text(ele.name),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).goNamed(
'chatRoom', 'chatRoom',
pathParameters: { pathParameters: {
'scope': ele.realm?.alias ?? 'global', 'scope': ele.realm?.alias ?? 'global',

View File

@ -103,7 +103,7 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
await sn.client await sn.client
.put('/cgi/co/questions/${widget.parentPost.id}/answer', data: { .put('/cgi/co/questions/${widget.parentPost.id}/answer', data: {
'publisher': answer.publisherId, 'publisher': widget.parentPost.publisherId,
'answer_id': answer.id, 'answer_id': answer.id,
}); });
if (!mounted) return; if (!mounted) return;

View File

@ -279,6 +279,8 @@ class _PostItemState extends State<PostItem> {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
final isAuthor = final isAuthor =
ua.isAuthorized && widget.data.publisher.accountId == ua.user?.id; 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 final displayableAttachments = widget.data.preload?.attachments
?.where((ele) => ?.where((ele) =>
@ -333,6 +335,7 @@ class _PostItemState extends State<PostItem> {
_PostActionPopup( _PostActionPopup(
data: widget.data, data: widget.data,
isAuthor: isAuthor, isAuthor: isAuthor,
isParentAuthor: isParentAuthor,
onShare: () => _doShare(context), onShare: () => _doShare(context),
onShareImage: () => _doShareViaPicture(context), onShareImage: () => _doShareViaPicture(context),
onSelectAnswer: widget.onSelectAnswer, onSelectAnswer: widget.onSelectAnswer,
@ -577,6 +580,7 @@ class _PostItemState extends State<PostItem> {
_PostActionPopup( _PostActionPopup(
data: widget.data, data: widget.data,
isAuthor: isAuthor, isAuthor: isAuthor,
isParentAuthor: isParentAuthor,
onShare: () => _doShare(context), onShare: () => _doShare(context),
onShareImage: () => _doShareViaPicture(context), onShareImage: () => _doShareViaPicture(context),
onSelectAnswer: widget.onSelectAnswer, onSelectAnswer: widget.onSelectAnswer,
@ -1317,6 +1321,7 @@ class _PostAvatar extends StatelessWidget {
class _PostActionPopup extends StatelessWidget { class _PostActionPopup extends StatelessWidget {
final SnPost data; final SnPost data;
final bool isAuthor; final bool isAuthor;
final bool isParentAuthor;
final Function onDeleted; final Function onDeleted;
final Function() onShare, onShareImage; final Function() onShare, onShareImage;
final Function()? onSelectAnswer; final Function()? onSelectAnswer;
@ -1324,6 +1329,7 @@ class _PostActionPopup extends StatelessWidget {
const _PostActionPopup({ const _PostActionPopup({
required this.data, required this.data,
required this.isAuthor, required this.isAuthor,
required this.isParentAuthor,
required this.onDeleted, required this.onDeleted,
required this.onShare, required this.onShare,
required this.onShareImage, required this.onShareImage,
@ -1397,7 +1403,7 @@ class _PostActionPopup extends StatelessWidget {
}, },
), ),
if (onTranslate != null) PopupMenuDivider(), if (onTranslate != null) PopupMenuDivider(),
if (isAuthor && onSelectAnswer != null) if (isParentAuthor && onSelectAnswer != null)
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [ children: [
@ -1410,7 +1416,7 @@ class _PostActionPopup extends StatelessWidget {
onSelectAnswer?.call(); onSelectAnswer?.call();
}, },
), ),
if (isAuthor && onSelectAnswer != null) PopupMenuDivider(), if (isParentAuthor && onSelectAnswer != null) PopupMenuDivider(),
if (isAuthor) if (isAuthor)
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(

View File

@ -2238,10 +2238,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: tray_manager name: tray_manager
sha256: "80be6c508159a6f3c57983de795209ac13453e9832fd574143b06dceee188ed2" sha256: c2da0f0f1ddb455e721cf68d05d1281fec75cf5df0a1d3cb67b6ca0bdfd5709d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.2" version: "0.4.0"
tuple: tuple:
dependency: transitive dependency: transitive
description: description:
@ -2525,11 +2525,10 @@ packages:
workmanager: workmanager:
dependency: "direct main" dependency: "direct main"
description: description:
path: workmanager name: workmanager
ref: main sha256: ed13530cccd28c5c9959ad42d657cd0666274ca74c56dea0ca183ddd527d3a00
resolved-ref: "4ce065135dc1b91fee918f81596b42a56850391d" url: "https://pub.dev"
url: "https://github.com/fluttercommunity/flutter_workmanager.git" source: hosted
source: git
version: "0.5.2" version: "0.5.2"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive

View File

@ -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 # 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 # 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. # 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: environment:
sdk: ^3.5.4 sdk: ^3.5.4
@ -103,11 +103,7 @@ dependencies:
flutter_svg: ^2.0.16 flutter_svg: ^2.0.16
home_widget: ^0.7.0 home_widget: ^0.7.0
receive_sharing_intent: ^1.8.1 receive_sharing_intent: ^1.8.1
workmanager: workmanager: ^0.5.2
git:
url: https://github.com/fluttercommunity/flutter_workmanager.git
path: workmanager
ref: main
flutter_app_update: ^3.2.2 flutter_app_update: ^3.2.2
in_app_review: ^2.0.10 in_app_review: ^2.0.10
version: ^3.0.2 version: ^3.0.2
@ -120,7 +116,7 @@ dependencies:
flutter_inappwebview: ^6.1.5 flutter_inappwebview: ^6.1.5
html: ^0.15.5 html: ^0.15.5
xml: ^6.5.0 xml: ^6.5.0
tray_manager: ^0.3.2 tray_manager: ^0.4.0
hotkey_manager: ^0.2.3 hotkey_manager: ^0.2.3
image_picker_android: ^0.8.12+20 image_picker_android: ^0.8.12+20
cached_network_image_platform_interface: ^4.1.1 cached_network_image_platform_interface: ^4.1.1
@ -179,6 +175,7 @@ flutter:
- assets/icon/icon-light-radius.png - assets/icon/icon-light-radius.png
- assets/icon/tray-icon.ico - assets/icon/tray-icon.ico
- assets/icon/tray-icon.png - assets/icon/tray-icon.png
- assets/icon/kanban-1st.jpg
- assets/translations/ - assets/translations/
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see