diff --git a/assets/audio/sfx/launch-done.mp3 b/assets/audio/sfx/launch-done.mp3 new file mode 100644 index 0000000..1f456f7 Binary files /dev/null and b/assets/audio/sfx/launch-done.mp3 differ diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 271a347..bb83128 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -939,5 +939,7 @@ "settingsSoundEffects": "Sound Effects", "settingsSoundEffectsDescription": "Enable the sound effects around the app.", "settingsResetMemorizedWindowSize": "Reset Window Size", - "settingsResetMemorizedWindowSizeDescription": "Reset the memorized window size, and set it to the default size." + "settingsResetMemorizedWindowSizeDescription": "Reset the memorized window size, and set it to the default size.", + "chatDirect": "Direct Messages", + "back": "返回" } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index c33c6a2..c0fc6fc 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -936,5 +936,7 @@ "settingsSoundEffects": "声音效果", "settingsSoundEffectsDescription": "在一些场合下启用声音特效。", "settingsResetMemorizedWindowSize": "重置窗口大小", - "settingsResetMemorizedWindowSizeDescription": "重置记忆的窗口大小,以重新设置为默认大小。" + "settingsResetMemorizedWindowSizeDescription": "重置记忆的窗口大小,以重新设置为默认大小。", + "chatDirect": "私信", + "back": "返回" } diff --git a/lib/main.dart b/lib/main.dart index cde9f03..c92383b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -347,7 +347,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { if (!mounted) return; _setPhaseText('keyPair'); final kp = context.read<KeyPairProvider>(); - await kp.reloadActive(); + kp.reloadActive(); kp.listen(); } catch (_) {} if (ua.isAuthorized) { @@ -396,8 +396,8 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { final cfg = context.read<ConfigProvider>(); if (!cfg.soundEffects) return; - final player = AudioPlayer(playerId: 'launch-intro-player'); - await player.play(AssetSource('audio/sfx/launch-intro.mp3'), volume: 0.5); + final player = AudioPlayer(playerId: 'launch-done-player'); + await player.play(AssetSource('audio/sfx/launch-done.mp3'), volume: 0.8); player.onPlayerComplete.listen((_) { player.dispose(); }); diff --git a/lib/providers/config.dart b/lib/providers/config.dart index 12113f3..166a298 100644 --- a/lib/providers/config.dart +++ b/lib/providers/config.dart @@ -13,7 +13,6 @@ const kNetworkServerStoreKey = 'app_server_url'; const kAppbarTransparentStoreKey = 'app_bar_transparent'; const kAppBackgroundStoreKey = 'app_has_background'; const kAppColorSchemeStoreKey = 'app_color_scheme'; -const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse'; const kAppNotifyWithHaptic = 'app_notify_with_haptic'; const kAppExpandPostLink = 'app_expand_post_link'; const kAppExpandChatLink = 'app_expand_chat_link'; @@ -47,27 +46,17 @@ class ConfigProvider extends ChangeNotifier { } bool drawerIsCollapsed = false; - bool drawerIsExpanded = false; void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) { bool newDrawerIsCollapsed = false; - bool newDrawerIsExpanded = false; if (withMediaQuery) { newDrawerIsCollapsed = MediaQuery.of(context).size.width < 600; - newDrawerIsExpanded = MediaQuery.of(context).size.width >= 601; } else { final rpb = ResponsiveBreakpoints.of(context); newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE); - newDrawerIsExpanded = rpb.largerThan(TABLET) - ? (prefs.getBool(kAppDrawerPreferCollapse) ?? false) - ? false - : true - : false; } - if (newDrawerIsExpanded != drawerIsExpanded || - newDrawerIsCollapsed != drawerIsCollapsed) { - drawerIsExpanded = newDrawerIsExpanded; + if (newDrawerIsCollapsed != drawerIsCollapsed) { drawerIsCollapsed = newDrawerIsCollapsed; notifyListeners(); } diff --git a/lib/providers/navigation.dart b/lib/providers/navigation.dart index ec8e037..faacbec 100644 --- a/lib/providers/navigation.dart +++ b/lib/providers/navigation.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:surface/types/realm.dart'; class AppNavListItem { final String title; @@ -60,11 +59,6 @@ class NavigationProvider extends ChangeNotifier { screen: 'chat', label: 'screenChat', ), - AppNavDestination( - icon: Icon(Symbols.account_circle, weight: 400, opticalSize: 20), - screen: 'account', - label: 'screenAccount', - ), AppNavDestination( icon: Icon(Symbols.group, weight: 400, opticalSize: 20), screen: 'realm', @@ -75,6 +69,11 @@ class NavigationProvider extends ChangeNotifier { screen: 'news', label: 'screenNews', ), + AppNavDestination( + icon: Icon(Symbols.settings, weight: 400, opticalSize: 20), + screen: 'settings', + label: 'screenSettings', + ), ]; static const List<String> kDefaultPinnedDestination = [ 'home', @@ -135,11 +134,4 @@ class NavigationProvider extends ChangeNotifier { _currentIndex = idx; notifyListeners(); } - - SnRealm? focusedRealm; - - void setFocusedRealm(SnRealm? realm) { - focusedRealm = realm; - notifyListeners(); - } } diff --git a/lib/providers/notification.dart b/lib/providers/notification.dart index b592a27..bbb503d 100644 --- a/lib/providers/notification.dart +++ b/lib/providers/notification.dart @@ -105,6 +105,7 @@ class NotificationProvider extends ChangeNotifier { if (now.day == 1 && now.month == 4) { _notifySoundPlayer.play( AssetSource('audio/notify/metal-pipe.mp3'), + volume: 0.6, ); } } diff --git a/lib/router.dart b/lib/router.dart index e8a8926..fe092a0 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -72,8 +72,8 @@ final _appRoutes = [ ), GoRoute( path: '/posts', - name: 'explore', - builder: (context, state) => const ExploreScreen(), + name: 'posts', + builder: (_, __) => const SizedBox.shrink(), routes: [ GoRoute( path: '/draft', @@ -111,156 +111,194 @@ final _appRoutes = [ state.uri.queryParameters['categories']?.split(','), ), ), + ], + ), + ShellRoute( + builder: (context, state, child) => ResponsiveScaffold( + asideFlex: 2, + contentFlex: 3, + aside: const ExploreScreen(), + child: child, + ), + routes: [ + GoRoute( + path: '/explore', + name: 'explore', + builder: (context, state) => const ResponsiveScaffoldLanding( + child: ExploreScreen(), + ), + ), + GoRoute( + path: '/posts/:slug', + name: 'postDetail', + builder: (context, state) => PostDetailScreen( + key: ValueKey(state.pathParameters['slug']!), + slug: state.pathParameters['slug']!, + preload: state.extra as SnPost?, + ), + ), GoRoute( path: '/publishers/:name', name: 'postPublisher', builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!), ), - GoRoute( - path: '/:slug', - name: 'postDetail', - builder: (context, state) => PostDetailScreen( - slug: state.pathParameters['slug']!, - preload: state.extra as SnPost?, - ), - ), ], ), - GoRoute( - path: '/account', - name: 'account', - builder: (context, state) => const AccountScreen(), + ShellRoute( + builder: (context, state, child) => ResponsiveScaffold( + aside: const AccountScreen(), + child: child, + ), routes: [ GoRoute( - path: '/punishments', - name: 'accountPunishments', - builder: (context, state) => const PunishmentsScreen(), - ), - GoRoute( - path: '/programs', - name: 'accountProgram', - builder: (context, state) => const AccountProgramScreen(), - ), - GoRoute( - path: '/contacts', - name: 'accountContactMethods', - builder: (context, state) => const AccountContactMethod(), - ), - GoRoute( - path: '/events', - name: 'accountActionEvents', - builder: (context, state) => const ActionEventScreen(), - ), - GoRoute( - path: '/tickets', - name: 'accountAuthTickets', - builder: (context, state) => const AccountAuthTicket(), - ), - GoRoute( - path: '/badges', - name: 'accountBadges', - builder: (context, state) => const AccountBadgesScreen(), - ), - GoRoute( - path: '/wallet', - name: 'accountWallet', - builder: (context, state) => const WalletScreen(), - ), - GoRoute( - path: '/keypairs', - name: 'accountKeyPairs', - builder: (context, state) => const KeyPairScreen(), - ), - GoRoute( - path: '/settings', - name: 'accountSettings', - builder: (context, state) => AccountSettingsScreen(), + path: '/account', + name: 'account', + builder: (context, state) => + const ResponsiveScaffoldLanding(child: AccountScreen()), routes: [ GoRoute( - path: '/notify', - name: 'accountSettingsNotify', - builder: (context, state) => const AccountNotifyPrefsScreen(), + path: '/punishments', + name: 'accountPunishments', + builder: (context, state) => const PunishmentsScreen(), ), GoRoute( - path: '/auth', - name: 'accountSettingsSecurity', - builder: (context, state) => const AccountSecurityPrefsScreen(), + path: '/programs', + name: 'accountProgram', + builder: (context, state) => const AccountProgramScreen(), + ), + GoRoute( + path: '/contacts', + name: 'accountContactMethods', + builder: (context, state) => const AccountContactMethod(), + ), + GoRoute( + path: '/events', + name: 'accountActionEvents', + builder: (context, state) => const ActionEventScreen(), + ), + GoRoute( + path: '/tickets', + name: 'accountAuthTickets', + builder: (context, state) => const AccountAuthTicket(), + ), + GoRoute( + path: '/badges', + name: 'accountBadges', + builder: (context, state) => const AccountBadgesScreen(), + ), + GoRoute( + path: '/wallet', + name: 'accountWallet', + builder: (context, state) => const WalletScreen(), + ), + GoRoute( + path: '/keypairs', + name: 'accountKeyPairs', + builder: (context, state) => const KeyPairScreen(), + ), + GoRoute( + path: '/settings', + name: 'accountSettings', + builder: (context, state) => AccountSettingsScreen(), + routes: [ + GoRoute( + path: '/notify', + name: 'accountSettingsNotify', + builder: (context, state) => const AccountNotifyPrefsScreen(), + ), + GoRoute( + path: '/auth', + name: 'accountSettingsSecurity', + builder: (context, state) => const AccountSecurityPrefsScreen(), + ), + ], + ), + GoRoute( + path: '/settings/factors', + name: 'factorSettings', + builder: (context, state) => FactorSettingsScreen(), + ), + GoRoute( + path: '/profile/edit', + name: 'accountProfileEdit', + builder: (context, state) => ProfileEditScreen(), + ), + GoRoute( + path: '/publishers', + name: 'accountPublishers', + builder: (context, state) => PublisherScreen(), + ), + GoRoute( + path: '/publishers/new', + name: 'accountPublisherNew', + builder: (context, state) => AccountPublisherNewScreen(), + ), + GoRoute( + path: '/publishers/edit/:name', + name: 'accountPublisherEdit', + builder: (context, state) => AccountPublisherEditScreen( + name: state.pathParameters['name']!, + ), ), ], ), - GoRoute( - path: '/settings/factors', - name: 'factorSettings', - builder: (context, state) => FactorSettingsScreen(), - ), - GoRoute( - path: '/profile/edit', - name: 'accountProfileEdit', - builder: (context, state) => ProfileEditScreen(), - ), - GoRoute( - path: '/publishers', - name: 'accountPublishers', - builder: (context, state) => PublisherScreen(), - ), - GoRoute( - path: '/publishers/new', - name: 'accountPublisherNew', - builder: (context, state) => AccountPublisherNewScreen(), - ), - GoRoute( - path: '/publishers/edit/:name', - name: 'accountPublisherEdit', - builder: (context, state) => AccountPublisherEditScreen( - name: state.pathParameters['name']!, - ), - ), - GoRoute( - path: '/profile/:name', - name: 'accountProfilePage', - pageBuilder: (context, state) => NoTransitionPage( - child: UserScreen(name: state.pathParameters['name']!), - ), - ), ], ), GoRoute( - path: '/chat', - name: 'chat', - builder: (context, state) => const ChatScreen(), + path: '/accounts/:name', + name: 'accountProfilePage', + pageBuilder: (context, state) => NoTransitionPage( + child: UserScreen(name: state.pathParameters['name']!), + ), + ), + ShellRoute( + builder: (context, state, child) => + ResponsiveScaffold(aside: const ChatScreen(), child: child), routes: [ GoRoute( - path: '/:scope/:alias', - name: 'chatRoom', - builder: (context, state) => ChatRoomScreen( - scope: state.pathParameters['scope']!, - alias: state.pathParameters['alias']!, - extra: state.extra as ChatRoomScreenExtra?, - ), - ), - GoRoute( - path: '/:scope/:alias/call', - name: 'chatCallRoom', - builder: (context, state) => CallRoomScreen( - scope: state.pathParameters['scope']!, - alias: state.pathParameters['alias']!, - ), - ), - GoRoute( - path: '/:scope/:alias/detail', - name: 'channelDetail', - builder: (context, state) => ChannelDetailScreen( - scope: state.pathParameters['scope']!, - alias: state.pathParameters['alias']!, - ), - ), - GoRoute( - path: '/manage', - name: 'chatManage', - builder: (context, state) => ChatManageScreen( - editingChannelAlias: state.uri.queryParameters['editing'], + path: '/chat', + name: 'chat', + builder: (context, state) => const ResponsiveScaffoldLanding( + child: ChatScreen(), ), + routes: [ + GoRoute( + path: '/:scope/:alias', + name: 'chatRoom', + builder: (context, state) => ChatRoomScreen( + key: ValueKey( + '${state.pathParameters['scope']!}:${state.pathParameters['alias']!}', + ), + scope: state.pathParameters['scope']!, + alias: state.pathParameters['alias']!, + extra: state.extra as ChatRoomScreenExtra?, + ), + ), + GoRoute( + path: '/:scope/:alias/call', + name: 'chatCallRoom', + builder: (context, state) => CallRoomScreen( + scope: state.pathParameters['scope']!, + alias: state.pathParameters['alias']!, + ), + ), + GoRoute( + path: '/:scope/:alias/detail', + name: 'channelDetail', + builder: (context, state) => ChannelDetailScreen( + scope: state.pathParameters['scope']!, + alias: state.pathParameters['alias']!, + ), + ), + GoRoute( + path: '/manage', + name: 'chatManage', + builder: (context, state) => ChatManageScreen( + editingChannelAlias: state.uri.queryParameters['editing'], + ), + ), + ], ), ], ), diff --git a/lib/screens/account.dart b/lib/screens/account.dart index 8fa83e1..e5d3feb 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -110,6 +110,7 @@ class AccountScreen extends StatelessWidget { final sn = context.read<SnNetworkProvider>(); return AppScaffold( + noBackground: true, appBar: AppBar( leading: AutoAppBarLeading(), title: Text("screenAccount").tr(), @@ -141,15 +142,6 @@ class AccountScreen extends StatelessWidget { ], ) : null, - actions: [ - IconButton( - icon: const Icon(Symbols.settings, fill: 1), - onPressed: () { - GoRouter.of(context).pushNamed('settings'); - }, - ), - const Gap(8), - ], ), body: SingleChildScrollView( child: ua.isAuthorized diff --git a/lib/screens/account/action_events.dart b/lib/screens/account/action_events.dart index d891a62..bc8e466 100644 --- a/lib/screens/account/action_events.dart +++ b/lib/screens/account/action_events.dart @@ -59,6 +59,7 @@ class _ActionEventScreenState extends State<ActionEventScreen> { @override Widget build(BuildContext context) { return AppScaffold( + noBackground: true, appBar: AppBar( leading: const PageBackButton(), title: Text('accountActionEvent').tr(), diff --git a/lib/screens/account/auth_tickets.dart b/lib/screens/account/auth_tickets.dart index eb59662..de13ad7 100644 --- a/lib/screens/account/auth_tickets.dart +++ b/lib/screens/account/auth_tickets.dart @@ -91,6 +91,7 @@ class _AccountAuthTicketState extends State<AccountAuthTicket> { @override Widget build(BuildContext context) { return AppScaffold( + noBackground: true, appBar: AppBar( leading: const PageBackButton(), title: Text('accountAuthTickets').tr(), diff --git a/lib/screens/account/badges.dart b/lib/screens/account/badges.dart index 86fe9c9..660583d 100644 --- a/lib/screens/account/badges.dart +++ b/lib/screens/account/badges.dart @@ -70,6 +70,7 @@ class _AccountBadgesScreenState extends State<AccountBadgesScreen> { @override Widget build(BuildContext context) { return AppScaffold( + noBackground: true, appBar: AppBar( title: Text('screenAccountBadges').tr(), ), diff --git a/lib/screens/account/contact_methods.dart b/lib/screens/account/contact_methods.dart index ae6f8fa..99607ac 100644 --- a/lib/screens/account/contact_methods.dart +++ b/lib/screens/account/contact_methods.dart @@ -69,6 +69,7 @@ class _AccountContactMethodState extends State<AccountContactMethod> { @override Widget build(BuildContext context) { return AppScaffold( + noBackground: true, appBar: AppBar( leading: const PageBackButton(), title: Text('accountContactMethods').tr(), diff --git a/lib/screens/account/factor_settings.dart b/lib/screens/account/factor_settings.dart index 56e63a7..6bb7b66 100644 --- a/lib/screens/account/factor_settings.dart +++ b/lib/screens/account/factor_settings.dart @@ -16,7 +16,11 @@ final Map<int, (String, String, IconData)> kFactorTypes = { 0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password), 1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email), 2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer), - 3: ('authFactorInAppNotify', 'authFactorInAppNotifyDescription', Symbols.notifications_active), + 3: ( + 'authFactorInAppNotify', + 'authFactorInAppNotifyDescription', + Symbols.notifications_active + ), }; class FactorSettingsScreen extends StatefulWidget { @@ -36,7 +40,10 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> { final sn = context.read<SnNetworkProvider>(); final resp = await sn.client.get('/cgi/id/users/me/factors'); _factors = List<SnAuthFactor>.from( - resp.data?.map((e) => SnAuthFactor.fromJson(e as Map<String, dynamic>)).toList() ?? [], + resp.data + ?.map((e) => SnAuthFactor.fromJson(e as Map<String, dynamic>)) + .toList() ?? + [], ); } catch (err) { if (!mounted) return; @@ -55,6 +62,7 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> { @override Widget build(BuildContext context) { return AppScaffold( + noBackground: true, appBar: AppBar( leading: PageBackButton(), title: Text('screenFactorSettings').tr(), @@ -96,7 +104,8 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> { return ListTile( title: Text(kFactorTypes[ele.type]!.$1).tr(), subtitle: Text(kFactorTypes[ele.type]!.$2).tr(), - contentPadding: const EdgeInsets.only(left: 24, right: 12), + contentPadding: + const EdgeInsets.only(left: 24, right: 12), leading: Icon(kFactorTypes[ele.type]!.$3), trailing: IconButton( icon: const Icon(Symbols.close), @@ -105,14 +114,17 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> { context .showConfirmDialog( 'authFactorDelete'.tr(), - 'authFactorDeleteDescription'.tr(args: [kFactorTypes[ele.type]!.$1.tr()]), + 'authFactorDeleteDescription'.tr( + args: [kFactorTypes[ele.type]!.$1.tr()]), ) .then((val) async { if (!val) return; try { if (!context.mounted) return; - final sn = context.read<SnNetworkProvider>(); - await sn.client.delete('/cgi/id/users/me/factors/${ele.id}'); + final sn = + context.read<SnNetworkProvider>(); + await sn.client.delete( + '/cgi/id/users/me/factors/${ele.id}'); _fetchFactors(); } catch (err) { if (!context.mounted) return; @@ -191,7 +203,9 @@ class _FactorNewDialogState extends State<_FactorNewDialog> { value: _factorType, items: kFactorTypes.entries.map( (ele) { - final contains = widget.currentlyHave.map((ele) => ele.type).contains(ele.key); + final contains = widget.currentlyHave + .map((ele) => ele.type) + .contains(ele.key); return DropdownMenuItem<int>( enabled: !contains, value: ele.key, diff --git a/lib/screens/account/keypairs.dart b/lib/screens/account/keypairs.dart index 8360452..61423a9 100644 --- a/lib/screens/account/keypairs.dart +++ b/lib/screens/account/keypairs.dart @@ -37,6 +37,7 @@ class _KeyPairScreenState extends State<KeyPairScreen> { @override Widget build(BuildContext context) { return AppScaffold( + noBackground: true, appBar: AppBar( title: Text('screenKeyPairs').tr(), ), diff --git a/lib/screens/account/prefs/notify.dart b/lib/screens/account/prefs/notify.dart index b3da351..957c48e 100644 --- a/lib/screens/account/prefs/notify.dart +++ b/lib/screens/account/prefs/notify.dart @@ -75,6 +75,7 @@ class _AccountNotifyPrefsScreenState extends State<AccountNotifyPrefsScreen> { @override Widget build(BuildContext context) { return AppScaffold( + noBackground: true, appBar: AppBar( leading: const PageBackButton(), title: Text('accountSettingsNotify').tr(), diff --git a/lib/screens/account/prefs/security.dart b/lib/screens/account/prefs/security.dart index 9d6c90c..9d2d071 100644 --- a/lib/screens/account/prefs/security.dart +++ b/lib/screens/account/prefs/security.dart @@ -70,6 +70,7 @@ class _AccountSecurityPrefsScreenState @override Widget build(BuildContext context) { return AppScaffold( + noBackground: true, appBar: AppBar( leading: const PageBackButton(), title: Text('accountSettingsSecurity').tr(), diff --git a/lib/screens/account/profile_edit.dart b/lib/screens/account/profile_edit.dart index 2e2696f..ba135a2 100644 --- a/lib/screens/account/profile_edit.dart +++ b/lib/screens/account/profile_edit.dart @@ -66,37 +66,40 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { _locationController.text = prof.profile!.location; _avatar = prof.avatar; _banner = prof.banner; - _links = prof.profile!.links.entries.map((ele) => (ele.key, ele.value)).toList(); + _links = + prof.profile!.links.entries.map((ele) => (ele.key, ele.value)).toList(); _birthday = prof.profile!.birthday?.toLocal(); if (_birthday != null) { - _birthdayController.text = DateFormat(_kDateFormat).format(prof.profile!.birthday!.toLocal()); + _birthdayController.text = + DateFormat(_kDateFormat).format(prof.profile!.birthday!.toLocal()); } } void _selectBirthday() async { await showCupertinoModalPopup<DateTime?>( context: context, - builder: - (BuildContext context) => Container( - height: 216, - padding: const EdgeInsets.only(top: 6.0), - margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), - color: Theme.of(context).colorScheme.surface, - child: SafeArea( - top: false, - child: CupertinoDatePicker( - initialDateTime: _birthday?.toLocal(), - mode: CupertinoDatePickerMode.date, - use24hFormat: true, - onDateTimeChanged: (DateTime newDate) { - setState(() { - _birthday = newDate; - _birthdayController.text = DateFormat(_kDateFormat).format(_birthday!); - }); - }, - ), - ), + builder: (BuildContext context) => Container( + height: 216, + padding: const EdgeInsets.only(top: 6.0), + margin: + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + color: Theme.of(context).colorScheme.surface, + child: SafeArea( + top: false, + child: CupertinoDatePicker( + initialDateTime: _birthday?.toLocal(), + mode: CupertinoDatePickerMode.date, + use24hFormat: true, + onDateTimeChanged: (DateTime newDate) { + setState(() { + _birthday = newDate; + _birthdayController.text = + DateFormat(_kDateFormat).format(_birthday!); + }); + }, ), + ), + ), ); } @@ -109,29 +112,32 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { Uint8List? rawBytes; if (!skipCrop) { - final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); - final aspectRatios = - place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)]; - final result = - (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) - ? await showCupertinoImageCropper( - // ignore: use_build_context_synchronously - context, - allowedAspectRatios: aspectRatios, - imageProvider: imageProvider, - ) - : await showMaterialImageCropper( - // ignore: use_build_context_synchronously - context, - allowedAspectRatios: aspectRatios, - imageProvider: imageProvider, - ); + final ImageProvider imageProvider = + kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); + final aspectRatios = place == 'banner' + ? [CropAspectRatio(width: 16, height: 7)] + : [CropAspectRatio(width: 1, height: 1)]; + final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) + ? await showCupertinoImageCropper( + // ignore: use_build_context_synchronously + context, + allowedAspectRatios: aspectRatios, + imageProvider: imageProvider, + ) + : await showMaterialImageCropper( + // ignore: use_build_context_synchronously + context, + allowedAspectRatios: aspectRatios, + imageProvider: imageProvider, + ); if (result == null) return; if (!mounted) return; setState(() => _isBusy = true); - rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List(); + rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))! + .buffer + .asUint8List(); } else { if (!mounted) return; setState(() => _isBusy = true); @@ -152,7 +158,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { if (!mounted) return; final sn = context.read<SnNetworkProvider>(); - await sn.client.put('/cgi/id/users/me/$place', data: {'attachment': attachment.rid}); + await sn.client + .put('/cgi/id/users/me/$place', data: {'attachment': attachment.rid}); if (!mounted) return; final ua = context.read<UserProvider>(); @@ -188,7 +195,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { 'location': _locationController.value.text, 'birthday': _birthday?.toUtc().toIso8601String(), 'links': { - for (final link in _links!.where((ele) => ele.$1.isNotEmpty && ele.$2.isNotEmpty)) link.$1: link.$2, + for (final link in _links! + .where((ele) => ele.$1.isNotEmpty && ele.$2.isNotEmpty)) + link.$1: link.$2, }, }, ); @@ -235,7 +244,10 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { final sn = context.read<SnNetworkProvider>(); return AppScaffold( - appBar: AppBar(leading: const PageBackButton(), title: Text('screenAccountProfileEdit').tr()), + noBackground: true, + appBar: AppBar( + leading: const PageBackButton(), + title: Text('screenAccountProfileEdit').tr()), body: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -253,11 +265,14 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { child: AspectRatio( aspectRatio: 16 / 9, child: Container( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - child: - _banner != null - ? AutoResizeUniversalImage(sn.getAttachmentUrl(_banner!), fit: BoxFit.cover) - : const SizedBox.shrink(), + color: Theme.of(context) + .colorScheme + .surfaceContainerHigh, + child: _banner != null + ? AutoResizeUniversalImage( + sn.getAttachmentUrl(_banner!), + fit: BoxFit.cover) + : const SizedBox.shrink(), ), ), ), @@ -294,12 +309,16 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { labelText: 'fieldUsername'.tr(), helperText: 'fieldUsernameCannotEditHint'.tr(), ), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), TextField( controller: _nicknameController, - decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldNickname'.tr()), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + decoration: InputDecoration( + border: const UnderlineInputBorder(), + labelText: 'fieldNickname'.tr()), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), Row( children: [ @@ -311,7 +330,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { border: const UnderlineInputBorder(), labelText: 'fieldFirstName'.tr(), ), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), ), const Gap(8), @@ -323,7 +343,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { border: const UnderlineInputBorder(), labelText: 'fieldLastName'.tr(), ), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), ), ], @@ -338,7 +359,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { border: const UnderlineInputBorder(), labelText: 'fieldGender'.tr(), ), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), ), const Gap(4), @@ -350,7 +372,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { border: const UnderlineInputBorder(), labelText: 'fieldPronouns'.tr(), ), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), ), ], @@ -360,8 +383,11 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { keyboardType: TextInputType.multiline, maxLines: null, minLines: 3, - decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldDescription'.tr()), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + decoration: InputDecoration( + border: const UnderlineInputBorder(), + labelText: 'fieldDescription'.tr()), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -373,18 +399,21 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { border: const UnderlineInputBorder(), labelText: 'fieldTimeZone'.tr(), ), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), ), const Gap(4), StyledWidget( IconButton( icon: const Icon(Symbols.calendar_month), - visualDensity: VisualDensity(horizontal: -4, vertical: -4), + visualDensity: + VisualDensity(horizontal: -4, vertical: -4), padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: () async { - _timezoneController.text = await FlutterTimezone.getLocalTimezone(); + _timezoneController.text = + await FlutterTimezone.getLocalTimezone(); }, ), ).padding(top: 6), @@ -392,7 +421,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { StyledWidget( IconButton( icon: const Icon(Symbols.clear), - visualDensity: VisualDensity(horizontal: -4, vertical: -4), + visualDensity: + VisualDensity(horizontal: -4, vertical: -4), padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: () { @@ -404,13 +434,18 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { ), TextField( controller: _locationController, - decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldLocation'.tr()), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + decoration: InputDecoration( + border: const UnderlineInputBorder(), + labelText: 'fieldLocation'.tr()), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), TextField( controller: _birthdayController, readOnly: true, - decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldBirthday'.tr()), + decoration: InputDecoration( + border: const UnderlineInputBorder(), + labelText: 'fieldBirthday'.tr()), onTap: () => _selectBirthday(), ), if (_links != null) @@ -418,7 +453,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { margin: const EdgeInsets.only(top: 16, bottom: 4), child: Container( width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -427,13 +463,17 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { Expanded( child: Text( 'fieldLinks'.tr(), - style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 17), + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontSize: 17), ), ), IconButton( padding: EdgeInsets.zero, constraints: const BoxConstraints(), - visualDensity: VisualDensity(horizontal: -4, vertical: -4), + visualDensity: + VisualDensity(horizontal: -4, vertical: -4), icon: const Icon(Symbols.add), onPressed: () { setState(() => _links!.add(('', ''))); @@ -457,7 +497,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { onChanged: (value) { _links![idx] = (value, _links![idx].$2); }, - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => FocusManager + .instance.primaryFocus + ?.unfocus(), ), ), const Gap(8), @@ -473,7 +515,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { onChanged: (value) { _links![idx] = (_links![idx].$1, value); }, - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => FocusManager + .instance.primaryFocus + ?.unfocus(), ), ), ], diff --git a/lib/screens/account/programs.dart b/lib/screens/account/programs.dart index 74f6c8b..92bb4b7 100644 --- a/lib/screens/account/programs.dart +++ b/lib/screens/account/programs.dart @@ -70,6 +70,7 @@ class _AccountProgramScreenState extends State<AccountProgramScreen> { @override Widget build(BuildContext context) { return AppScaffold( + noBackground: true, appBar: AppBar( title: Text('accountProgram').tr(), ), diff --git a/lib/screens/account/publishers/publisher_edit.dart b/lib/screens/account/publishers/publisher_edit.dart index a388284..804b321 100644 --- a/lib/screens/account/publishers/publisher_edit.dart +++ b/lib/screens/account/publishers/publisher_edit.dart @@ -27,10 +27,12 @@ class AccountPublisherEditScreen extends StatefulWidget { const AccountPublisherEditScreen({super.key, required this.name}); @override - State<AccountPublisherEditScreen> createState() => _AccountPublisherEditScreenState(); + State<AccountPublisherEditScreen> createState() => + _AccountPublisherEditScreenState(); } -class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> { +class _AccountPublisherEditScreenState + extends State<AccountPublisherEditScreen> { bool _isBusy = false; SnPublisher? _publisher; @@ -115,29 +117,32 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> Uint8List? rawBytes; if (!skipCrop) { - final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); - final aspectRatios = - place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)]; - final result = - (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) - ? await showCupertinoImageCropper( - // ignore: use_build_context_synchronously - context, - allowedAspectRatios: aspectRatios, - imageProvider: imageProvider, - ) - : await showMaterialImageCropper( - // ignore: use_build_context_synchronously - context, - allowedAspectRatios: aspectRatios, - imageProvider: imageProvider, - ); + final ImageProvider imageProvider = + kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); + final aspectRatios = place == 'banner' + ? [CropAspectRatio(width: 16, height: 7)] + : [CropAspectRatio(width: 1, height: 1)]; + final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) + ? await showCupertinoImageCropper( + // ignore: use_build_context_synchronously + context, + allowedAspectRatios: aspectRatios, + imageProvider: imageProvider, + ) + : await showMaterialImageCropper( + // ignore: use_build_context_synchronously + context, + allowedAspectRatios: aspectRatios, + imageProvider: imageProvider, + ); if (result == null) return; if (!mounted) return; setState(() => _isBusy = true); - rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List(); + rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))! + .buffer + .asUint8List(); } else { if (!mounted) return; setState(() => _isBusy = true); @@ -191,7 +196,10 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> final sn = context.read<SnNetworkProvider>(); return AppScaffold( - appBar: AppBar(leading: PageBackButton(), title: Text('screenAccountPublisherEdit').tr()), + noBackground: true, + appBar: AppBar( + leading: PageBackButton(), + title: Text('screenAccountPublisherEdit').tr()), body: SingleChildScrollView( child: Column( children: [ @@ -208,11 +216,14 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> child: AspectRatio( aspectRatio: 16 / 9, child: Container( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - child: - _banner != null - ? AutoResizeUniversalImage(sn.getAttachmentUrl(_banner!), fit: BoxFit.cover) - : const SizedBox.shrink(), + color: Theme.of(context) + .colorScheme + .surfaceContainerHigh, + child: _banner != null + ? AutoResizeUniversalImage( + sn.getAttachmentUrl(_banner!), + fit: BoxFit.cover) + : const SizedBox.shrink(), ), ), ), @@ -245,13 +256,15 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> labelText: 'fieldUsername'.tr(), helperText: 'fieldUsernameCannotEditHint'.tr(), ), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), const Gap(4), TextField( controller: _nickController, decoration: InputDecoration(labelText: 'fieldNickname'.tr()), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), const Gap(4), TextField( @@ -259,7 +272,8 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> maxLines: null, minLines: 3, decoration: InputDecoration(labelText: 'fieldDescription'.tr()), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), const Gap(12), Row( diff --git a/lib/screens/account/publishers/publisher_new.dart b/lib/screens/account/publishers/publisher_new.dart index 9a59eec..d0f6758 100644 --- a/lib/screens/account/publishers/publisher_new.dart +++ b/lib/screens/account/publishers/publisher_new.dart @@ -25,7 +25,8 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> { @override Widget build(BuildContext context) { - return AppScaffold( + return AppScaffold( + noBackground: true, appBar: AppBar( leading: const PageBackButton(), title: Text('screenAccountPublisherNew').tr(), diff --git a/lib/screens/account/publishers/publishers.dart b/lib/screens/account/publishers/publishers.dart index d9d2c6a..5d5d902 100644 --- a/lib/screens/account/publishers/publishers.dart +++ b/lib/screens/account/publishers/publishers.dart @@ -33,7 +33,8 @@ class _PublisherScreenState extends State<PublisherScreen> { try { final resp = await sn.client.get('/cgi/co/publishers/me'); - final List<SnPublisher> out = List<SnPublisher>.from(resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []); + final List<SnPublisher> out = List<SnPublisher>.from( + resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []); if (!mounted) return; @@ -81,6 +82,7 @@ class _PublisherScreenState extends State<PublisherScreen> { @override Widget build(BuildContext context) { return AppScaffold( + noBackground: true, appBar: AppBar( leading: const PageBackButton(), title: Text('screenAccountPublishers').tr(), @@ -93,7 +95,9 @@ class _PublisherScreenState extends State<PublisherScreen> { contentPadding: const EdgeInsets.symmetric(horizontal: 24), leading: const Icon(Symbols.add_circle), onTap: () { - GoRouter.of(context).pushNamed('accountPublisherNew').then((value) { + GoRouter.of(context) + .pushNamed('accountPublisherNew') + .then((value) { if (value == true) { _publishers.clear(); _fetchPublishers(); @@ -119,7 +123,8 @@ class _PublisherScreenState extends State<PublisherScreen> { return ListTile( title: Text(publisher.nick), subtitle: Text('@${publisher.name}'), - contentPadding: const EdgeInsets.symmetric(horizontal: 16), + contentPadding: + const EdgeInsets.symmetric(horizontal: 16), leading: AccountImage(content: publisher.avatar), trailing: PopupMenuButton( itemBuilder: (BuildContext context) => [ diff --git a/lib/screens/account/punishments.dart b/lib/screens/account/punishments.dart index abd8dce..1b3529a 100644 --- a/lib/screens/account/punishments.dart +++ b/lib/screens/account/punishments.dart @@ -55,6 +55,7 @@ class _PunishmentsScreenState extends State<PunishmentsScreen> { @override Widget build(BuildContext context) { return AppScaffold( + noBackground: true, appBar: AppBar( title: Text('accountPunishments').tr(), leading: PageBackButton(), diff --git a/lib/screens/account/settings.dart b/lib/screens/account/settings.dart index 6e9a842..7727d59 100644 --- a/lib/screens/account/settings.dart +++ b/lib/screens/account/settings.dart @@ -37,6 +37,7 @@ class AccountSettingsScreen extends StatelessWidget { final ua = context.watch<UserProvider>(); return AppScaffold( + noBackground: true, appBar: AppBar( leading: PageBackButton(), title: Text('screenAccountSettings').tr(), diff --git a/lib/screens/chat.dart b/lib/screens/chat.dart index 7068de6..62714f8 100644 --- a/lib/screens/chat.dart +++ b/lib/screens/chat.dart @@ -1,3 +1,5 @@ +import 'package:animations/animations.dart'; +import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; @@ -6,21 +8,22 @@ import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; -import 'package:responsive_framework/responsive_framework.dart'; +import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/channel.dart'; import 'package:surface/providers/sn_network.dart'; +import 'package:surface/providers/sn_realm.dart'; import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/userinfo.dart'; -import 'package:surface/screens/chat/room.dart'; import 'package:surface/types/chat.dart'; +import 'package:surface/types/realm.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_select.dart'; import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; -import 'package:surface/widgets/navigation/app_background.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/unauthorized_hint.dart'; +import 'package:surface/widgets/universal_image.dart'; import 'package:uuid/uuid.dart'; class ChatScreen extends StatefulWidget { @@ -38,6 +41,7 @@ class _ChatScreenState extends State<ChatScreen> { List<SnChannel>? _channels; Map<int, SnChatMessage>? _lastMessages; Map<int, int>? _unreadCounts; + Map<int, int>? _unreadCountsGrouped; Future<void> _fetchWhatsNew() async { final sn = context.read<SnNetworkProvider>(); @@ -45,19 +49,48 @@ class _ChatScreenState extends State<ChatScreen> { if (resp.data == null) return; final List<dynamic> out = resp.data; setState(() { - _unreadCounts = {for (var v in out) v['channel_id']: v['count']}; + _unreadCounts ??= {}; + _unreadCountsGrouped ??= {}; + for (var v in out) { + _unreadCounts![v['channel_id']] = v['count']; + final channel = + _channels?.firstWhereOrNull((ele) => ele.id == v['channel_id']); + if (channel != null) { + if (channel.realmId != null) { + _unreadCountsGrouped![channel.realmId!] ??= 0; + _unreadCountsGrouped![channel.realmId!] = + (_unreadCountsGrouped![channel.realmId!]! + v['count']).toInt(); + } + if (channel.type == 1) { + _unreadCountsGrouped![0] ??= 0; + _unreadCountsGrouped![0] = + (_unreadCountsGrouped![0]! + v['count']).toInt(); + } + } + } }); } - void _refreshChannels({bool noRemote = false}) { + void _refreshChannels({bool withBoost = false, bool noRemote = false}) { + final ct = context.read<ChatChannelProvider>(); final ua = context.read<UserProvider>(); if (!ua.isAuthorized) { setState(() => _isBusy = false); return; } + if (!withBoost) { + if (!noRemote) { + ct.refreshAvailableChannels(); + } + } else { + setState(() { + _channels = ct.availableChannels; + }); + } + final chan = context.read<ChatChannelProvider>(); - chan.fetchChannels(noRemote: noRemote).listen((channels) async { + chan.fetchChannels(noRemote: true).listen((channels) async { final lastMessages = await chan.getLastMessages(channels); _lastMessages = {for (final val in lastMessages) val.channelId: val}; channels.sort((a, b) { @@ -99,6 +132,7 @@ class _ChatScreenState extends State<ChatScreen> { ..onDone(() { if (!mounted) return; setState(() => _isBusy = false); + _fetchWhatsNew(); }); } @@ -130,40 +164,51 @@ class _ChatScreenState extends State<ChatScreen> { } } - SnChannel? _focusChannel; - @override void initState() { super.initState(); - _refreshChannels(); - _fetchWhatsNew(); + _refreshChannels(withBoost: true); } void _onTapChannel(SnChannel channel) { - final doExpand = ResponsiveBreakpoints.of(context).largerOrEqualTo(DESKTOP); - - if (doExpand) { - setState(() => _focusChannel = channel); - return; + setState(() => _unreadCounts?[channel.id] = 0); + if (ResponsiveScaffold.getIsExpand(context)) { + GoRouter.of(context).pushReplacementNamed( + 'chatRoom', + pathParameters: { + 'scope': channel.realm?.alias ?? 'global', + 'alias': channel.alias, + }, + ).then((value) { + if (mounted) { + setState(() => _unreadCounts?[channel.id] = 0); + _refreshChannels(noRemote: true); + } + }); + } else { + GoRouter.of(context).pushNamed( + 'chatRoom', + pathParameters: { + 'scope': channel.realm?.alias ?? 'global', + 'alias': channel.alias, + }, + ).then((value) { + if (mounted) { + setState(() => _unreadCounts?[channel.id] = 0); + _refreshChannels(noRemote: true); + } + }); } - GoRouter.of(context).pushNamed( - 'chatRoom', - pathParameters: { - 'scope': channel.realm?.alias ?? 'global', - 'alias': channel.alias, - }, - ).then((value) { - if (mounted) { - _unreadCounts?[channel.id] = 0; - setState(() => _unreadCounts?[channel.id] = 0); - _refreshChannels(noRemote: true); - } - }); } + SnRealm? _focusedRealm; + bool _isDirect = false; + @override Widget build(BuildContext context) { final ua = context.read<UserProvider>(); + final sn = context.read<SnNetworkProvider>(); + final rel = context.read<SnRealmProvider>(); if (!ua.isAuthorized) { return AppScaffold( @@ -177,10 +222,8 @@ class _ChatScreenState extends State<ChatScreen> { ); } - final doExpand = ResponsiveBreakpoints.of(context).largerOrEqualTo(DESKTOP); - - final chatList = AppScaffold( - noBackground: doExpand, + return AppScaffold( + noBackground: true, appBar: AppBar( leading: AutoAppBarLeading(), title: Text('screenChat').tr(), @@ -248,64 +291,198 @@ class _ChatScreenState extends State<ChatScreen> { body: Column( children: [ LoadingIndicator(isActive: _isBusy), - Expanded( - child: MediaQuery.removePadding( - context: context, - removeTop: true, + if (_channels != null && ResponsiveScaffold.getIsExpand(context)) + Expanded( child: RefreshIndicator( - onRefresh: () => Future.wait([ - Future.sync(() => _refreshChannels()), - _fetchWhatsNew(), - ]), - child: ListView.builder( - itemCount: _channels?.length ?? 0, - itemBuilder: (context, idx) { - final channel = _channels![idx]; - final lastMessage = _lastMessages?[channel.id]; + onRefresh: () => Future.sync(() => _refreshChannels()), + child: Builder(builder: (context) { + final scopeList = ListView( + key: const Key('realm-list-view'), + padding: EdgeInsets.zero, + children: [ + ListTile( + minTileHeight: 48, + leading: + const Icon(Symbols.inbox_text).padding(right: 4), + contentPadding: EdgeInsets.only(left: 24, right: 24), + title: Text('chatDirect').tr(), + trailing: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (_unreadCountsGrouped?[0] != null && + (_unreadCountsGrouped?[0] ?? 0) > 0) + Badge( + label: Text( + _unreadCountsGrouped![0].toString(), + ), + ), + ], + ), + onTap: () { + setState(() => _isDirect = true); + }, + ), + ...rel.availableRealms.map((ele) { + return ListTile( + minTileHeight: 48, + contentPadding: EdgeInsets.only(left: 20, right: 24), + leading: AccountImage( + content: ele.avatar, + radius: 16, + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (_unreadCountsGrouped?[ele.id] != null && + (_unreadCountsGrouped?[ele.id] ?? 0) > 0) + Badge( + label: Text( + _unreadCountsGrouped![ele.id].toString(), + ), + ), + ], + ), + title: Text(ele.name), + onTap: () { + setState(() => _focusedRealm = ele); + }, + ); + }), + ], + ); - return _ChatChannelEntry( - channel: channel, - lastMessage: lastMessage, - unreadCount: _unreadCounts?[channel.id], - onTap: () { - if (doExpand) { - _unreadCounts?[channel.id] = 0; - setState(() => _focusChannel = channel); - return; - } - _onTapChannel(channel); - }, - ); - }, + final directChatList = ListView( + key: Key('direct-chat-list-view'), + padding: EdgeInsets.zero, + children: [ + ListTile( + minTileHeight: 48, + leading: const Icon(Symbols.arrow_left_alt), + contentPadding: EdgeInsets.only(left: 24), + title: Text('back').tr(), + onTap: () { + setState(() => _isDirect = false); + }, + ), + const Divider(height: 1), + ..._channels!.where((ele) => ele.type == 1).map( + (ele) { + return _ChatChannelEntry( + channel: ele, + unreadCount: _unreadCounts?[ele.id], + lastMessage: _lastMessages?[ele.id], + isCompact: true, + onTap: () => _onTapChannel(ele), + ); + }, + ) + ], + ); + + final realmScopedChatList = _focusedRealm == null + ? const SizedBox.shrink() + : ListView( + key: ValueKey(_focusedRealm), + padding: EdgeInsets.zero, + children: [ + if (_focusedRealm!.banner != null) + AspectRatio( + aspectRatio: 16 / 9, + child: AutoResizeUniversalImage( + sn.getAttachmentUrl( + _focusedRealm!.banner!, + ), + fit: BoxFit.cover, + ), + ), + ListTile( + minTileHeight: 48, + tileColor: Theme.of(context) + .colorScheme + .surfaceContainer, + leading: AccountImage( + content: _focusedRealm!.avatar, + radius: 16, + ), + contentPadding: EdgeInsets.only( + left: 20, + right: 16, + ), + trailing: IconButton( + icon: const Icon(Symbols.close), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + visualDensity: VisualDensity.compact, + onPressed: () { + setState(() => _focusedRealm = null); + }, + ), + title: Text(_focusedRealm!.name), + ), + ...(_channels! + .where( + (ele) => ele.realmId == _focusedRealm?.id) + .map( + (ele) { + return _ChatChannelEntry( + channel: ele, + unreadCount: _unreadCounts?[ele.id], + lastMessage: _lastMessages?[ele.id], + onTap: () => _onTapChannel(ele), + isCompact: true, + ); + }, + )) + ], + ); + + return PageTransitionSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (Widget child, + Animation<double> primaryAnimation, + Animation<double> secondaryAnimation) { + return SharedAxisTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + fillColor: Colors.transparent, + transitionType: SharedAxisTransitionType.horizontal, + child: child, + ); + }, + child: (_focusedRealm == null && !_isDirect) + ? scopeList + : _isDirect + ? directChatList + : realmScopedChatList, + ); + }), + ), + ) + else if (_channels != null) + Expanded( + child: RefreshIndicator( + onRefresh: () => Future.sync(() => _refreshChannels()), + child: ListView( + key: const Key('chat-list-view'), + padding: EdgeInsets.zero, + children: [ + ...(_channels!.map((ele) { + return _ChatChannelEntry( + channel: ele, + unreadCount: _unreadCounts?[ele.id], + lastMessage: _lastMessages?[ele.id], + onTap: () => _onTapChannel(ele), + ); + })) + ], ), ), ), - ), ], ), ); - - if (doExpand) { - return AppBackground( - isRoot: true, - child: Row( - children: [ - SizedBox(width: 340, child: chatList), - const VerticalDivider(width: 1), - if (_focusChannel != null) - Expanded( - child: ChatRoomScreen( - key: ValueKey(_focusChannel!.id), - scope: _focusChannel!.realm?.alias ?? 'global', - alias: _focusChannel!.alias, - ), - ), - ], - ), - ); - } - - return chatList; } } @@ -314,11 +491,13 @@ class _ChatChannelEntry extends StatelessWidget { final int? unreadCount; final SnChatMessage? lastMessage; final Function? onTap; + final bool isCompact; const _ChatChannelEntry({ required this.channel, this.unreadCount, this.lastMessage, this.onTap, + this.isCompact = false, }); @override @@ -337,6 +516,34 @@ class _ChatChannelEntry extends StatelessWidget { ? ud.getFromCache(otherMember.accountId)?.nick ?? channel.name : channel.name; + if (isCompact) { + return ListTile( + minTileHeight: 48, + contentPadding: + EdgeInsets.only(left: otherMember != null ? 20 : 24, right: 24), + leading: otherMember != null + ? AccountImage( + content: ud.getFromCache(otherMember.accountId)?.avatar, + radius: 16, + ) + : const Icon(Symbols.tag), + trailing: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (unreadCount != null && (unreadCount ?? 0) > 0) + Badge( + label: Text(unreadCount.toString()), + ), + ], + ), + title: Text(title), + onTap: () { + onTap?.call(); + }, + ); + } + return ListTile( title: Row( children: [ @@ -399,7 +606,7 @@ class _ChatChannelEntry extends StatelessWidget { content: otherMember != null ? ud.getFromCache(otherMember.accountId)?.avatar : channel.realm?.avatar, - fallbackWidget: const Icon(Symbols.chat, size: 20), + fallbackWidget: const Icon(Symbols.tag, size: 20), ), onTap: () => onTap?.call(), ); diff --git a/lib/screens/chat/call_room.dart b/lib/screens/chat/call_room.dart index 5595c87..f774cd9 100644 --- a/lib/screens/chat/call_room.dart +++ b/lib/screens/chat/call_room.dart @@ -37,7 +37,8 @@ class _CallRoomScreenState extends State<CallRoomScreen> { return Stack( children: [ Container( - color: Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75), + color: + Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75), child: call.focusTrack != null ? InteractiveParticipantWidget( isFixedAvatar: false, @@ -72,7 +73,8 @@ class _CallRoomScreenState extends State<CallRoomScreen> { color: Theme.of(context).cardColor, participant: track, onTap: () { - if (track.participant.sid != call.focusTrack?.participant.sid) { + if (track.participant.sid != + call.focusTrack?.participant.sid) { call.setFocusTrack(track); } }, @@ -114,10 +116,14 @@ class _CallRoomScreenState extends State<CallRoomScreen> { child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(8)), child: InteractiveParticipantWidget( - color: Theme.of(context).colorScheme.surfaceContainerHigh.withOpacity(0.75), + color: Theme.of(context) + .colorScheme + .surfaceContainerHigh + .withOpacity(0.75), participant: track, onTap: () { - if (track.participant.sid != call.focusTrack?.participant.sid) { + if (track.participant.sid != + call.focusTrack?.participant.sid) { call.setFocusTrack(track); } }, @@ -149,6 +155,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> { listenable: call, builder: (context, _) { return AppScaffold( + noBackground: true, appBar: AppBar( title: RichText( textAlign: TextAlign.center, @@ -183,7 +190,8 @@ class _CallRoomScreenState extends State<CallRoomScreen> { Builder(builder: (context) { final call = context.read<ChatCallProvider>(); final connectionQuality = - call.room.localParticipant?.connectionQuality ?? livekit.ConnectionQuality.unknown; + call.room.localParticipant?.connectionQuality ?? + livekit.ConnectionQuality.unknown; return Expanded( child: Column( mainAxisSize: MainAxisSize.min, @@ -205,24 +213,35 @@ class _CallRoomScreenState extends State<CallRoomScreen> { children: [ Text( { - livekit.ConnectionState.disconnected: 'callStatusDisconnected'.tr(), - livekit.ConnectionState.connected: 'callStatusConnected'.tr(), - livekit.ConnectionState.connecting: 'callStatusConnecting'.tr(), - livekit.ConnectionState.reconnecting: 'callStatusReconnecting'.tr(), + livekit.ConnectionState.disconnected: + 'callStatusDisconnected'.tr(), + livekit.ConnectionState.connected: + 'callStatusConnected'.tr(), + livekit.ConnectionState.connecting: + 'callStatusConnecting'.tr(), + livekit.ConnectionState.reconnecting: + 'callStatusReconnecting'.tr(), }[call.room.connectionState]!, ), const Gap(6), - if (connectionQuality != livekit.ConnectionQuality.unknown) + if (connectionQuality != + livekit.ConnectionQuality.unknown) Icon( { - livekit.ConnectionQuality.excellent: Icons.signal_cellular_alt, - livekit.ConnectionQuality.good: Icons.signal_cellular_alt_2_bar, - livekit.ConnectionQuality.poor: Icons.signal_cellular_alt_1_bar, + livekit.ConnectionQuality.excellent: + Icons.signal_cellular_alt, + livekit.ConnectionQuality.good: + Icons.signal_cellular_alt_2_bar, + livekit.ConnectionQuality.poor: + Icons.signal_cellular_alt_1_bar, }[connectionQuality], color: { - livekit.ConnectionQuality.excellent: Colors.green, - livekit.ConnectionQuality.good: Colors.orange, - livekit.ConnectionQuality.poor: Colors.red, + livekit.ConnectionQuality.excellent: + Colors.green, + livekit.ConnectionQuality.good: + Colors.orange, + livekit.ConnectionQuality.poor: + Colors.red, }[connectionQuality], size: 16, ) @@ -244,7 +263,9 @@ class _CallRoomScreenState extends State<CallRoomScreen> { Row( children: [ IconButton( - icon: _layoutMode == 0 ? const Icon(Icons.view_list) : const Icon(Icons.grid_view), + icon: _layoutMode == 0 + ? const Icon(Icons.view_list) + : const Icon(Icons.grid_view), onPressed: () { _switchLayout(); }, diff --git a/lib/screens/chat/channel_detail.dart b/lib/screens/chat/channel_detail.dart index 1d86144..f1154c3 100644 --- a/lib/screens/chat/channel_detail.dart +++ b/lib/screens/chat/channel_detail.dart @@ -220,6 +220,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id; return AppScaffold( + noBackground: true, appBar: AppBar( title: _channel != null ? Text(_channel!.name) : Text('loading').tr(), ), diff --git a/lib/screens/chat/manage.dart b/lib/screens/chat/manage.dart index 2eceac2..67007d2 100644 --- a/lib/screens/chat/manage.dart +++ b/lib/screens/chat/manage.dart @@ -49,7 +49,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> { resp.data?.map((e) => SnRealm.fromJson(e)) ?? [], ); if (_editingChannel != null) { - _belongToRealm = _realms?.firstWhereOrNull((e) => e.id == _editingChannel!.realmId); + _belongToRealm = + _realms?.firstWhereOrNull((e) => e.id == _editingChannel!.realmId); } } catch (err) { if (mounted) context.showErrorDialog(err); @@ -97,7 +98,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> { 'is_community': _isCommunity, if (_editingChannel != null && _belongToRealm == null) 'new_belongs_realm': 'global' - else if (_editingChannel != null && _belongToRealm?.id != _editingChannel?.realm?.id) + else if (_editingChannel != null && + _belongToRealm?.id != _editingChannel?.realm?.id) 'new_belongs_realm': _belongToRealm!.alias, }; @@ -139,8 +141,11 @@ class _ChatManageScreenState extends State<ChatManageScreen> { @override Widget build(BuildContext context) { return AppScaffold( + noBackground: true, appBar: AppBar( - title: widget.editingChannelAlias != null ? Text('screenChatManage').tr() : Text('screenChatNew').tr(), + title: widget.editingChannelAlias != null + ? Text('screenChatManage').tr() + : Text('screenChatNew').tr(), ), body: SingleChildScrollView( child: Column( @@ -152,7 +157,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> { leadingPadding: const EdgeInsets.only(left: 10, right: 20), dividerColor: Colors.transparent, content: Text( - 'channelEditingNotice'.tr(args: ['#${_editingChannel!.alias}']), + 'channelEditingNotice' + .tr(args: ['#${_editingChannel!.alias}']), ), actions: [ TextButton( @@ -192,12 +198,15 @@ class _ChatManageScreenState extends State<ChatManageScreen> { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(item.name).textStyle(Theme.of(context).textTheme.bodyMedium!), + Text(item.name).textStyle(Theme.of(context) + .textTheme + .bodyMedium!), Text( item.description, maxLines: 1, overflow: TextOverflow.ellipsis, - ).textStyle(Theme.of(context).textTheme.bodySmall!), + ).textStyle( + Theme.of(context).textTheme.bodySmall!), ], ), ), @@ -213,7 +222,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> { CircleAvatar( radius: 16, backgroundColor: Colors.transparent, - foregroundColor: Theme.of(context).colorScheme.onSurface, + foregroundColor: + Theme.of(context).colorScheme.onSurface, child: const Icon(Symbols.clear), ), const Gap(12), @@ -222,7 +232,9 @@ class _ChatManageScreenState extends State<ChatManageScreen> { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('fieldChatBelongToRealmUnset').tr().textStyle( + Text('fieldChatBelongToRealmUnset') + .tr() + .textStyle( Theme.of(context).textTheme.bodyMedium!, ), ], @@ -257,7 +269,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> { helperText: 'fieldChatAliasHint'.tr(), helperMaxLines: 2, ), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), const Gap(4), TextField( @@ -266,7 +279,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> { border: const UnderlineInputBorder(), labelText: 'fieldChatName'.tr(), ), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), const Gap(4), TextField( @@ -277,7 +291,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> { border: const UnderlineInputBorder(), labelText: 'fieldChatDescription'.tr(), ), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), const Gap(12), CheckboxListTile( diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index 22950cb..efd3dab 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -304,6 +304,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> { final ud = context.read<UserDirectoryProvider>(); return AppScaffold( + noBackground: true, appBar: AppBar( title: Text( _channel?.type == 1 diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index f08119e..6b7f086 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -157,6 +157,7 @@ class _ExploreScreenState extends State<ExploreScreen> Widget build(BuildContext context) { final cfg = context.watch<ConfigProvider>(); return AppScaffold( + noBackground: true, floatingActionButtonLocation: ExpandableFab.location, floatingActionButton: ExpandableFab( key: _fabKey, @@ -243,6 +244,8 @@ class _ExploreScreenState extends State<ExploreScreen> GoRouter.of(context).pushNamed('postShuffle'); }, ), + if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) + const Gap(48), Expanded( child: Center( child: IconButton( @@ -534,6 +537,7 @@ class _PostListWidgetState extends State<_PostListWidget> { switch (ele.type) { case 'interactive.post': return OpenablePostItem( + useReplace: true, data: SnPost.fromJson(ele.data), maxWidth: 640, onChanged: (data) { diff --git a/lib/screens/post/post_detail.dart b/lib/screens/post/post_detail.dart index fa2d49f..2514058 100644 --- a/lib/screens/post/post_detail.dart +++ b/lib/screens/post/post_detail.dart @@ -12,7 +12,6 @@ import 'package:surface/providers/userinfo.dart'; import 'package:surface/types/post.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; -import 'package:surface/widgets/navigation/app_background.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/post/post_comment_list.dart'; import 'package:surface/widgets/post/post_item.dart'; @@ -66,115 +65,111 @@ class _PostDetailScreenState extends State<PostDetailScreen> { final double maxWidth = _data?.type == 'video' ? double.infinity : 640; - return AppBackground( - isRoot: widget.onBack != null, - child: AppScaffold( - appBar: AppBar( - leading: BackButton( - onPressed: () { - if (widget.onBack != null) { - widget.onBack!.call(); - } - if (GoRouter.of(context).canPop()) { - GoRouter.of(context).pop(context); - return; - } - GoRouter.of(context).replaceNamed('explore'); - }, - ), - title: _data?.body['title'] != null - ? RichText( - textAlign: TextAlign.center, - text: TextSpan(children: [ - TextSpan( - text: _data?.body['title'] ?? 'postNoun'.tr(), - style: Theme.of(context).textTheme.titleLarge!.copyWith( - color: - Theme.of(context).appBarTheme.foregroundColor!, - ), - ), - const TextSpan(text: '\n'), - TextSpan( - text: 'postDetail'.tr(), - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: - Theme.of(context).appBarTheme.foregroundColor!, - ), - ), - ]), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ) - : Text('postDetail').tr(), + return AppScaffold( + noBackground: true, + appBar: AppBar( + leading: BackButton( + onPressed: () { + if (widget.onBack != null) { + widget.onBack!.call(); + } + if (GoRouter.of(context).canPop()) { + GoRouter.of(context).pop(context); + return; + } + GoRouter.of(context).replaceNamed('explore'); + }, ), - body: CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: LoadingIndicator(isActive: _isBusy), - ), - if (_data != null) - SliverToBoxAdapter( - child: PostItem( - data: _data!, - maxWidth: maxWidth, - showComments: false, - showFullPost: true, - onChanged: (data) { - setState(() => _data = data); - }, - onDeleted: () { - Navigator.pop(context); - }, - ), - ), - if (_data != null) - SliverToBoxAdapter( - child: Divider(height: 1).padding(top: 8), - ), - if (_data != null) - SliverToBoxAdapter( - child: Container( - constraints: BoxConstraints(maxWidth: maxWidth), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon(Symbols.comment, size: 24), - const Gap(16), - Text('postCommentsDetailed') - .plural(_data!.metric.replyCount) - .textStyle(Theme.of(context).textTheme.titleLarge!), - ], - ).padding(horizontal: 20, vertical: 12).center(), - ), - ), - if (_data != null && ua.isAuthorized) - SliverToBoxAdapter( - child: PostCommentQuickAction( - parentPost: _data!, - maxWidth: maxWidth, - onPosted: () { - setState(() { - _data = _data!.copyWith( - metric: _data!.metric.copyWith( - replyCount: _data!.metric.replyCount + 1, + title: _data?.body['title'] != null + ? RichText( + textAlign: TextAlign.center, + text: TextSpan(children: [ + TextSpan( + text: _data?.body['title'] ?? 'postNoun'.tr(), + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Theme.of(context).appBarTheme.foregroundColor!, ), - ); - }); - _childListKey.currentState!.refresh(); - }, - ), + ), + const TextSpan(text: '\n'), + TextSpan( + text: 'postDetail'.tr(), + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: Theme.of(context).appBarTheme.foregroundColor!, + ), + ), + ]), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ) + : Text('postDetail').tr(), + ), + body: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: LoadingIndicator(isActive: _isBusy), + ), + if (_data != null) + SliverToBoxAdapter( + child: PostItem( + data: _data!, + maxWidth: maxWidth, + showComments: false, + showFullPost: true, + onChanged: (data) { + setState(() => _data = data); + }, + onDeleted: () { + Navigator.pop(context); + }, ), - if (_data != null) SliverGap(8), - if (_data != null) - PostCommentSliverList( - key: _childListKey, + ), + if (_data != null) + SliverToBoxAdapter( + child: Divider(height: 1).padding(top: 8), + ), + if (_data != null) + SliverToBoxAdapter( + child: Container( + constraints: BoxConstraints(maxWidth: maxWidth), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Symbols.comment, size: 24), + const Gap(16), + Text('postCommentsDetailed') + .plural(_data!.metric.replyCount) + .textStyle(Theme.of(context).textTheme.titleLarge!), + ], + ).padding(horizontal: 20, vertical: 12).center(), + ), + ), + if (_data != null && ua.isAuthorized) + SliverToBoxAdapter( + child: PostCommentQuickAction( parentPost: _data!, maxWidth: maxWidth, + onPosted: () { + setState(() { + _data = _data!.copyWith( + metric: _data!.metric.copyWith( + replyCount: _data!.metric.replyCount + 1, + ), + ); + }); + _childListKey.currentState!.refresh(); + }, ), - if (_data != null) - SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)), - ], - ), + ), + if (_data != null) SliverGap(8), + if (_data != null) + PostCommentSliverList( + key: _childListKey, + parentPost: _data!, + maxWidth: maxWidth, + ), + if (_data != null) + SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)), + ], ), ); } diff --git a/lib/screens/post/publisher_page.dart b/lib/screens/post/publisher_page.dart index fdea667..0b8cb06 100644 --- a/lib/screens/post/publisher_page.dart +++ b/lib/screens/post/publisher_page.dart @@ -286,6 +286,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> final sn = context.read<SnNetworkProvider>(); return AppScaffold( + noBackground: true, body: NestedScrollView( controller: _scrollController, headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 0610eb9..5e379a0 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -325,20 +325,6 @@ class _SettingsScreenState extends State<SettingsScreen> { setState(() {}); }, ), - CheckboxListTile( - secondary: const Icon(Symbols.left_panel_close), - title: Text('settingsDrawerPreferCollapse').tr(), - subtitle: - Text('settingsDrawerPreferCollapseDescription').tr(), - contentPadding: const EdgeInsets.only(left: 24, right: 17), - value: _prefs.getBool(kAppDrawerPreferCollapse) ?? false, - onChanged: (value) { - _prefs.setBool(kAppDrawerPreferCollapse, value ?? false); - final cfg = context.read<ConfigProvider>(); - cfg.calcDrawerSize(context); - setState(() {}); - }, - ), CheckboxListTile( secondary: const Icon(Symbols.hide), title: Text('settingsHideBottomNav').tr(), diff --git a/lib/screens/wallet.dart b/lib/screens/wallet.dart index 799d8ee..dd49a3a 100644 --- a/lib/screens/wallet.dart +++ b/lib/screens/wallet.dart @@ -45,7 +45,9 @@ class _WalletScreenState extends State<WalletScreen> { @override Widget build(BuildContext context) { return AppScaffold( - appBar: AppBar(leading: PageBackButton(), title: Text('screenAccountWallet').tr()), + noBackground: true, + appBar: AppBar( + leading: PageBackButton(), title: Text('screenAccountWallet').tr()), body: Column( children: [ LoadingIndicator(isActive: _isBusy), @@ -66,7 +68,9 @@ class _WalletScreenState extends State<WalletScreen> { SizedBox(width: double.infinity), Text( NumberFormat.compactCurrency( - locale: EasyLocalization.of(context)!.currentLocale.toString(), + locale: EasyLocalization.of(context)! + .currentLocale + .toString(), symbol: '${'walletCurrencyShort'.tr()} ', decimalDigits: 2, ).format(double.parse(_wallet!.balance)), @@ -76,17 +80,21 @@ class _WalletScreenState extends State<WalletScreen> { const Gap(16), Text( NumberFormat.compactCurrency( - locale: EasyLocalization.of(context)!.currentLocale.toString(), + locale: EasyLocalization.of(context)! + .currentLocale + .toString(), symbol: '${'walletCurrencyGoldenShort'.tr()} ', decimalDigits: 2, ).format(double.parse(_wallet!.goldenBalance)), style: Theme.of(context).textTheme.titleLarge, ), - Text('walletCurrencyGolden'.plural(double.parse(_wallet!.goldenBalance))), + Text('walletCurrencyGolden' + .plural(double.parse(_wallet!.goldenBalance))), ], ).padding(horizontal: 20, vertical: 24), ).padding(horizontal: 8, top: 16, bottom: 4), - if (_wallet != null) Expanded(child: _WalletTransactionList(myself: _wallet!)), + if (_wallet != null) + Expanded(child: _WalletTransactionList(myself: _wallet!)), ], ), ); @@ -116,7 +124,10 @@ class _WalletTransactionListState extends State<_WalletTransactionList> { queryParameters: {'take': 10, 'offset': _transactions.length}, ); _totalCount = resp.data['count']; - _transactions.addAll(resp.data['data']?.map((e) => SnTransaction.fromJson(e)).cast<SnTransaction>() ?? []); + _transactions.addAll(resp.data['data'] + ?.map((e) => SnTransaction.fromJson(e)) + .cast<SnTransaction>() ?? + []); } catch (err) { if (!mounted) return; context.showErrorDialog(err); @@ -141,7 +152,8 @@ class _WalletTransactionListState extends State<_WalletTransactionList> { child: InfiniteList( itemCount: _transactions.length, isLoading: _isBusy, - hasReachedMax: _totalCount != null && _transactions.length >= _totalCount!, + hasReachedMax: + _totalCount != null && _transactions.length >= _totalCount!, onFetchData: () { _fetchTransactions(); }, @@ -149,7 +161,9 @@ class _WalletTransactionListState extends State<_WalletTransactionList> { final ele = _transactions[idx]; final isIncoming = ele.payeeId == widget.myself.id; return ListTile( - leading: isIncoming ? const Icon(Symbols.call_received) : const Icon(Symbols.call_made), + leading: isIncoming + ? const Icon(Symbols.call_received) + : const Icon(Symbols.call_made), title: Text( '${isIncoming ? '+' : '-'}${ele.amount} ${'walletCurrencyShort'.tr()}', style: TextStyle(color: isIncoming ? Colors.green : Colors.red), @@ -162,12 +176,20 @@ class _WalletTransactionListState extends State<_WalletTransactionList> { Row( children: [ Text( - 'walletTransactionType${ele.currency.capitalize()}'.tr(), + 'walletTransactionType${ele.currency.capitalize()}' + .tr(), style: Theme.of(context).textTheme.labelSmall, ), - Text(' · ').textStyle(Theme.of(context).textTheme.labelSmall!).padding(right: 4), + Text(' · ') + .textStyle(Theme.of(context).textTheme.labelSmall!) + .padding(right: 4), Text( - DateFormat(null, EasyLocalization.of(context)!.currentLocale.toString()).format(ele.createdAt), + DateFormat( + null, + EasyLocalization.of(context)! + .currentLocale + .toString()) + .format(ele.createdAt), style: Theme.of(context).textTheme.labelSmall, ), ], @@ -199,33 +221,34 @@ class _CreateWalletWidgetState extends State<_CreateWalletWidget> { final TextEditingController passwordController = TextEditingController(); final password = await showDialog<String?>( context: context, - builder: - (ctx) => AlertDialog( - title: Text('walletCreate').tr(), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text('walletCreatePassword').tr(), - const Gap(8), - TextField( - autofocus: true, - obscureText: true, - controller: passwordController, - decoration: InputDecoration(labelText: 'fieldPassword'.tr()), - ), - ], + builder: (ctx) => AlertDialog( + title: Text('walletCreate').tr(), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text('walletCreatePassword').tr(), + const Gap(8), + TextField( + autofocus: true, + obscureText: true, + controller: passwordController, + decoration: InputDecoration(labelText: 'fieldPassword'.tr()), ), - actions: [ - TextButton(onPressed: () => Navigator.of(ctx).pop(), child: Text('cancel').tr()), - TextButton( - onPressed: () { - Navigator.of(ctx).pop(passwordController.text); - }, - child: Text('next').tr(), - ), - ], + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: Text('cancel').tr()), + TextButton( + onPressed: () { + Navigator.of(ctx).pop(passwordController.text); + }, + child: Text('next').tr(), ), + ], + ), ); WidgetsBinding.instance.addPostFrameCallback((_) { passwordController.dispose(); @@ -257,12 +280,18 @@ class _CreateWalletWidgetState extends State<_CreateWalletWidget> { children: [ CircleAvatar(radius: 28, child: Icon(Symbols.add, size: 28)), const Gap(12), - Text('walletCreate', style: Theme.of(context).textTheme.titleLarge).tr(), - Text('walletCreateSubtitle', style: Theme.of(context).textTheme.bodyMedium).tr(), + Text('walletCreate', + style: Theme.of(context).textTheme.titleLarge) + .tr(), + Text('walletCreateSubtitle', + style: Theme.of(context).textTheme.bodyMedium) + .tr(), const Gap(8), Align( alignment: Alignment.centerRight, - child: TextButton(onPressed: _isBusy ? null : () => _createWallet(), child: Text('next').tr()), + child: TextButton( + onPressed: _isBusy ? null : () => _createWallet(), + child: Text('next').tr()), ), ], ).padding(horizontal: 20, vertical: 24), diff --git a/lib/widgets/connection_indicator.dart b/lib/widgets/connection_indicator.dart index d411636..4f56b4e 100644 --- a/lib/widgets/connection_indicator.dart +++ b/lib/widgets/connection_indicator.dart @@ -16,12 +16,7 @@ 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 : 80.0; return ListenableBuilder( listenable: ws, @@ -35,41 +30,52 @@ class ConnectionIndicator extends StatelessWidget { child: GestureDetector( child: Material( 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, - 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), + child: ua.isAuthorized + ? Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (ws.isBusy) + Text( + 'serverConnecting', + ).tr().textColor(Theme.of(context) + .colorScheme + .onSecondaryContainer) + else if (!ws.isConnected) + Text( + 'serverDisconnected', + ).tr().textColor(Theme.of(context) + .colorScheme + .onSecondaryContainer) + else + Text( + 'serverConnected', + ).tr().textColor(Theme.of(context) + .colorScheme + .onSecondaryContainer), + const Gap(8), + if (ws.isBusy) + const CircularProgressIndicator( + strokeWidth: 2.5, + padding: EdgeInsets.zero, + ) + .width(12) + .height(12) + .padding(horizontal: 4, right: 4) + else if (!ws.isConnected) + const Icon(Symbols.power_off, size: 18) + else + const Icon(Symbols.power, size: 18), + ], + ).padding(horizontal: 8, vertical: 4) + : const SizedBox.shrink(), + ) + .opacity(show ? 1 : 0, animate: true) + .animate(const Duration(milliseconds: 300), Curves.easeInOut), onTap: () { if (!ws.isConnected && !ws.isBusy) { ws.connect(); diff --git a/lib/widgets/context_menu.dart b/lib/widgets/context_menu.dart index 940f6da..ce77857 100644 --- a/lib/widgets/context_menu.dart +++ b/lib/widgets/context_menu.dart @@ -26,9 +26,7 @@ class ContextMenuArea extends StatelessWidget { final cfg = context.read<ConfigProvider>(); if (!cfg.drawerIsCollapsed) { // Leave padding for side navigation - mousePosition = cfg.drawerIsExpanded - ? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2) - : mousePosition.copyWith(dx: mousePosition.dx - 80 * 2); + mousePosition = mousePosition.copyWith(dx: mousePosition.dx - 80 * 2); } }, child: GestureDetector( @@ -40,7 +38,8 @@ class ContextMenuArea extends StatelessWidget { } void _showMenu(BuildContext context, Offset mousePosition) async { - final menu = contextMenu.copyWith(position: contextMenu.position ?? mousePosition); + final menu = + contextMenu.copyWith(position: contextMenu.position ?? mousePosition); final value = await showContextMenu(context, contextMenu: menu); onItemSelected?.call(value); } diff --git a/lib/widgets/navigation/app_drawer_navigation.dart b/lib/widgets/navigation/app_drawer_navigation.dart index a008852..679ca37 100644 --- a/lib/widgets/navigation/app_drawer_navigation.dart +++ b/lib/widgets/navigation/app_drawer_navigation.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:animations/animations.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -11,14 +10,9 @@ import 'package:go_router/go_router.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/navigation.dart'; -import 'package:surface/providers/sn_network.dart'; -import 'package:surface/providers/sn_realm.dart'; import 'package:surface/providers/userinfo.dart'; import 'package:surface/widgets/account/account_image.dart'; -import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/version_label.dart'; class AppNavigationDrawer extends StatefulWidget { @@ -45,27 +39,18 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> { Widget build(BuildContext context) { final ua = context.read<UserProvider>(); final nav = context.watch<NavigationProvider>(); - final cfg = context.watch<ConfigProvider>(); - - final backgroundColor = cfg.drawerIsExpanded ? Colors.transparent : null; return ListenableBuilder( listenable: nav, builder: (context, _) { return Drawer( elevation: widget.elevation, - backgroundColor: backgroundColor, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(0))), child: Column( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!kIsWeb && - (Platform.isWindows || - Platform.isLinux || - Platform.isMacOS) && - !cfg.drawerIsExpanded) + (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) Container( decoration: BoxDecoration( border: Border( @@ -78,42 +63,36 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> { child: WindowTitleBarBox(), ), Gap(MediaQuery.of(context).padding.top), - Expanded( - child: _DrawerContentList(), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Solar Network').bold(), + AppVersionLabel(), + ], + ).padding( + horizontal: 32, + vertical: 12, + ), + Expanded( + child: ListView( + padding: EdgeInsets.zero, + children: [ + ...nav.destinations.mapIndexed((idx, ele) { + return ListTile( + leading: ele.icon, + title: Text(ele.label).tr(), + contentPadding: EdgeInsets.symmetric(horizontal: 24), + selected: nav.currentIndex == idx, + onTap: () { + GoRouter.of(context).pushNamed(ele.screen); + nav.setIndex(idx); + }, + ); + }) + ], + ), ), - Row( - spacing: 8, - children: - nav.destinations.where((ele) => ele.isPinned).mapIndexed( - (idx, ele) { - return Expanded( - child: Tooltip( - message: ele.label.tr(), - child: IconButton( - icon: ele.icon, - color: nav.currentIndex == idx - ? Theme.of(context).colorScheme.onPrimaryContainer - : Theme.of(context).colorScheme.onSurface, - style: ButtonStyle( - backgroundColor: WidgetStatePropertyAll( - nav.currentIndex == idx - ? Theme.of(context) - .colorScheme - .primaryContainer - : Colors.transparent, - ), - ), - onPressed: () { - GoRouter.of(context).goNamed(ele.screen); - Scaffold.of(context).closeDrawer(); - nav.setIndex(idx); - }, - ), - ), - ); - }, - ).toList(), - ).padding(horizontal: 16, bottom: 8), Align( alignment: Alignment.bottomCenter, child: ListTile( @@ -167,163 +146,3 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> { ); } } - -class _DrawerContentList extends StatelessWidget { - const _DrawerContentList(); - - @override - Widget build(BuildContext context) { - final ct = context.read<ChatChannelProvider>(); - final sn = context.read<SnNetworkProvider>(); - final nav = context.watch<NavigationProvider>(); - final rel = context.watch<SnRealmProvider>(); - - return PageTransitionSwitcher( - duration: const Duration(milliseconds: 300), - transitionBuilder: (Widget child, Animation<double> primaryAnimation, - Animation<double> secondaryAnimation) { - return SharedAxisTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - fillColor: Colors.transparent, - transitionType: SharedAxisTransitionType.horizontal, - child: child, - ); - }, - child: nav.focusedRealm == null - ? ListView( - key: const Key('realm-list-view'), - padding: EdgeInsets.zero, - children: [ - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Solar Network').bold(), - AppVersionLabel(), - ], - ).padding( - horizontal: 32, - vertical: 12, - ), - ...rel.availableRealms.map((ele) { - return ListTile( - minTileHeight: 48, - contentPadding: EdgeInsets.symmetric(horizontal: 24), - leading: AccountImage( - content: ele.avatar, - radius: 16, - ), - title: Text(ele.name), - onTap: () { - nav.setFocusedRealm(ele); - }, - ); - }), - 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( - key: ValueKey(nav.focusedRealm), - padding: EdgeInsets.zero, - children: [ - if (nav.focusedRealm!.banner != null) - AspectRatio( - aspectRatio: 16 / 9, - child: AutoResizeUniversalImage( - sn.getAttachmentUrl( - nav.focusedRealm!.banner!, - ), - fit: BoxFit.cover, - ), - ), - ListTile( - minTileHeight: 48, - tileColor: Theme.of(context).colorScheme.surfaceContainer, - contentPadding: EdgeInsets.only( - left: 24, - right: 16, - ), - leading: AccountImage( - content: nav.focusedRealm!.avatar, - radius: 16, - ), - trailing: IconButton( - icon: const Icon(Symbols.close), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - visualDensity: VisualDensity.compact, - onPressed: () { - nav.setFocusedRealm(null); - }, - ), - title: Text(nav.focusedRealm!.name), - onTap: () { - GoRouter.of(context).goNamed( - 'realmDetail', - pathParameters: { - 'alias': nav.focusedRealm!.alias, - }, - ); - Scaffold.of(context).closeDrawer(); - }, - ), - ListTile( - minTileHeight: 48, - contentPadding: EdgeInsets.only( - left: 28, - right: 8, - ), - leading: const Icon(Symbols.globe), - title: Text('community').tr(), - onTap: () { - GoRouter.of(context).goNamed( - 'realmCommunity', - pathParameters: { - 'alias': nav.focusedRealm!.alias, - }, - ); - Scaffold.of(context).closeDrawer(); - }, - ), - if (ct.availableChannels - .where((ele) => ele.realmId == nav.focusedRealm?.id) - .isNotEmpty) - const Divider(height: 1), - ...(ct.availableChannels - .where((ele) => ele.realmId == nav.focusedRealm?.id) - .map((ele) { - return ListTile( - minTileHeight: 48, - contentPadding: EdgeInsets.only( - left: 28, - right: 8, - ), - leading: const Icon(Symbols.tag), - title: Text(ele.name), - onTap: () { - GoRouter.of(context).goNamed( - 'chatRoom', - pathParameters: { - 'scope': ele.realm?.alias ?? 'global', - 'alias': ele.alias, - }, - ); - Scaffold.of(context).closeDrawer(); - }, - ); - })) - ], - ), - ); - } -} diff --git a/lib/widgets/navigation/app_rail_navigation.dart b/lib/widgets/navigation/app_rail_navigation.dart index c3c7a55..e11cf51 100644 --- a/lib/widgets/navigation/app_rail_navigation.dart +++ b/lib/widgets/navigation/app_rail_navigation.dart @@ -1,10 +1,12 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; -import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/navigation.dart'; +import 'package:surface/providers/userinfo.dart'; +import 'package:surface/widgets/account/account_image.dart'; class AppRailNavigation extends StatefulWidget { const AppRailNavigation({super.key}); @@ -18,43 +20,59 @@ class _AppRailNavigationState extends State<AppRailNavigation> { void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - context.read<NavigationProvider>().autoDetectIndex(GoRouter.maybeOf(context)); + context + .read<NavigationProvider>() + .autoDetectIndex(GoRouter.maybeOf(context)); }); } @override Widget build(BuildContext context) { + final ua = context.watch<UserProvider>(); final nav = context.watch<NavigationProvider>(); return ListenableBuilder( listenable: nav, builder: (context, _) { - final destinations = nav.destinations.where((ele) => ele.isPinned).toList(); + final destinations = nav.destinations.toList(); return SizedBox( width: 80, child: NavigationRail( - selectedIndex: - nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null, + labelType: NavigationRailLabelType.selected, + backgroundColor: Theme.of(context) + .colorScheme + .surfaceContainerLow + .withOpacity(0.5), + selectedIndex: nav.currentIndex != null && + nav.currentIndex! < nav.destinations.length + ? nav.currentIndex + : null, destinations: [ - ...destinations.where((ele) => ele.isPinned).map((ele) { + ...destinations.map((ele) { return NavigationRailDestination( icon: ele.icon, label: Text(ele.label).tr(), ); }), ], + leading: const Gap(4), trailing: Expanded( child: Align( alignment: Alignment.bottomCenter, - child: StyledWidget( - IconButton( - icon: const Icon(Symbols.menu), - onPressed: () { - Scaffold.of(context).openDrawer(); + child: Padding( + padding: EdgeInsets.only(bottom: 24), + child: GestureDetector( + child: AccountImage( + content: ua.user?.avatar, + fallbackWidget: + ua.isAuthorized ? null : const Icon(Symbols.login), + ), + onTap: () { + GoRouter.of(context).goNamed('account'); }, ), - ).padding(bottom: 16), + ), ), ), onDestinationSelected: (idx) { diff --git a/lib/widgets/navigation/app_scaffold.dart b/lib/widgets/navigation/app_scaffold.dart index 7208f62..4e1a1d3 100644 --- a/lib/widgets/navigation/app_scaffold.dart +++ b/lib/widgets/navigation/app_scaffold.dart @@ -66,7 +66,9 @@ class AppScaffold extends StatelessWidget { return Scaffold( extendBody: true, extendBodyBehindAppBar: true, - backgroundColor: Theme.of(context).scaffoldBackgroundColor, + backgroundColor: noBackground + ? Colors.transparent + : Theme.of(context).scaffoldBackgroundColor, body: SizedBox.expand( child: noBackground ? content @@ -111,7 +113,6 @@ class AppRootScaffold extends StatelessWidget { final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; final isCollapseDrawer = cfg.drawerIsCollapsed; - final isExpandedDrawer = cfg.drawerIsExpanded; final routeName = GoRouter.of(context) .routerDelegate @@ -132,19 +133,7 @@ class AppRootScaffold extends StatelessWidget { ? body : Row( children: [ - Container( - decoration: BoxDecoration( - border: Border( - right: BorderSide( - color: Theme.of(context).dividerColor, - width: 1 / devicePixelRatio, - ), - ), - ), - child: isExpandedDrawer - ? AppNavigationDrawer(elevation: 0) - : AppRailNavigation(), - ), + AppRailNavigation(), Expanded(child: body), ], ); @@ -232,10 +221,72 @@ class AppRootScaffold extends StatelessWidget { ), ], ), - drawer: !isExpandedDrawer ? AppNavigationDrawer() : null, drawerEdgeDragWidth: isPopable ? 0 : null, + drawer: isCollapseDrawer ? const AppNavigationDrawer() : null, bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null, ); } } + +class ResponsiveScaffold extends StatelessWidget { + final Widget aside; + final Widget? child; + final int asideFlex; + final int contentFlex; + const ResponsiveScaffold({ + super.key, + required this.aside, + required this.child, + this.asideFlex = 1, + this.contentFlex = 2, + }); + + static bool getIsExpand(BuildContext context) { + return ResponsiveBreakpoints.of(context).largerOrEqualTo(TABLET); + } + + @override + Widget build(BuildContext context) { + if (getIsExpand(context)) { + return AppBackground( + isRoot: true, + child: Row( + children: [ + Flexible( + flex: asideFlex, + child: aside, + ), + VerticalDivider(width: 1), + if (child != null && child != aside) + Flexible(flex: contentFlex, child: child!) + else + Flexible( + flex: contentFlex, + child: ResponsiveScaffoldLanding(child: null), + ), + ], + ), + ); + } + + return AppBackground(isRoot: true, child: child ?? aside); + } +} + +class ResponsiveScaffoldLanding extends StatelessWidget { + final Widget? child; + const ResponsiveScaffoldLanding({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + if (ResponsiveScaffold.getIsExpand(context) || child == null) { + return AppScaffold( + noBackground: true, + appBar: AppBar(), + body: const SizedBox.shrink(), + ); + } + return child!; + } +} diff --git a/lib/widgets/post/post_comment_list.dart b/lib/widgets/post/post_comment_list.dart index 4e7e23d..c7a981d 100644 --- a/lib/widgets/post/post_comment_list.dart +++ b/lib/widgets/post/post_comment_list.dart @@ -4,7 +4,6 @@ import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; -import 'package:responsive_framework/responsive_framework.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/post.dart'; import 'package:surface/providers/sn_network.dart'; @@ -30,24 +29,14 @@ class PostCommentQuickAction extends StatelessWidget { return Container( height: 240, constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), - margin: ResponsiveBreakpoints.of(context).largerThan(MOBILE) - ? const EdgeInsets.symmetric(vertical: 8) - : EdgeInsets.zero, decoration: BoxDecoration( - borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE) - ? const BorderRadius.all(Radius.circular(8)) - : BorderRadius.zero, - border: ResponsiveBreakpoints.of(context).largerThan(MOBILE) - ? Border.all( - color: Theme.of(context).dividerColor, - width: 1 / devicePixelRatio, - ) - : Border.symmetric( - horizontal: BorderSide( - color: Theme.of(context).dividerColor, - width: 1 / devicePixelRatio, - ), - ), + borderRadius: BorderRadius.zero, + border: Border.symmetric( + horizontal: BorderSide( + color: Theme.of(context).dividerColor, + width: 1 / devicePixelRatio, + ), + ), ), child: PostMiniEditor( postReplyId: parentPost.id, diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index 99a5785..cd08ccf 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'dart:math' as math; -import 'package:animations/animations.dart'; import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:file_saver/file_saver.dart'; @@ -26,7 +25,6 @@ import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/translation.dart'; import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/userinfo.dart'; -import 'package:surface/screens/post/post_detail.dart'; import 'package:surface/types/attachment.dart'; import 'package:surface/types/post.dart'; import 'package:surface/types/reaction.dart'; @@ -53,6 +51,7 @@ class OpenablePostItem extends StatelessWidget { final bool showMenu; final bool showFullPost; final bool showExpandableComments; + final bool useReplace; final double? maxWidth; final Function(SnPost data)? onChanged; final Function()? onDeleted; @@ -66,6 +65,7 @@ class OpenablePostItem extends StatelessWidget { this.showMenu = true, this.showFullPost = false, this.showExpandableComments = false, + this.useReplace = false, this.maxWidth, this.onChanged, this.onDeleted, @@ -74,40 +74,32 @@ class OpenablePostItem extends StatelessWidget { @override Widget build(BuildContext context) { - final cfg = context.read<ConfigProvider>(); - return Container( constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), child: Center( - child: OpenContainer( - closedBuilder: (_, __) => Container( - constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), - child: PostItem( - data: data, - maxWidth: maxWidth, - showComments: showComments, - showFullPost: showFullPost, - showExpandableComments: showExpandableComments, - onChanged: onChanged, - onDeleted: onDeleted, - onSelectAnswer: onSelectAnswer, - ), - ), - openBuilder: (_, close) => PostDetailScreen( - slug: data.id.toString(), - preload: data, - onBack: close, - ), - openColor: Colors.transparent, - openElevation: 0, - transitionType: ContainerTransitionType.fade, - closedElevation: 0, - closedColor: Theme.of(context).colorScheme.surface.withOpacity( - cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0 : 1, - ), - closedShape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(16)), + child: GestureDetector( + child: PostItem( + data: data, + maxWidth: maxWidth, + showComments: showComments, + showFullPost: showFullPost, + showExpandableComments: showExpandableComments, + onChanged: onChanged, + onDeleted: onDeleted, + onSelectAnswer: onSelectAnswer, ), + onTap: () { + if (useReplace) { + GoRouter.of(context) + .pushReplacementNamed('postDetail', pathParameters: { + 'slug': data.id.toString(), + }); + } else { + GoRouter.of(context).pushNamed('postDetail', pathParameters: { + 'slug': data.id.toString(), + }); + } + }, ), ), ); diff --git a/pubspec.yaml b/pubspec.yaml index c01dd53..3112c12 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 2.4.2+84 +version: 2.4.2+85 environment: sdk: ^3.5.4