diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index ec90d08..1df1a27 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -890,5 +890,11 @@ }, "settingsHideBottomNav": "Hide Bottom Navigation", "settingsHideBottomNavDescription": "Hide the bottom navigation bar, and show the navigation buttons in the drawer.", - "reCaptcha": "reCaptcha" + "reCaptcha": "reCaptcha", + "friends": "Friends", + "friendsDescription": "Manage your friendships.", + "album": "Album", + "albumDescription": "View albums and manage attachments.", + "stickers": "Stickers", + "stickersDescription": "View sticker packs and manage stickers." } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 612b5e0..442e7fb 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -888,5 +888,11 @@ }, "settingsHideBottomNav": "隐藏底部导航栏", "settingsHideBottomNavDescription": "隐藏底部导航栏,在侧边栏抽屉显示导航按钮。", - "reCaptcha": "人机验证" + "reCaptcha": "人机验证", + "friends": "好友", + "friendsDescription": "管理好友关系。", + "album": "相册", + "albumDescription": "查看相册与管理上传附件。", + "stickers": "贴图", + "stickersDescription": "查看贴图包与管理贴图。" } diff --git a/assets/translations/zh-HK.json b/assets/translations/zh-HK.json index 3044e36..6c748bd 100644 --- a/assets/translations/zh-HK.json +++ b/assets/translations/zh-HK.json @@ -888,5 +888,11 @@ }, "settingsHideBottomNav": "隱藏底部導航欄", "settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。", - "reCaptcha": "人機驗證" + "reCaptcha": "人機驗證", + "friends": "好友", + "friendsDescription": "管理好友關係。", + "album": "相冊", + "albumDescription": "查看相冊與管理上傳附件。", + "stickers": "貼圖", + "stickersDescription": "查看貼圖包與管理貼圖。" } diff --git a/assets/translations/zh-TW.json b/assets/translations/zh-TW.json index 57eb7ad..1273c26 100644 --- a/assets/translations/zh-TW.json +++ b/assets/translations/zh-TW.json @@ -888,5 +888,11 @@ }, "settingsHideBottomNav": "隱藏底部導航欄", "settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。", - "reCaptcha": "人機驗證" + "reCaptcha": "人機驗證", + "friends": "好友", + "friendsDescription": "管理好友關係。", + "album": "相冊", + "albumDescription": "查看相冊與管理上傳附件。", + "stickers": "貼圖", + "stickersDescription": "查看貼圖包與管理貼圖。" } diff --git a/lib/main.dart b/lib/main.dart index c2be15e..856bb66 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:developer'; import 'dart:io'; -import 'dart:math' hide log; import 'dart:ui'; import 'package:bitsdojo_window/bitsdojo_window.dart'; @@ -161,7 +160,7 @@ class SolianApp extends StatelessWidget { Provider(create: (ctx) => SnNetworkProvider(ctx)), Provider(create: (ctx) => UserDirectoryProvider(ctx)), Provider(create: (ctx) => SnAttachmentProvider(ctx)), - Provider(create: (ctx) => SnRealmProvider(ctx)), + ChangeNotifierProvider(create: (ctx) => SnRealmProvider(ctx)), Provider(create: (ctx) => SnPostContentProvider(ctx)), Provider(create: (ctx) => SnRelationshipProvider(ctx)), Provider(create: (ctx) => SnLinkPreviewProvider(ctx)), @@ -331,25 +330,29 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { if (!mounted) return; _setPhaseText('keyPair'); final kp = context.read(); - await kp.reloadActive(); - kp.listen(); - if (!mounted) return; - _setPhaseText('stickers'); - final sticker = context.read(); - await sticker.listSticker(); - if (!mounted) return; - _setPhaseText('userDirectory'); - final ud = context.read(); - await ud.loadAccountCache(); - if (!mounted) return; - _setPhaseText('realm'); - final rm = context.read(); - await rm.refreshAvailableRealms(); - if (!mounted) return; - _setPhaseText('chat'); - final ct = context.read(); - await ct.refreshAvailableChannels(); - _setPhaseText('done'); + try { + await kp.reloadActive(); + kp.listen(); + } catch (_) {} + if (ua.isAuthorized) { + if (!mounted) return; + _setPhaseText('stickers'); + final sticker = context.read(); + await sticker.listSticker(); + if (!mounted) return; + _setPhaseText('userDirectory'); + final ud = context.read(); + await ud.loadAccountCache(); + if (!mounted) return; + _setPhaseText('realm'); + final rm = context.read(); + await rm.refreshAvailableRealms(); + if (!mounted) return; + _setPhaseText('chat'); + final ct = context.read(); + await ct.refreshAvailableChannels(); + _setPhaseText('done'); + } } catch (err) { if (!mounted) return; await context.showErrorDialog(err); @@ -534,7 +537,6 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { key: Key('app-splash-screen-$_isBusy'), child: Stack( children: [ - CustomPaint(painter: GraphPainter()), Center( child: Container( constraints: const BoxConstraints( @@ -574,44 +576,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 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; -} diff --git a/lib/providers/channel.dart b/lib/providers/channel.dart index 3aae13e..a8146ae 100644 --- a/lib/providers/channel.dart +++ b/lib/providers/channel.dart @@ -41,6 +41,11 @@ class ChatChannelProvider extends ChangeNotifier { }); } + void addAvailableChannel(SnChannel channel) { + _availableChannels.add(channel); + notifyListeners(); + } + Future _saveChannelToLocal(Iterable channels) async { await Future.wait( channels.map( diff --git a/lib/providers/navigation.dart b/lib/providers/navigation.dart index 4a9fb6b..30d1a2d 100644 --- a/lib/providers/navigation.dart +++ b/lib/providers/navigation.dart @@ -61,26 +61,6 @@ class NavigationProvider extends ChangeNotifier { screen: 'news', label: 'screenNews', ), - AppNavDestination( - icon: Icon(Symbols.emoji_emotions, weight: 400, opticalSize: 20), - screen: 'stickers', - label: 'screenStickers', - ), - AppNavDestination( - icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20), - screen: 'album', - label: 'screenAlbum', - ), - AppNavDestination( - icon: Icon(Symbols.diversity_4, weight: 400, opticalSize: 20), - screen: 'friend', - label: 'screenFriend', - ), - AppNavDestination( - icon: Icon(Symbols.notifications, weight: 400, opticalSize: 20), - screen: 'notification', - label: 'screenNotification', - ), ]; static const List kDefaultPinnedDestination = [ 'home', diff --git a/lib/providers/sn_realm.dart b/lib/providers/sn_realm.dart index efb3dde..5f47dd1 100644 --- a/lib/providers/sn_realm.dart +++ b/lib/providers/sn_realm.dart @@ -8,7 +8,7 @@ import 'package:surface/providers/database.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/types/realm.dart'; -class SnRealmProvider { +class SnRealmProvider extends ChangeNotifier { late final SnNetworkProvider _sn; late final DatabaseProvider _dt; @@ -39,6 +39,11 @@ class SnRealmProvider { return out; } + void addAvailableRealm(SnRealm realm) { + _availableRealms.add(realm); + notifyListeners(); + } + Future getRealm(dynamic aliasOrId) async { if (_cache.containsKey(aliasOrId.toString())) { return _cache[aliasOrId.toString()]!; diff --git a/lib/providers/userinfo.dart b/lib/providers/userinfo.dart index fac3563..4dd21ac 100644 --- a/lib/providers/userinfo.dart +++ b/lib/providers/userinfo.dart @@ -64,6 +64,7 @@ class UserProvider extends ChangeNotifier { } Future refreshUser() async { + if (!isAuthorized) return null; final resp = await _sn.client.get('/cgi/id/users/me'); final out = SnAccount.fromJson(resp.data); diff --git a/lib/screens/account.dart b/lib/screens/account.dart index cb5284b..2f3ec70 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -158,23 +158,33 @@ class _AuthorizedAccountScreen extends StatelessWidget { }, ), ListTile( - title: Text('abuseReport').tr(), - subtitle: Text('abuseReportActionDescription').tr(), + title: Text('friends').tr(), + subtitle: Text('friendsDescription').tr(), contentPadding: const EdgeInsets.symmetric(horizontal: 24), - leading: const Icon(Symbols.flag), + leading: const Icon(Symbols.person), trailing: const Icon(Symbols.chevron_right), onTap: () { - GoRouter.of(context).pushNamed('abuseReport'); + GoRouter.of(context).pushNamed('friend'); }, ), ListTile( - title: Text('factorSettings').tr(), - subtitle: Text('factorSettingsSubtitle').tr(), + title: Text('album').tr(), + subtitle: Text('albumDescription').tr(), contentPadding: const EdgeInsets.symmetric(horizontal: 24), - leading: const Icon(Symbols.lock), + leading: const Icon(Symbols.photo_library), trailing: const Icon(Symbols.chevron_right), onTap: () { - GoRouter.of(context).pushNamed('factorSettings'); + GoRouter.of(context).pushNamed('album'); + }, + ), + ListTile( + title: Text('stickers').tr(), + subtitle: Text('stickersDescription').tr(), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: const Icon(Symbols.emoji_emotions), + trailing: const Icon(Symbols.chevron_right), + onTap: () { + GoRouter.of(context).pushNamed('stickers'); }, ), ListTile( @@ -237,6 +247,16 @@ class _AuthorizedAccountScreen extends StatelessWidget { GoRouter.of(context).pushNamed('accountSettings'); }, ), + ListTile( + title: Text('abuseReport').tr(), + subtitle: Text('abuseReportActionDescription').tr(), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: const Icon(Symbols.flag), + trailing: const Icon(Symbols.chevron_right), + onTap: () { + GoRouter.of(context).pushNamed('abuseReport'); + }, + ), ListTile( title: Text('accountLogout').tr(), subtitle: Text('accountLogoutSubtitle').tr(), diff --git a/lib/screens/account/account_settings.dart b/lib/screens/account/account_settings.dart index 6d6900d..6e9a842 100644 --- a/lib/screens/account/account_settings.dart +++ b/lib/screens/account/account_settings.dart @@ -117,6 +117,16 @@ class AccountSettingsScreen extends StatelessWidget { GoRouter.of(context).pushNamed('accountSettingsSecurity'); }, ), + ListTile( + title: Text('factorSettings').tr(), + subtitle: Text('factorSettingsSubtitle').tr(), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: const Icon(Symbols.lock), + trailing: const Icon(Symbols.chevron_right), + onTap: () { + GoRouter.of(context).pushNamed('factorSettings'); + }, + ), ListTile( title: Text('accountProfileEdit').tr(), subtitle: Text('accountProfileEditSubtitle').tr(), diff --git a/lib/screens/album.dart b/lib/screens/album.dart index d519a59..eeaf766 100644 --- a/lib/screens/album.dart +++ b/lib/screens/album.dart @@ -10,7 +10,6 @@ import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/user_directory.dart'; import 'package:surface/types/attachment.dart'; -import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/attachment/attachment_zoom.dart'; import 'package:surface/widgets/attachment/attachment_item.dart'; import 'package:surface/widgets/dialog.dart'; @@ -106,7 +105,7 @@ class _AlbumScreenState extends State { controller: _scrollController, slivers: [ SliverAppBar( - leading: AutoAppBarLeading(), + leading: PageBackButton(), title: Text('screenAlbum').tr(), ), SliverToBoxAdapter( @@ -119,7 +118,8 @@ class _AlbumScreenState extends State { child: CircularProgressIndicator( value: _billing?.includedRatio ?? 0, strokeWidth: 8, - backgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh, + backgroundColor: + Theme.of(context).colorScheme.surfaceContainerHigh, ), ).padding(all: 12), const Gap(24), @@ -129,7 +129,8 @@ class _AlbumScreenState extends State { children: [ Text('attachmentBillingUploaded').tr().bold(), Text( - (_billing?.currentBytes ?? 0).formatBytes(decimals: 4), + (_billing?.currentBytes ?? 0) + .formatBytes(decimals: 4), style: GoogleFonts.robotoMono(), ), Text('attachmentBillingDiscount').tr().bold(), diff --git a/lib/screens/friend.dart b/lib/screens/friend.dart index 912bfa1..b211715 100644 --- a/lib/screens/friend.dart +++ b/lib/screens/friend.dart @@ -201,7 +201,7 @@ class _FriendScreenState extends State { if (!ua.isAuthorized) { return AppScaffold( appBar: AppBar( - leading: AutoAppBarLeading(), + leading: PageBackButton(), title: Text('screenFriend').tr(), ), body: Center( @@ -254,7 +254,8 @@ class _FriendScreenState extends State { trailing: const Icon(Symbols.chevron_right), onTap: _showBlocks, ), - if (_requests.isNotEmpty || _blocks.isNotEmpty) const Divider(height: 1), + if (_requests.isNotEmpty || _blocks.isNotEmpty) + const Divider(height: 1), Expanded( child: MediaQuery.removePadding( context: context, @@ -270,7 +271,8 @@ class _FriendScreenState extends State { final relation = _relations[index]; final other = relation.related; return ListTile( - contentPadding: const EdgeInsets.only(right: 24, left: 16), + contentPadding: + const EdgeInsets.only(right: 24, left: 16), leading: AccountImage(content: other?.avatar), title: Text(other?.nick ?? 'unknown'), subtitle: Text(other?.nick ?? 'unknown'), @@ -286,12 +288,16 @@ class _FriendScreenState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ InkWell( - onTap: _isUpdating ? null : () => _changeRelation(relation, 2), + onTap: _isUpdating + ? null + : () => _changeRelation(relation, 2), child: Text('friendBlock').tr(), ), const Gap(8), InkWell( - onTap: _isUpdating ? null : () => _deleteRelation(relation), + onTap: _isUpdating + ? null + : () => _deleteRelation(relation), child: Text('friendDeleteAction').tr(), ), ], @@ -420,7 +426,9 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text(kFriendStatus[relation.status] ?? 'unknown').tr().opacity(0.75), + Text(kFriendStatus[relation.status] ?? 'unknown') + .tr() + .opacity(0.75), if (relation.status == 0) Row( mainAxisAlignment: MainAxisAlignment.end, @@ -441,7 +449,8 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> { mainAxisAlignment: MainAxisAlignment.end, children: [ InkWell( - onTap: _isBusy ? null : () => _changeRelation(relation, 1), + onTap: + _isBusy ? null : () => _changeRelation(relation, 1), child: Text('friendUnblock').tr(), ), const Gap(8), diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 4f37fcc..c13da17 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -806,7 +806,7 @@ class _HomeDashNotificationWidgetState child: IconButton( icon: const Icon(Symbols.arrow_right_alt), onPressed: () { - GoRouter.of(context).goNamed('notification'); + GoRouter.of(context).pushNamed('notification'); }, ), ), diff --git a/lib/screens/notification.dart b/lib/screens/notification.dart index c48d90f..3305898 100644 --- a/lib/screens/notification.dart +++ b/lib/screens/notification.dart @@ -149,8 +149,9 @@ class _NotificationScreenState extends State { if (!ua.isAuthorized) { return AppScaffold( appBar: AppBar( - leading: AutoAppBarLeading(), - title: Text('screenNotification').tr()), + leading: PageBackButton(), + title: Text('screenNotification').tr(), + ), body: Center(child: UnauthorizedHint()), ); } diff --git a/lib/screens/realm/realm_discovery.dart b/lib/screens/realm/realm_discovery.dart index ffa389f..49cac78 100644 --- a/lib/screens/realm/realm_discovery.dart +++ b/lib/screens/realm/realm_discovery.dart @@ -4,8 +4,10 @@ import 'package:gap/gap.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; +import 'package:surface/providers/channel.dart'; import 'package:surface/providers/config.dart'; import 'package:surface/providers/sn_network.dart'; +import 'package:surface/providers/sn_realm.dart'; import 'package:surface/providers/userinfo.dart'; import 'package:surface/types/chat.dart'; import 'package:surface/types/realm.dart'; @@ -57,7 +59,9 @@ class _RealmDiscoveryScreenState extends State { title: Text('screenRealmDiscovery').tr(), actions: [ IconButton( - icon: _isCompactView ? const Icon(Symbols.view_list) : const Icon(Symbols.view_module), + icon: _isCompactView + ? const Icon(Symbols.view_list) + : const Icon(Symbols.view_module), onPressed: () { setState(() => _isCompactView = !_isCompactView); context.read().realmCompactView = _isCompactView; @@ -117,7 +121,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> { try { setState(() => _isBusy = true); final sn = context.read(); - 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.from( resp.data.map((e) => SnChannel.fromJson(e)).cast(), ); @@ -135,10 +140,13 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> { setState(() => _isJoining = true); final sn = context.read(); final ua = context.read(); - await sn.client.post('/cgi/id/realms/${widget.realm.alias}/members', data: { + final rel = context.read(); + await sn.client + .post('/cgi/id/realms/${widget.realm.alias}/members', data: { 'related': ua.user?.name, }); await _joinSelectedChannels(); + rel.addAvailableRealm(widget.realm); if (!mounted) return; context.showSnackbar('realmJoined'.tr(args: [widget.realm.name])); Navigator.pop(context); @@ -156,13 +164,20 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> { try { final sn = context.read(); final ua = context.read(); - await sn.client.post('/cgi/im/channels/${widget.realm.alias}/$channel/members', data: { - 'related': ua.user?.name, - }); + await sn.client.post( + '/cgi/im/channels/${widget.realm.alias}/$channel/members', + data: { + 'related': ua.user?.name, + }); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } + final ct = context.read(); + for (final channel + in _channels!.where((ele) => _planJoinChannels.contains(ele.alias))) { + ct.addAvailableChannel(channel); + } } } @@ -182,7 +197,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> { children: [ const Icon(Symbols.group_add, size: 24), const Gap(16), - Text('realmJoin', style: Theme.of(context).textTheme.titleLarge).tr(), + Text('realmJoin', style: Theme.of(context).textTheme.titleLarge) + .tr(), ], ).padding(horizontal: 20, top: 16, bottom: 12), Row( @@ -216,7 +232,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> { Container( width: double.infinity, color: Theme.of(context).colorScheme.surfaceContainerHigh, - child: Text('realmCommunityPublicChannelsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium) + child: Text('realmCommunityPublicChannelsHint'.tr(), + style: Theme.of(context).textTheme.bodyMedium) .padding(horizontal: 24, vertical: 8), ), Expanded( diff --git a/lib/screens/stickers.dart b/lib/screens/stickers.dart index 475d684..e254974 100644 --- a/lib/screens/stickers.dart +++ b/lib/screens/stickers.dart @@ -9,7 +9,6 @@ import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_sticker.dart'; import 'package:surface/providers/userinfo.dart'; import 'package:surface/types/attachment.dart'; -import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/attachment/attachment_item.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; @@ -134,7 +133,7 @@ class _StickerScreenState extends State Widget build(BuildContext context) { return AppScaffold( appBar: AppBar( - leading: AutoAppBarLeading(), + leading: PageBackButton(), title: Text('screenStickers').tr(), actions: [ IconButton( diff --git a/lib/widgets/navigation/app_drawer_navigation.dart b/lib/widgets/navigation/app_drawer_navigation.dart index e2b2606..40b5498 100644 --- a/lib/widgets/navigation/app_drawer_navigation.dart +++ b/lib/widgets/navigation/app_drawer_navigation.dart @@ -155,7 +155,7 @@ class _DrawerContentList extends StatelessWidget { final ct = context.read(); final sn = context.read(); final nav = context.watch(); - final rel = context.read(); + final rel = context.watch(); return PageTransitionSwitcher( duration: const Duration(milliseconds: 300), @@ -188,13 +188,14 @@ class _DrawerContentList extends StatelessWidget { ListTile( minTileHeight: 48, contentPadding: EdgeInsets.only(left: 28, right: 16), - leading: const Icon(Symbols.home), + leading: const Icon(Symbols.home).padding(right: 4), title: Text('screenHome').tr(), onTap: () { GoRouter.of(context).goNamed('home'); Scaffold.of(context).closeDrawer(); }, ), + const Divider(height: 1).padding(vertical: 4), ...rel.availableRealms.map((ele) { return ListTile( minTileHeight: 48, @@ -209,6 +210,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(