Compare commits
	
		
			14 Commits
		
	
	
		
			2.2.2+54
			...
			cb4a2598c8
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| cb4a2598c8 | |||
| 950612dc07 | |||
| cbd1eaf1af | |||
| ac41cbd99f | |||
| 9f9c90abc4 | |||
| 87029e3538 | |||
| 127d9adc09 | |||
| c82dc7ad85 | |||
| 36bcff7a7c | |||
| 38201b547a | |||
| ed0334fcda | |||
| fbb486b90b | |||
| 9b34f385d5 | |||
| bb7b731602 | 
| @@ -193,6 +193,13 @@ | ||||
|   "settingsColorSchemeDescription": "Set the application primary color.", | ||||
|   "settingsColorSeed": "Color Seed", | ||||
|   "settingsColorSeedDescription": "Select one of the present color schemes.", | ||||
|   "settingsFeatures": "Features", | ||||
|   "settingsNotifyWithHaptic": "Haptic when Notified", | ||||
|   "settingsNotifyWithHapticDescription": "Vibrate lightly when a new notification appears in the foreground.", | ||||
|   "settingsExpandPostLink": "Expand Post Link", | ||||
|   "settingsExpandPostLinkDescription": "Expand the post link in the post list.", | ||||
|   "settingsExpandChatLink": "Expand Chat Link", | ||||
|   "settingsExpandChatLinkDescription": "Expand the chat link in the chat list.", | ||||
|   "settingsNetwork": "Network", | ||||
|   "settingsNetworkServer": "HyperNet Server", | ||||
|   "settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.", | ||||
|   | ||||
| @@ -191,6 +191,13 @@ | ||||
|   "settingsColorSchemeDescription": "设置应用主题色。", | ||||
|   "settingsColorSeed": "预设色彩主题", | ||||
|   "settingsColorSeedDescription": "选择一个预设色彩主题。", | ||||
|   "settingsFeatures": "功能", | ||||
|   "settingsNotifyWithHaptic": "新通知时振动", | ||||
|   "settingsNotifyWithHapticDescription": "在应用在前台时收到新通知出现时出发轻量的振动。", | ||||
|   "settingsExpandPostLink": "展开帖子链接", | ||||
|   "settingsExpandPostLinkDescription": "在帖子列表中展开显示帖子中的链接。", | ||||
|   "settingsExpandChatLink": "展开聊天链接", | ||||
|   "settingsExpandChatLinkDescription": "在聊天信息中展开显示内容中的链接。", | ||||
|   "settingsNetwork": "网络", | ||||
|   "settingsNetworkServer": "HyperNet 服务器", | ||||
|   "settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。", | ||||
|   | ||||
| @@ -191,6 +191,9 @@ | ||||
|   "settingsColorSchemeDescription": "設置應用主題色。", | ||||
|   "settingsColorSeed": "預設色彩主題", | ||||
|   "settingsColorSeedDescription": "選擇一個預設色彩主題。", | ||||
|   "settingsFeatures": "功能", | ||||
|   "settingsNotifyWithHaptic": "新通知時振動", | ||||
|   "settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。", | ||||
|   "settingsNetwork": "網絡", | ||||
|   "settingsNetworkServer": "HyperNet 服務器", | ||||
|   "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", | ||||
| @@ -213,8 +216,9 @@ | ||||
|   "sensitiveContentCollapsed": "敏感內容已摺疊。", | ||||
|   "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", | ||||
|   "sensitiveContentReveal": "顯示內容", | ||||
|   "serverConnecting": "正在連接服務器…", | ||||
|   "serverDisconnected": "已與服務器斷開連接", | ||||
|   "serverConnecting": "正在連接…", | ||||
|   "serverDisconnected": "已斷開連接", | ||||
|   "serverConnected": "已連接", | ||||
|   "fieldChatAlias": "頻道別名", | ||||
|   "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。", | ||||
|   "fieldChatName": "名稱", | ||||
| @@ -292,6 +296,7 @@ | ||||
|   "addAttachmentFromCameraPhoto": "拍攝照片", | ||||
|   "addAttachmentFromCameraVideo": "拍攝視頻", | ||||
|   "addAttachmentFromRandomId": "通過訪問 ID 鏈接", | ||||
|   "attachmentDetailInfo": "附件詳細信息", | ||||
|   "attachmentPastedImage": "粘貼的圖片", | ||||
|   "attachmentInsertLink": "插入連接", | ||||
|   "attachmentSetAsPostThumbnail": "設置為帖子縮略圖", | ||||
|   | ||||
| @@ -191,6 +191,9 @@ | ||||
|   "settingsColorSchemeDescription": "設置應用主題色。", | ||||
|   "settingsColorSeed": "預設色彩主題", | ||||
|   "settingsColorSeedDescription": "選擇一個預設色彩主題。", | ||||
|   "settingsFeatures": "功能", | ||||
|   "settingsNotifyWithHaptic": "新通知時振動", | ||||
|   "settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。", | ||||
|   "settingsNetwork": "網絡", | ||||
|   "settingsNetworkServer": "HyperNet 服務器", | ||||
|   "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", | ||||
| @@ -213,8 +216,9 @@ | ||||
|   "sensitiveContentCollapsed": "敏感內容已摺疊。", | ||||
|   "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", | ||||
|   "sensitiveContentReveal": "顯示內容", | ||||
|   "serverConnecting": "正在連接服務器…", | ||||
|   "serverDisconnected": "已與服務器斷開連接", | ||||
|   "serverConnecting": "正在連接…", | ||||
|   "serverDisconnected": "已斷開連接", | ||||
|   "serverConnected": "已連接", | ||||
|   "fieldChatAlias": "頻道別名", | ||||
|   "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。", | ||||
|   "fieldChatName": "名稱", | ||||
| @@ -292,6 +296,7 @@ | ||||
|   "addAttachmentFromCameraPhoto": "拍攝照片", | ||||
|   "addAttachmentFromCameraVideo": "拍攝視頻", | ||||
|   "addAttachmentFromRandomId": "通過訪問 ID 鏈接", | ||||
|   "attachmentDetailInfo": "附件詳細信息", | ||||
|   "attachmentPastedImage": "粘貼的圖片", | ||||
|   "attachmentInsertLink": "插入連接", | ||||
|   "attachmentSetAsPostThumbnail": "設置為帖子縮略圖", | ||||
|   | ||||
| @@ -260,7 +260,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> { | ||||
|     try { | ||||
|       final cfg = context.read<ConfigProvider>(); | ||||
|       WidgetsBinding.instance.addPostFrameCallback((_) { | ||||
|         cfg.calcDrawerSize(context); | ||||
|         cfg.calcDrawerSize(context, withMediaQuery: true); | ||||
|       }); | ||||
|       final home = context.read<HomeWidgetProvider>(); | ||||
|       await home.initialize(); | ||||
|   | ||||
| @@ -14,6 +14,9 @@ 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'; | ||||
|  | ||||
| const Map<String, FilterQuality> kImageQualityLevel = { | ||||
|   'settingsImageQualityLowest': FilterQuality.none, | ||||
| @@ -38,14 +41,22 @@ class ConfigProvider extends ChangeNotifier { | ||||
|   bool drawerIsCollapsed = false; | ||||
|   bool drawerIsExpanded = false; | ||||
|  | ||||
|   void calcDrawerSize(BuildContext context) { | ||||
|   void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) { | ||||
|     bool newDrawerIsCollapsed = false; | ||||
|     bool newDrawerIsExpanded = false; | ||||
|     if (withMediaQuery) { | ||||
|       newDrawerIsCollapsed = MediaQuery.of(context).size.width < 450; | ||||
|       newDrawerIsExpanded = MediaQuery.of(context).size.width >= 451; | ||||
|     } else { | ||||
|       final rpb = ResponsiveBreakpoints.of(context); | ||||
|     final newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE); | ||||
|     final newDrawerIsExpanded = rpb.largerThan(TABLET) | ||||
|       newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE); | ||||
|       newDrawerIsCollapsed = rpb.largerThan(TABLET) | ||||
|           ? (prefs.getBool(kAppDrawerPreferCollapse) ?? false) | ||||
|               ? false | ||||
|               : true | ||||
|           : false; | ||||
|     } | ||||
|  | ||||
|     if (newDrawerIsExpanded != drawerIsExpanded || newDrawerIsCollapsed != drawerIsCollapsed) { | ||||
|       drawerIsExpanded = newDrawerIsExpanded; | ||||
|       drawerIsCollapsed = newDrawerIsCollapsed; | ||||
|   | ||||
| @@ -4,8 +4,10 @@ import 'dart:io'; | ||||
| import 'package:firebase_messaging/firebase_messaging.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_udid/flutter_udid.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/providers/websocket.dart'; | ||||
| @@ -15,11 +17,13 @@ class NotificationProvider extends ChangeNotifier { | ||||
|   late final SnNetworkProvider _sn; | ||||
|   late final UserProvider _ua; | ||||
|   late final WebSocketProvider _ws; | ||||
|   late final ConfigProvider _cfg; | ||||
|  | ||||
|   NotificationProvider(BuildContext context) { | ||||
|     _sn = context.read<SnNetworkProvider>(); | ||||
|     _ua = context.read<UserProvider>(); | ||||
|     _ws = context.read<WebSocketProvider>(); | ||||
|     _cfg = context.read<ConfigProvider>(); | ||||
|   } | ||||
|  | ||||
|   Future<void> registerPushNotifications() async { | ||||
| @@ -75,6 +79,8 @@ class NotificationProvider extends ChangeNotifier { | ||||
|         final notification = SnNotification.fromJson(event.payload!); | ||||
|         notifications.add(notification); | ||||
|         notifyListeners(); | ||||
|         final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true; | ||||
|         if (doHaptic) HapticFeedback.lightImpact(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|   | ||||
							
								
								
									
										129
									
								
								lib/router.dart
									
									
									
									
									
								
							
							
						
						
									
										129
									
								
								lib/router.dart
									
									
									
									
									
								
							| @@ -34,32 +34,37 @@ import 'package:surface/widgets/about.dart'; | ||||
| import 'package:surface/widgets/navigation/app_background.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| Widget _fadeThroughTransition( | ||||
|     BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { | ||||
|   return FadeThroughTransition( | ||||
|     animation: animation, | ||||
|     secondaryAnimation: secondaryAnimation, | ||||
|     fillColor: Colors.transparent, | ||||
|     child: child, | ||||
|   ); | ||||
| } | ||||
|  | ||||
| final _appRoutes = [ | ||||
|   ShellRoute( | ||||
|     builder: (context, state, child) => AppPageScaffold( | ||||
|       body: child, | ||||
|       showAppBar: false, | ||||
|     ), | ||||
|     routes: [ | ||||
|   GoRoute( | ||||
|     path: '/', | ||||
|     name: 'home', | ||||
|         pageBuilder: (context, state) => NoTransitionPage( | ||||
|     pageBuilder: (context, state) => CustomTransitionPage( | ||||
|       transitionsBuilder: _fadeThroughTransition, | ||||
|       child: const HomeScreen(), | ||||
|     ), | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/posts', | ||||
|     name: 'explore', | ||||
|         pageBuilder: (context, state) => NoTransitionPage( | ||||
|     pageBuilder: (context, state) => CustomTransitionPage( | ||||
|       transitionsBuilder: _fadeThroughTransition, | ||||
|       child: const ExploreScreen(), | ||||
|     ), | ||||
|     routes: [ | ||||
|       GoRoute( | ||||
|         path: '/write/:mode', | ||||
|         name: 'postEditor', | ||||
|             builder: (context, state) => AppBackground( | ||||
|               child: PostEditorScreen( | ||||
|         builder: (context, state) => PostEditorScreen( | ||||
|           mode: state.pathParameters['mode']!, | ||||
|           postEditId: int.tryParse( | ||||
|             state.uri.queryParameters['editing'] ?? '', | ||||
| @@ -73,48 +78,42 @@ final _appRoutes = [ | ||||
|           extraProps: state.extra as PostEditorExtraProps?, | ||||
|         ), | ||||
|       ), | ||||
|           ), | ||||
|       GoRoute( | ||||
|         path: '/search', | ||||
|         name: 'postSearch', | ||||
|             builder: (context, state) => AppBackground( | ||||
|               child: PostSearchScreen( | ||||
|         builder: (context, state) => PostSearchScreen( | ||||
|           initialTags: state.uri.queryParameters['tags']?.split(','), | ||||
|           initialCategories: state.uri.queryParameters['categories']?.split(','), | ||||
|         ), | ||||
|       ), | ||||
|           ), | ||||
|       GoRoute( | ||||
|         path: '/publishers/:name', | ||||
|         name: 'postPublisher', | ||||
|             builder: (context, state) => AppBackground( | ||||
|               child: PostPublisherScreen(name: state.pathParameters['name']!), | ||||
|             ), | ||||
|         builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/:slug', | ||||
|         name: 'postDetail', | ||||
|             builder: (context, state) => AppBackground( | ||||
|               child: PostDetailScreen( | ||||
|         builder: (context, state) => PostDetailScreen( | ||||
|           slug: state.pathParameters['slug']!, | ||||
|           preload: state.extra as SnPost?, | ||||
|         ), | ||||
|       ), | ||||
|           ), | ||||
|     ], | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/account', | ||||
|     name: 'account', | ||||
|         pageBuilder: (context, state) => NoTransitionPage( | ||||
|     pageBuilder: (context, state) => CustomTransitionPage( | ||||
|       transitionsBuilder: _fadeThroughTransition, | ||||
|       child: const AccountScreen(), | ||||
|     ), | ||||
|         routes: [], | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/chat', | ||||
|     name: 'chat', | ||||
|         pageBuilder: (context, state) => NoTransitionPage( | ||||
|     pageBuilder: (context, state) => CustomTransitionPage( | ||||
|       transitionsBuilder: _fadeThroughTransition, | ||||
|       child: const ChatScreen(), | ||||
|     ), | ||||
|     routes: [ | ||||
| @@ -160,46 +159,34 @@ final _appRoutes = [ | ||||
|               animation: animation, | ||||
|               secondaryAnimation: secondaryAnimation, | ||||
|               fillColor: Colors.transparent, | ||||
|                   child: AppBackground( | ||||
|               child: child, | ||||
|                   ), | ||||
|             ); | ||||
|           }, | ||||
|         ), | ||||
|       ), | ||||
|           GoRoute( | ||||
|             path: '/:alias', | ||||
|             name: 'realmDetail', | ||||
|             builder: (context, state) => AppBackground( | ||||
|               child: RealmDetailScreen(alias: state.pathParameters['alias']!), | ||||
|             ), | ||||
|           ), | ||||
|     ], | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/realm', | ||||
|     name: 'realm', | ||||
|         pageBuilder: (context, state) => NoTransitionPage( | ||||
|     pageBuilder: (context, state) => CustomTransitionPage( | ||||
|       transitionsBuilder: _fadeThroughTransition, | ||||
|       child: const RealmScreen(), | ||||
|     ), | ||||
|     routes: [ | ||||
|       GoRoute( | ||||
|         path: '/:alias', | ||||
|         name: 'realmDetail', | ||||
|         builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/manage', | ||||
|         name: 'realmManage', | ||||
|         pageBuilder: (context, state) => CustomTransitionPage( | ||||
|           transitionsBuilder: _fadeThroughTransition, | ||||
|           child: RealmManageScreen( | ||||
|             editingRealmAlias: state.uri.queryParameters['editing'], | ||||
|           ), | ||||
|               transitionsBuilder: (context, animation, secondaryAnimation, child) { | ||||
|                 return FadeThroughTransition( | ||||
|                   animation: animation, | ||||
|                   secondaryAnimation: secondaryAnimation, | ||||
|                   fillColor: Colors.transparent, | ||||
|                   child: AppBackground( | ||||
|                     child: child, | ||||
|                   ), | ||||
|                 ); | ||||
|               }, | ||||
|         ), | ||||
|       ), | ||||
|     ], | ||||
| @@ -207,7 +194,8 @@ final _appRoutes = [ | ||||
|   GoRoute( | ||||
|     path: '/album', | ||||
|     name: 'album', | ||||
|         pageBuilder: (context, state) => NoTransitionPage( | ||||
|     pageBuilder: (context, state) => CustomTransitionPage( | ||||
|       transitionsBuilder: _fadeThroughTransition, | ||||
|       child: const AlbumScreen(), | ||||
|     ), | ||||
|   ), | ||||
| @@ -225,64 +213,43 @@ final _appRoutes = [ | ||||
|       child: const NotificationScreen(), | ||||
|     ), | ||||
|   ), | ||||
|     ], | ||||
|   ), | ||||
|   ShellRoute( | ||||
|     builder: (context, state, child) => AppPageScaffold(body: child), | ||||
|     routes: [ | ||||
|   GoRoute( | ||||
|     path: '/auth/login', | ||||
|     name: 'authLogin', | ||||
|         builder: (context, state) => const AppBackground( | ||||
|           child: LoginScreen(), | ||||
|         ), | ||||
|     builder: (context, state) => LoginScreen(), | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/auth/register', | ||||
|     name: 'authRegister', | ||||
|         builder: (context, state) => const AppBackground( | ||||
|           child: RegisterScreen(), | ||||
|         ), | ||||
|     builder: (context, state) => RegisterScreen(), | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/reports', | ||||
|     name: 'abuseReport', | ||||
|         builder: (context, state) => const AppBackground( | ||||
|           child: AbuseReportScreen(), | ||||
|         ), | ||||
|     builder: (context, state) => AbuseReportScreen(), | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/account/profile/edit', | ||||
|     name: 'accountProfileEdit', | ||||
|         builder: (context, state) => const AppBackground( | ||||
|           child: ProfileEditScreen(), | ||||
|         ), | ||||
|     builder: (context, state) => ProfileEditScreen(), | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/account/publishers', | ||||
|     name: 'accountPublishers', | ||||
|         builder: (context, state) => const AppBackground( | ||||
|           child: PublisherScreen(), | ||||
|         ), | ||||
|     builder: (context, state) => PublisherScreen(), | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/account/publishers/new', | ||||
|     name: 'accountPublisherNew', | ||||
|         builder: (context, state) => const AppBackground( | ||||
|           child: AccountPublisherNewScreen(), | ||||
|         ), | ||||
|     builder: (context, state) => AccountPublisherNewScreen(), | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/account/publishers/edit/:name', | ||||
|     name: 'accountPublisherEdit', | ||||
|         builder: (context, state) => AppBackground( | ||||
|           child: AccountPublisherEditScreen( | ||||
|     builder: (context, state) => AccountPublisherEditScreen( | ||||
|       name: state.pathParameters['name']!, | ||||
|     ), | ||||
|   ), | ||||
|       ), | ||||
|     ], | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/account/:name', | ||||
|     name: 'accountProfilePage', | ||||
| @@ -290,29 +257,15 @@ final _appRoutes = [ | ||||
|       child: UserScreen(name: state.pathParameters['name']!), | ||||
|     ), | ||||
|   ), | ||||
|   ShellRoute( | ||||
|     builder: (context, state, child) => AppPageScaffold(body: child), | ||||
|     routes: [ | ||||
|   GoRoute( | ||||
|     path: '/settings', | ||||
|     name: 'settings', | ||||
|         builder: (context, state) => const AppBackground( | ||||
|           child: SettingsScreen(), | ||||
|     builder: (context, state) => SettingsScreen(), | ||||
|   ), | ||||
|       ), | ||||
|     ], | ||||
|   ), | ||||
|   ShellRoute( | ||||
|     builder: (context, state, child) => AppPageScaffold(body: child), | ||||
|     routes: [ | ||||
|   GoRoute( | ||||
|     path: '/about', | ||||
|     name: 'about', | ||||
|         builder: (context, state) => const AppBackground( | ||||
|           child: AboutScreen(), | ||||
|         ), | ||||
|       ), | ||||
|     ], | ||||
|     builder: (context, state) => AboutScreen(), | ||||
|   ), | ||||
| ]; | ||||
|  | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| import '../types/account.dart'; | ||||
|  | ||||
| @@ -56,7 +57,11 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: const PageBackButton(), | ||||
|         title: Text('screenAbuseReport').tr(), | ||||
|       ), | ||||
|       body: Column( | ||||
|         children: [ | ||||
|           ListTile( | ||||
| @@ -73,6 +78,7 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> { | ||||
|           else | ||||
|             Expanded( | ||||
|               child: ListView.builder( | ||||
|                 padding: EdgeInsets.only(top: 8), | ||||
|                 itemCount: _reports.length, | ||||
|                 itemBuilder: (context, idx) { | ||||
|                   return ListTile( | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import 'package:surface/providers/websocket.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/app_bar_leading.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| class AccountScreen extends StatelessWidget { | ||||
|   const AccountScreen({super.key}); | ||||
| @@ -20,7 +21,7 @@ class AccountScreen extends StatelessWidget { | ||||
|   Widget build(BuildContext context) { | ||||
|     final ua = context.watch<UserProvider>(); | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: AutoAppBarLeading(), | ||||
|         title: Text("screenAccount").tr(), | ||||
|   | ||||
| @@ -18,6 +18,7 @@ import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
|  | ||||
| class ProfileEditScreen extends StatefulWidget { | ||||
| @@ -81,8 +82,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|             onDateTimeChanged: (DateTime newDate) { | ||||
|               setState(() { | ||||
|                 _birthday = newDate; | ||||
|                 _birthdayController.text = | ||||
|                     DateFormat(_kDateFormat).format(_birthday!); | ||||
|                 _birthdayController.text = DateFormat(_kDateFormat).format(_birthday!); | ||||
|               }); | ||||
|             }, | ||||
|           ), | ||||
| @@ -96,11 +96,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|     if (image == null) return; | ||||
|     if (!mounted) return; | ||||
|  | ||||
|     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 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 | ||||
| @@ -122,10 +120,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|  | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     final rawBytes = | ||||
|         (await result.uiImage.toByteData(format: ImageByteFormat.png))! | ||||
|             .buffer | ||||
|             .asUint8List(); | ||||
|     final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List(); | ||||
|  | ||||
|     try { | ||||
|       final attachment = await attach.directUploadOne( | ||||
| @@ -212,7 +207,12 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|  | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     return SingleChildScrollView( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: const PageBackButton(), | ||||
|         title: Text('screenAccountProfileEdit').tr(), | ||||
|       ), | ||||
|       body: SingleChildScrollView( | ||||
|         child: Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
| @@ -229,8 +229,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|                       child: AspectRatio( | ||||
|                         aspectRatio: 16 / 9, | ||||
|                         child: Container( | ||||
|                         color: | ||||
|                             Theme.of(context).colorScheme.surfaceContainerHigh, | ||||
|                           color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||
|                           child: _banner != null | ||||
|                               ? AutoResizeUniversalImage( | ||||
|                                   sn.getAttachmentUrl(_banner!), | ||||
| @@ -343,6 +342,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|             ).padding(horizontal: padding), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -241,6 +241,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     return Scaffold( | ||||
|       backgroundColor: Colors.transparent, | ||||
|       body: CustomScrollView( | ||||
|         controller: _scrollController, | ||||
|         slivers: [ | ||||
| @@ -594,7 +595,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|                 subtitle: Text('@${ele.name}'), | ||||
|                 trailing: const Icon(Symbols.chevron_right), | ||||
|                 onTap: () { | ||||
|                   GoRouter.of(context).pushNamed( | ||||
|                   GoRouter.of(context).goNamed( | ||||
|                     'postPublisher', | ||||
|                     pathParameters: {'name': ele.name}, | ||||
|                   ); | ||||
|   | ||||
| @@ -18,6 +18,7 @@ import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
|  | ||||
| class AccountPublisherEditScreen extends StatefulWidget { | ||||
| @@ -176,7 +177,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> | ||||
|   Widget build(BuildContext context) { | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       body: SingleChildScrollView( | ||||
|         child: Column( | ||||
|           children: [ | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/types/realm.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| class AccountPublisherNewScreen extends StatefulWidget { | ||||
|   const AccountPublisherNewScreen({super.key}); | ||||
| @@ -24,7 +25,11 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|     return  AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: const PageBackButton(), | ||||
|         title: Text('screenAccountPublisherNew').tr(), | ||||
|       ), | ||||
|       body: SingleChildScrollView( | ||||
|         child: Column( | ||||
|           children: [ | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| class PublisherScreen extends StatefulWidget { | ||||
|   const PublisherScreen({super.key}); | ||||
| @@ -32,8 +33,7 @@ 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; | ||||
|  | ||||
| @@ -53,7 +53,11 @@ class _PublisherScreenState extends State<PublisherScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: const PageBackButton(), | ||||
|         title: Text('screenAccountPublishers').tr(), | ||||
|       ), | ||||
|       body: Column( | ||||
|         children: [ | ||||
|           ListTile( | ||||
| @@ -62,9 +66,7 @@ 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(); | ||||
| @@ -75,6 +77,9 @@ class _PublisherScreenState extends State<PublisherScreen> { | ||||
|           const Divider(height: 1), | ||||
|           LoadingIndicator(isActive: _isBusy), | ||||
|           Expanded( | ||||
|             child: MediaQuery.removePadding( | ||||
|               context: context, | ||||
|               removeTop: true, | ||||
|               child: RefreshIndicator( | ||||
|                 onRefresh: () { | ||||
|                   _publishers.clear(); | ||||
| @@ -120,6 +125,7 @@ class _PublisherScreenState extends State<PublisherScreen> { | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import 'package:surface/widgets/app_bar_leading.dart'; | ||||
| import 'package:surface/widgets/attachment/attachment_zoom.dart'; | ||||
| import 'package:surface/widgets/attachment/attachment_item.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:uuid/uuid.dart'; | ||||
|  | ||||
| class AlbumScreen extends StatefulWidget { | ||||
| @@ -82,7 +83,7 @@ class _AlbumScreenState extends State<AlbumScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       body: CustomScrollView( | ||||
|         controller: _scrollController, | ||||
|         slivers: [ | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/types/auth.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| import '../../providers/websocket.dart'; | ||||
| @@ -35,7 +36,12 @@ class _LoginScreenState extends State<LoginScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Theme( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: const PageBackButton(), | ||||
|         title: Text('screenAuthLogin').tr(), | ||||
|       ), | ||||
|       body: Theme( | ||||
|         data: Theme.of(context).copyWith(canvasColor: Colors.transparent), | ||||
|         child: SingleChildScrollView( | ||||
|           child: PageTransitionSwitcher( | ||||
| @@ -96,6 +102,7 @@ class _LoginScreenState extends State<LoginScreen> { | ||||
|             }, | ||||
|           ).padding(all: 24), | ||||
|         ).center(), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| class RegisterScreen extends StatefulWidget { | ||||
| @@ -54,7 +55,12 @@ class _RegisterScreenState extends State<RegisterScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return StyledWidget(Container( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: const PageBackButton(), | ||||
|         title: Text('screenAuthRegister').tr(), | ||||
|       ), | ||||
|       body: StyledWidget(Container( | ||||
|         constraints: const BoxConstraints(maxWidth: 380), | ||||
|         child: SingleChildScrollView( | ||||
|           child: Column( | ||||
| @@ -180,10 +186,7 @@ class _RegisterScreenState extends State<RegisterScreen> { | ||||
|                           'termAcceptNextWithAgree'.tr(), | ||||
|                           textAlign: TextAlign.end, | ||||
|                           style: Theme.of(context).textTheme.bodySmall!.copyWith( | ||||
|                           color: Theme.of(context) | ||||
|                               .colorScheme | ||||
|                               .onSurface | ||||
|                               .withAlpha((255 * 0.75).round()), | ||||
|                                 color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()), | ||||
|                               ), | ||||
|                         ), | ||||
|                         Material( | ||||
| @@ -223,6 +226,7 @@ class _RegisterScreenState extends State<RegisterScreen> { | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|     )).padding(all: 24).center(); | ||||
|       )).padding(all: 24).center(), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -13,6 +13,7 @@ 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_scaffold.dart'; | ||||
| import 'package:surface/widgets/unauthorized_hint.dart'; | ||||
| import 'package:uuid/uuid.dart'; | ||||
|  | ||||
| @@ -120,7 +121,7 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|     final ua = context.read<UserProvider>(); | ||||
|  | ||||
|     if (!ua.isAuthorized) { | ||||
|       return Scaffold( | ||||
|       return AppScaffold( | ||||
|         appBar: AppBar( | ||||
|           leading: AutoAppBarLeading(), | ||||
|           title: Text('screenChat').tr(), | ||||
| @@ -131,7 +132,7 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: AutoAppBarLeading(), | ||||
|         title: Text('screenChat').tr(), | ||||
| @@ -195,6 +196,9 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|         children: [ | ||||
|           LoadingIndicator(isActive: _isBusy), | ||||
|           Expanded( | ||||
|             child: MediaQuery.removePadding( | ||||
|               context: context, | ||||
|               removeTop: true, | ||||
|               child: RefreshIndicator( | ||||
|                 onRefresh: () => Future.sync(() => _refreshChannels()), | ||||
|                 child: ListView.builder( | ||||
| @@ -276,6 +280,7 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   | ||||
| @@ -9,10 +9,12 @@ import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/chat_call.dart'; | ||||
| import 'package:surface/widgets/chat/call/call_controls.dart'; | ||||
| import 'package:surface/widgets/chat/call/call_participant.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| class CallRoomScreen extends StatefulWidget { | ||||
|   final String scope; | ||||
|   final String alias; | ||||
|  | ||||
|   const CallRoomScreen({super.key, required this.scope, required this.alias}); | ||||
|  | ||||
|   @override | ||||
| @@ -35,8 +37,7 @@ 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, | ||||
| @@ -71,8 +72,7 @@ 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,14 +114,10 @@ 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); | ||||
|                   } | ||||
|                 }, | ||||
| @@ -152,31 +148,24 @@ class _CallRoomScreenState extends State<CallRoomScreen> { | ||||
|     return ListenableBuilder( | ||||
|         listenable: call, | ||||
|         builder: (context, _) { | ||||
|           return Scaffold( | ||||
|           return AppScaffold( | ||||
|             appBar: AppBar( | ||||
|               title: RichText( | ||||
|                 textAlign: TextAlign.center, | ||||
|                 text: TextSpan(children: [ | ||||
|                   TextSpan( | ||||
|                     text: 'call'.tr(), | ||||
|                     style: Theme.of(context) | ||||
|                         .textTheme | ||||
|                         .titleLarge! | ||||
|                         .copyWith(color: Colors.white), | ||||
|                     style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.white), | ||||
|                   ), | ||||
|                   const TextSpan(text: '\n'), | ||||
|                   TextSpan( | ||||
|                     text: call.lastDuration.toString(), | ||||
|                     style: Theme.of(context) | ||||
|                         .textTheme | ||||
|                         .bodySmall! | ||||
|                         .copyWith(color: Colors.white), | ||||
|                     style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.white), | ||||
|                   ), | ||||
|                 ]), | ||||
|               ), | ||||
|             ), | ||||
|             body: SafeArea( | ||||
|               child: GestureDetector( | ||||
|             body: GestureDetector( | ||||
|               behavior: HitTestBehavior.translucent, | ||||
|               child: Column( | ||||
|                 children: [ | ||||
| @@ -190,8 +179,7 @@ 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, | ||||
| @@ -213,35 +201,24 @@ 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, | ||||
|                                       ) | ||||
| @@ -263,9 +240,7 @@ 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(); | ||||
|                               }, | ||||
| @@ -277,8 +252,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> { | ||||
|                   ), | ||||
|                   Expanded( | ||||
|                     child: Material( | ||||
|                         color: | ||||
|                             Theme.of(context).colorScheme.surfaceContainerLow, | ||||
|                       color: Theme.of(context).colorScheme.surfaceContainerLow, | ||||
|                       child: Builder( | ||||
|                         builder: (context) { | ||||
|                           switch (_layoutMode) { | ||||
| @@ -303,7 +277,6 @@ class _CallRoomScreenState extends State<CallRoomScreen> { | ||||
|               ), | ||||
|               onTap: () {}, | ||||
|             ), | ||||
|             ), | ||||
|           ); | ||||
|         }); | ||||
|   } | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import 'package:surface/types/chat.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||
|  | ||||
| class ChannelDetailScreen extends StatefulWidget { | ||||
| @@ -189,7 +190,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | ||||
|  | ||||
|     final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id; | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: _channel != null ? Text(_channel!.name) : Text('loading').tr(), | ||||
|       ), | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import 'package:surface/types/realm.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:uuid/uuid.dart'; | ||||
|  | ||||
| class ChatManageScreen extends StatefulWidget { | ||||
| @@ -121,7 +122,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: widget.editingChannelAlias != null | ||||
|             ? Text('screenChatManage').tr() | ||||
|   | ||||
| @@ -20,6 +20,7 @@ import 'package:surface/widgets/chat/chat_message_input.dart'; | ||||
| import 'package:surface/widgets/chat/chat_typing_indicator.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||
|  | ||||
| import '../../providers/user_directory.dart'; | ||||
| @@ -211,7 +212,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> { | ||||
|     final call = context.watch<ChatCallProvider>(); | ||||
|     final ud = context.read<UserDirectoryProvider>(); | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: Text( | ||||
|           _channel?.type == 1 | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import 'package:surface/screens/post/post_detail.dart'; | ||||
| import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/widgets/app_bar_leading.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/post/post_item.dart'; | ||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||
|  | ||||
| @@ -95,7 +96,7 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       floatingActionButtonLocation: ExpandableFab.location, | ||||
|       floatingActionButton: ExpandableFab( | ||||
|         key: _fabKey, | ||||
| @@ -212,7 +213,7 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|             const SliverGap(8), | ||||
|             const SliverGap(12), | ||||
|             SliverInfiniteList( | ||||
|               itemCount: _posts.length, | ||||
|               isLoading: _isBusy, | ||||
| @@ -242,10 +243,10 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|                     ), | ||||
|                     openColor: Colors.transparent, | ||||
|                     openElevation: 0, | ||||
|                     closedColor: Theme.of(context).colorScheme.surface, | ||||
|                     closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.75), | ||||
|                     transitionType: ContainerTransitionType.fade, | ||||
|                     closedShape: const RoundedRectangleBorder( | ||||
|                       borderRadius: BorderRadius.all(Radius.circular(8)), | ||||
|                       borderRadius: BorderRadius.all(Radius.circular(16)), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ); | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import 'package:surface/widgets/account/account_image.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_scaffold.dart'; | ||||
|  | ||||
| import '../providers/userinfo.dart'; | ||||
| import '../widgets/unauthorized_hint.dart'; | ||||
| @@ -180,7 +181,7 @@ class _FriendScreenState extends State<FriendScreen> { | ||||
|     final ua = context.read<UserProvider>(); | ||||
|  | ||||
|     if (!ua.isAuthorized) { | ||||
|       return Scaffold( | ||||
|       return AppScaffold( | ||||
|         appBar: AppBar( | ||||
|           leading: AutoAppBarLeading(), | ||||
|           title: Text('screenFriend').tr(), | ||||
| @@ -191,7 +192,7 @@ class _FriendScreenState extends State<FriendScreen> { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: AutoAppBarLeading(), | ||||
|         title: Text('screenFriend').tr(), | ||||
| @@ -233,6 +234,9 @@ class _FriendScreenState extends State<FriendScreen> { | ||||
|           if (_requests.isNotEmpty || _blocks.isNotEmpty) | ||||
|             const Divider(height: 1), | ||||
|           Expanded( | ||||
|             child: MediaQuery.removePadding( | ||||
|               context: context, | ||||
|               removeTop: true, | ||||
|               child: RefreshIndicator( | ||||
|                 onRefresh: () => Future.wait([ | ||||
|                   _fetchRelations(), | ||||
| @@ -282,6 +286,7 @@ class _FriendScreenState extends State<FriendScreen> { | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   | ||||
| @@ -25,6 +25,7 @@ import 'package:surface/types/check_in.dart'; | ||||
| import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/widgets/app_bar_leading.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/post/post_item.dart'; | ||||
|  | ||||
| class HomeScreenDashEntry { | ||||
| @@ -67,7 +68,7 @@ class _HomeScreenState extends State<HomeScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: AutoAppBarLeading(), | ||||
|         title: Text("screenHome").tr(), | ||||
| @@ -387,6 +388,8 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> { | ||||
|                         Text( | ||||
|                           'dailyCheckInNone', | ||||
|                           style: Theme.of(context).textTheme.bodyLarge, | ||||
|                           maxLines: 2, | ||||
|                           overflow: TextOverflow.ellipsis, | ||||
|                         ).tr(), | ||||
|                       ], | ||||
|                     ) | ||||
|   | ||||
| @@ -14,6 +14,7 @@ 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/markdown_content.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/post/post_item.dart'; | ||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||
|  | ||||
| @@ -137,7 +138,7 @@ class _NotificationScreenState extends State<NotificationScreen> { | ||||
|     final ua = context.read<UserProvider>(); | ||||
|  | ||||
|     if (!ua.isAuthorized) { | ||||
|       return Scaffold( | ||||
|       return AppScaffold( | ||||
|         appBar: AppBar( | ||||
|           leading: AutoAppBarLeading(), | ||||
|           title: Text('screenNotification').tr(), | ||||
| @@ -148,7 +149,7 @@ class _NotificationScreenState extends State<NotificationScreen> { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: AutoAppBarLeading(), | ||||
|         title: Text('screenNotification').tr(), | ||||
|   | ||||
| @@ -14,6 +14,7 @@ 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'; | ||||
| import 'package:surface/widgets/post/post_mini_editor.dart'; | ||||
| @@ -67,7 +68,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> { | ||||
|  | ||||
|     return AppBackground( | ||||
|       isRoot: widget.onBack != null, | ||||
|       child: Scaffold( | ||||
|       child: AppScaffold( | ||||
|         appBar: AppBar( | ||||
|           leading: BackButton( | ||||
|             onPressed: () { | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/post/post_item.dart'; | ||||
| import 'package:surface/widgets/post/post_media_pending_list.dart'; | ||||
| import 'package:surface/widgets/post/post_meta_editor.dart'; | ||||
| @@ -128,7 +129,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|     return ListenableBuilder( | ||||
|       listenable: _writeController, | ||||
|       builder: (context, _) { | ||||
|         return Scaffold( | ||||
|         return AppScaffold( | ||||
|           appBar: AppBar( | ||||
|             leading: BackButton( | ||||
|               onPressed: () { | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/post.dart'; | ||||
| import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/post/post_item.dart'; | ||||
| import 'package:surface/widgets/post/post_tags_field.dart'; | ||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||
| @@ -119,7 +120,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> { | ||||
|       ), | ||||
|     ]; | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: Text('screenPostSearch').tr(), | ||||
|         actions: [ | ||||
|   | ||||
| @@ -17,6 +17,7 @@ import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/types/realm.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/post/post_item.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||
| @@ -274,7 +275,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi | ||||
|  | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       body: NestedScrollView( | ||||
|         controller: _scrollController, | ||||
|         headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import 'package:surface/widgets/account/account_image.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_scaffold.dart'; | ||||
| import 'package:surface/widgets/unauthorized_hint.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
|  | ||||
| @@ -83,7 +84,7 @@ class _RealmScreenState extends State<RealmScreen> { | ||||
|     final ua = context.read<UserProvider>(); | ||||
|  | ||||
|     if (!ua.isAuthorized) { | ||||
|       return Scaffold( | ||||
|       return AppScaffold( | ||||
|         appBar: AppBar( | ||||
|           leading: AutoAppBarLeading(), | ||||
|           title: Text('screenRealm').tr(), | ||||
| @@ -94,7 +95,7 @@ class _RealmScreenState extends State<RealmScreen> { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: AutoAppBarLeading(), | ||||
|         title: Text('screenRealm').tr(), | ||||
| @@ -118,6 +119,9 @@ class _RealmScreenState extends State<RealmScreen> { | ||||
|         children: [ | ||||
|           LoadingIndicator(isActive: _isBusy), | ||||
|           Expanded( | ||||
|             child: MediaQuery.removePadding( | ||||
|               context: context, | ||||
|               removeTop: true, | ||||
|               child: RefreshIndicator( | ||||
|                 onRefresh: _fetchRealms, | ||||
|                 child: ListView.builder( | ||||
| @@ -196,7 +200,9 @@ class _RealmScreenState extends State<RealmScreen> { | ||||
|                                   clipBehavior: Clip.none, | ||||
|                                   fit: StackFit.expand, | ||||
|                                   children: [ | ||||
|                                   Container( | ||||
|                                     ClipRRect( | ||||
|                                       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                                       child: Container( | ||||
|                                         color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                                         child: (realm.banner?.isEmpty ?? true) | ||||
|                                             ? const SizedBox.shrink() | ||||
| @@ -205,6 +211,7 @@ class _RealmScreenState extends State<RealmScreen> { | ||||
|                                                 fit: BoxFit.cover, | ||||
|                                               ), | ||||
|                                       ), | ||||
|                                     ), | ||||
|                                     Positioned( | ||||
|                                       bottom: -30, | ||||
|                                       left: 18, | ||||
| @@ -240,6 +247,7 @@ class _RealmScreenState extends State<RealmScreen> { | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   | ||||
| @@ -18,6 +18,7 @@ import 'package:surface/types/realm.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
| import 'package:uuid/uuid.dart'; | ||||
|  | ||||
| @@ -179,7 +180,7 @@ class _RealmManageScreenState extends State<RealmManageScreen> { | ||||
|   Widget build(BuildContext context) { | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: widget.editingRealmAlias != null | ||||
|             ? Text('screenRealmManage').tr() | ||||
|   | ||||
| @@ -8,13 +8,13 @@ import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/user_directory.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/types/realm.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||
|  | ||||
| import '../../types/post.dart'; | ||||
|  | ||||
| class RealmDetailScreen extends StatefulWidget { | ||||
|   final String alias; | ||||
|  | ||||
| @@ -70,19 +70,11 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> { | ||||
|   Widget build(BuildContext context) { | ||||
|     return DefaultTabController( | ||||
|       length: 3, | ||||
|       child: Scaffold( | ||||
|       child: AppScaffold( | ||||
|         body: NestedScrollView( | ||||
|           headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { | ||||
|             // These are the slivers that show up in the "outer" scroll view. | ||||
|             return <Widget>[ | ||||
|               SliverOverlapAbsorber( | ||||
|                 // This widget takes the overlapping behavior of the SliverAppBar, | ||||
|                 // and redirects it to the SliverOverlapInjector below. If it is | ||||
|                 // missing, then it is possible for the nested "inner" scroll view | ||||
|                 // below to end up under the SliverAppBar even when the inner | ||||
|                 // scroll view thinks it has not been scrolled. | ||||
|                 // This is not necessary if the "headerSliverBuilder" only builds | ||||
|                 // widgets that do not overlap the next sliver. | ||||
|                 handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), | ||||
|                 sliver: SliverAppBar( | ||||
|                   title: Text(_realm?.name ?? 'loading'.tr()), | ||||
| @@ -428,7 +420,7 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> { | ||||
|  | ||||
|     return Column( | ||||
|       children: [ | ||||
|         const Gap(16), | ||||
|         const Gap(8), | ||||
|         ListTile( | ||||
|           leading: const Icon(Symbols.edit), | ||||
|           trailing: const Icon(Symbols.chevron_right), | ||||
|   | ||||
| @@ -18,6 +18,7 @@ import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/theme.dart'; | ||||
| import 'package:surface/theme.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| const Map<String, Color> kColorSchemes = { | ||||
|   'colorSchemeIndigo': Colors.indigo, | ||||
| @@ -67,7 +68,11 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|   Widget build(BuildContext context) { | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: const PageBackButton(), | ||||
|         title: Text('screenSettings').tr(), | ||||
|       ), | ||||
|       body: SingleChildScrollView( | ||||
|         child: Column( | ||||
|           spacing: 16, | ||||
| @@ -255,6 +260,48 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|             Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 Text('settingsFeatures').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4), | ||||
|                 CheckboxListTile( | ||||
|                   secondary: const Icon(Symbols.vibration), | ||||
|                   contentPadding: const EdgeInsets.only(left: 24, right: 17), | ||||
|                   title: Text('settingsNotifyWithHaptic').tr(), | ||||
|                   subtitle: Text('settingsNotifyWithHapticDescription').tr(), | ||||
|                   value: _prefs.getBool(kAppNotifyWithHaptic) ?? true, | ||||
|                   onChanged: (value) { | ||||
|                     setState(() { | ||||
|                       _prefs.setBool(kAppNotifyWithHaptic, value ?? false); | ||||
|                     }); | ||||
|                   }, | ||||
|                 ), | ||||
|                 CheckboxListTile( | ||||
|                   secondary: const Icon(Symbols.link), | ||||
|                   title: Text('settingsExpandPostLink').tr(), | ||||
|                   subtitle: Text('settingsExpandPostLinkDescription').tr(), | ||||
|                   contentPadding: const EdgeInsets.only(left: 24, right: 17), | ||||
|                   value: _prefs.getBool(kAppExpandPostLink) ?? true, | ||||
|                   onChanged: (value) { | ||||
|                     setState(() { | ||||
|                       _prefs.setBool(kAppExpandPostLink, value ?? false); | ||||
|                     }); | ||||
|                   }, | ||||
|                 ), | ||||
|                 CheckboxListTile( | ||||
|                   secondary: const Icon(Symbols.chat), | ||||
|                   title: Text('settingsExpandChatLink').tr(), | ||||
|                   subtitle: Text('settingsExpandChatLinkDescription').tr(), | ||||
|                   contentPadding: const EdgeInsets.only(left: 24, right: 17), | ||||
|                   value: _prefs.getBool(kAppExpandChatLink) ?? true, | ||||
|                   onChanged: (value) { | ||||
|                     setState(() { | ||||
|                       _prefs.setBool(kAppExpandChatLink, value ?? false); | ||||
|                     }); | ||||
|                   }, | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|             Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|   | ||||
| @@ -34,9 +34,10 @@ Future<ThemeData> createAppTheme( | ||||
|   ); | ||||
|  | ||||
|   final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false; | ||||
|   final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true); | ||||
|  | ||||
|   return ThemeData( | ||||
|     useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true), | ||||
|     useMaterial3: useM3, | ||||
|     colorScheme: colorScheme, | ||||
|     brightness: brightness, | ||||
|     iconTheme: IconThemeData( | ||||
| @@ -45,17 +46,19 @@ Future<ThemeData> createAppTheme( | ||||
|       opticalSize: 20, | ||||
|       color: colorScheme.onSurface, | ||||
|     ), | ||||
|     snackBarTheme: SnackBarThemeData( | ||||
|       behavior: useM3 ? SnackBarBehavior.floating : SnackBarBehavior.fixed, | ||||
|     ), | ||||
|     appBarTheme: AppBarTheme( | ||||
|       centerTitle: true, | ||||
|       elevation: hasAppBarBlurry ? 0 : null, | ||||
|       backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary, | ||||
|       foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary, | ||||
|     ), | ||||
|     scaffoldBackgroundColor: Colors.transparent, | ||||
|     pageTransitionsTheme: PageTransitionsTheme( | ||||
|       builders: { | ||||
|         TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), | ||||
|         TargetPlatform.iOS: ZoomPageTransitionsBuilder(), | ||||
|         TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), | ||||
|         TargetPlatform.macOS: ZoomPageTransitionsBuilder(), | ||||
|         TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(), | ||||
|         TargetPlatform.linux: ZoomPageTransitionsBuilder(), | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:package_info_plus/package_info_plus.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| class AboutScreen extends StatelessWidget { | ||||
| @@ -12,7 +13,12 @@ class AboutScreen extends StatelessWidget { | ||||
|   Widget build(BuildContext context) { | ||||
|     const denseButtonStyle = ButtonStyle(visualDensity: VisualDensity(vertical: -4)); | ||||
|  | ||||
|     return SizedBox( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: const PageBackButton(), | ||||
|         title: Text('screenAbout').tr(), | ||||
|       ), | ||||
|       body: SizedBox( | ||||
|         width: double.infinity, | ||||
|         child: Column( | ||||
|           mainAxisAlignment: MainAxisAlignment.center, | ||||
| @@ -104,6 +110,7 @@ class AboutScreen extends StatelessWidget { | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -15,6 +15,7 @@ class AttachmentList extends StatefulWidget { | ||||
|   final List<SnAttachment?> data; | ||||
|   final bool bordered; | ||||
|   final bool gridded; | ||||
|   final bool columned; | ||||
|   final BoxFit fit; | ||||
|   final double? maxHeight; | ||||
|   final double? minWidth; | ||||
| @@ -26,6 +27,7 @@ class AttachmentList extends StatefulWidget { | ||||
|     required this.data, | ||||
|     this.bordered = false, | ||||
|     this.gridded = false, | ||||
|     this.columned = false, | ||||
|     this.fit = BoxFit.cover, | ||||
|     this.maxHeight, | ||||
|     this.minWidth, | ||||
| @@ -105,45 +107,10 @@ class _AttachmentListState extends State<AttachmentList> { | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         if (widget.gridded) { | ||||
|         final fullOfImage = | ||||
|             widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length; | ||||
|           if(!fullOfImage) { | ||||
|             return Container( | ||||
|               margin: widget.padding ?? EdgeInsets.zero, | ||||
|               decoration: BoxDecoration( | ||||
|                 color: backgroundColor, | ||||
|                 border: Border( | ||||
|                   top: borderSide, | ||||
|                   bottom: borderSide, | ||||
|                 ), | ||||
|                 borderRadius: AttachmentList.kDefaultRadius, | ||||
|               ), | ||||
|               child: ClipRRect( | ||||
|                 borderRadius: AttachmentList.kDefaultRadius, | ||||
|                 child: Column( | ||||
|                   spacing: 4, | ||||
|                   children: widget.data | ||||
|                       .mapIndexed( | ||||
|                         (idx, ele) => GestureDetector( | ||||
|                           child: AspectRatio( | ||||
|                             aspectRatio: ele?.data['ratio']?.toDouble() ?? 1, | ||||
|                             child: Container( | ||||
|                               constraints: constraints, | ||||
|                               child: AttachmentItem( | ||||
|                                 data: ele, | ||||
|                                 heroTag: heroTags[idx], | ||||
|                                 fit: BoxFit.cover, | ||||
|                               ), | ||||
|                             ), | ||||
|                           ), | ||||
|                         ), | ||||
|                       ) | ||||
|                       .toList(), | ||||
|                 ), | ||||
|               ), | ||||
|             ); | ||||
|           } | ||||
|  | ||||
|         if (widget.gridded && fullOfImage) { | ||||
|           return Container( | ||||
|             margin: widget.padding ?? EdgeInsets.zero, | ||||
|             decoration: BoxDecoration( | ||||
| @@ -191,6 +158,44 @@ class _AttachmentListState extends State<AttachmentList> { | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         if ((!fullOfImage && widget.gridded) || widget.columned) { | ||||
|           return Container( | ||||
|             margin: widget.padding ?? EdgeInsets.zero, | ||||
|             decoration: BoxDecoration( | ||||
|               color: backgroundColor, | ||||
|               border: Border( | ||||
|                 top: borderSide, | ||||
|                 bottom: borderSide, | ||||
|               ), | ||||
|               borderRadius: AttachmentList.kDefaultRadius, | ||||
|             ), | ||||
|             child: ClipRRect( | ||||
|               borderRadius: AttachmentList.kDefaultRadius, | ||||
|               child: Column( | ||||
|                 children: widget.data | ||||
|                     .mapIndexed( | ||||
|                       (idx, ele) => GestureDetector( | ||||
|                         child: AspectRatio( | ||||
|                           aspectRatio: ele?.data['ratio']?.toDouble() ?? 1, | ||||
|                           child: Container( | ||||
|                             constraints: constraints, | ||||
|                             child: AttachmentItem( | ||||
|                               data: ele, | ||||
|                               heroTag: heroTags[idx], | ||||
|                               fit: BoxFit.cover, | ||||
|                             ), | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ) | ||||
|                     .expand((ele) => [ele, const Divider(height: 1)]) | ||||
|                     .toList() | ||||
|                   ..removeLast(), | ||||
|               ), | ||||
|             ), | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         return Container( | ||||
|           constraints: BoxConstraints(maxHeight: constraints.maxHeight), | ||||
|           child: ScrollConfiguration( | ||||
|   | ||||
| @@ -365,7 +365,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> { | ||||
|         ), | ||||
|         onVerticalDragUpdate: (details) { | ||||
|           if (_showDetail) return; | ||||
|           if (details.delta.dy < 0) { | ||||
|           if (details.delta.dy <= -40) { | ||||
|             _showDetail = true; | ||||
|             showModalBottomSheet( | ||||
|               context: context, | ||||
| @@ -415,6 +415,7 @@ class _AttachmentZoomDetailPopup extends StatelessWidget { | ||||
|             ], | ||||
|           ).padding(horizontal: 20, top: 16, bottom: 12), | ||||
|           Expanded( | ||||
|             child: SingleChildScrollView( | ||||
|               child: Table( | ||||
|                 columnWidths: { | ||||
|                   0: IntrinsicColumnWidth(), | ||||
| @@ -487,6 +488,7 @@ class _AttachmentZoomDetailPopup extends StatelessWidget { | ||||
|                 ], | ||||
|               ).padding(horizontal: 20, vertical: 8), | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:popover/popover.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
| import 'package:surface/providers/user_directory.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/types/chat.dart'; | ||||
| @@ -53,6 +54,8 @@ class ChatMessage extends StatelessWidget { | ||||
|  | ||||
|     final dateFormatter = DateFormat('MM/dd HH:mm'); | ||||
|  | ||||
|     final cfg = context.read<ConfigProvider>(); | ||||
|  | ||||
|     return SwipeTo( | ||||
|       key: Key('chat-message-${data.id}'), | ||||
|       iconOnLeftSwipe: Symbols.reply, | ||||
| @@ -192,7 +195,10 @@ class ChatMessage extends StatelessWidget { | ||||
|                 ], | ||||
|               ).opacity(isPending ? 0.5 : 1), | ||||
|             ), | ||||
|             if (data.body['text'] != null && data.type == 'messages.new' && (data.body['text']?.isNotEmpty ?? false)) | ||||
|             if (data.body['text'] != null && | ||||
|                 data.type == 'messages.new' && | ||||
|                 (data.body['text']?.isNotEmpty ?? false) && | ||||
|                 (cfg.prefs.getBool(kAppExpandChatLink) ?? true)) | ||||
|               LinkPreviewWidget(text: data.body['text']!), | ||||
|             if (data.preload?.attachments?.isNotEmpty ?? false) | ||||
|               AttachmentList( | ||||
|   | ||||
| @@ -18,8 +18,11 @@ class ConnectionIndicator extends StatelessWidget { | ||||
|       listenable: ws, | ||||
|       builder: (context, _) { | ||||
|         final ua = context.read<UserProvider>(); | ||||
|         final show = (ws.isBusy || !ws.isConnected) && ua.isAuthorized; | ||||
|  | ||||
|         return GestureDetector( | ||||
|         return IgnorePointer( | ||||
|           ignoring: !show, | ||||
|           child: GestureDetector( | ||||
|             child: Material( | ||||
|               elevation: 2, | ||||
|               shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), | ||||
| @@ -48,7 +51,7 @@ class ConnectionIndicator extends StatelessWidget { | ||||
|                       ], | ||||
|                     ).padding(horizontal: 8, vertical: 4) | ||||
|                   : const SizedBox.shrink(), | ||||
|           ).opacity((ws.isBusy || !ws.isConnected) && ua.isAuthorized ? 1 : 0, animate: true).animate( | ||||
|             ).opacity(show ? 1 : 0, animate: true).animate( | ||||
|                   const Duration(milliseconds: 300), | ||||
|                   Curves.easeInOut, | ||||
|                 ), | ||||
| @@ -57,6 +60,7 @@ class ConnectionIndicator extends StatelessWidget { | ||||
|                 ws.connect(); | ||||
|               } | ||||
|             }, | ||||
|           ), | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   | ||||
| @@ -7,12 +7,11 @@ import 'package:marquee/marquee.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:responsive_framework/responsive_framework.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/link_preview.dart'; | ||||
| import 'package:surface/types/link.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| import '../providers/link_preview.dart'; | ||||
|  | ||||
| class LinkPreviewWidget extends StatefulWidget { | ||||
|   final String text; | ||||
|  | ||||
| @@ -81,8 +80,9 @@ class _LinkPreviewEntry extends StatelessWidget { | ||||
|                   child: AspectRatio( | ||||
|                     aspectRatio: 16 / 9, | ||||
|                     child: ClipRRect( | ||||
|                       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                       child: AutoResizeUniversalImage( | ||||
|                         meta.image!, | ||||
|                         meta.image!.startsWith('//') ? 'https:${meta.image}' : meta.image!, | ||||
|                         fit: BoxFit.contain, | ||||
|                       ), | ||||
|                     ), | ||||
| @@ -94,11 +94,14 @@ class _LinkPreviewEntry extends StatelessWidget { | ||||
|                   crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                   children: [ | ||||
|                     if (meta.icon?.isNotEmpty ?? false) | ||||
|                       StyledWidget( | ||||
|                         meta.icon!.endsWith('.svg') | ||||
|                             ? SvgPicture.network(meta.icon!) | ||||
|                       SizedBox( | ||||
|                         width: 36, | ||||
|                         height: 36, | ||||
|                         child: meta.icon!.endsWith('.svg') | ||||
|                             ? SvgPicture.network(meta.icon!, width: 36, height: 36) | ||||
|                             : UniversalImage( | ||||
|                                 meta.icon!, | ||||
|                                 noErrorWidget: true, | ||||
|                                 width: 36, | ||||
|                                 height: 36, | ||||
|                                 cacheHeight: 36, | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:bitsdojo_window/bitsdojo_window.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| @@ -12,7 +11,6 @@ import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
| import 'package:surface/providers/navigation.dart'; | ||||
| import 'package:surface/widgets/connection_indicator.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_background.dart'; | ||||
| import 'package:surface/widgets/navigation/app_bottom_navigation.dart'; | ||||
| import 'package:surface/widgets/navigation/app_drawer_navigation.dart'; | ||||
| @@ -21,34 +19,76 @@ import 'package:surface/widgets/notify_indicator.dart'; | ||||
|  | ||||
| final globalRootScaffoldKey = GlobalKey<ScaffoldState>(); | ||||
|  | ||||
| class AppPageScaffold extends StatelessWidget { | ||||
|   final String? title; | ||||
| class AppScaffold extends StatelessWidget { | ||||
|   final Widget? body; | ||||
|   final bool showAppBar; | ||||
|   final bool showBottomNavigation; | ||||
|   final PreferredSizeWidget? bottomNavigationBar; | ||||
|   final PreferredSizeWidget? bottomSheet; | ||||
|   final Drawer? drawer; | ||||
|   final Widget? endDrawer; | ||||
|   final FloatingActionButtonAnimator? floatingActionButtonAnimator; | ||||
|   final FloatingActionButtonLocation? floatingActionButtonLocation; | ||||
|   final Widget? floatingActionButton; | ||||
|   final AppBar? appBar; | ||||
|   final DrawerCallback? onDrawerChanged; | ||||
|   final DrawerCallback? onEndDrawerChanged; | ||||
|  | ||||
|   const AppPageScaffold({ | ||||
|   const AppScaffold({ | ||||
|     super.key, | ||||
|     this.title, | ||||
|     this.appBar, | ||||
|     this.body, | ||||
|     this.showAppBar = true, | ||||
|     this.showBottomNavigation = false, | ||||
|     this.floatingActionButton, | ||||
|     this.floatingActionButtonLocation, | ||||
|     this.floatingActionButtonAnimator, | ||||
|     this.bottomNavigationBar, | ||||
|     this.bottomSheet, | ||||
|     this.drawer, | ||||
|     this.endDrawer, | ||||
|     this.onDrawerChanged, | ||||
|     this.onEndDrawerChanged, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final state = GoRouter.maybeOf(context); | ||||
|     final routeName = state?.routerDelegate.currentConfiguration.last.route.name; | ||||
|  | ||||
|     final autoTitle = state != null ? 'screen${routeName?.capitalize()}' : 'screen'; | ||||
|     final appBarHeight = appBar?.preferredSize.height ?? 0; | ||||
|     final safeTop = MediaQuery.of(context).padding.top; | ||||
|  | ||||
|     return Scaffold( | ||||
|       appBar: showAppBar | ||||
|           ? AppBar( | ||||
|               title: Text(title ?? autoTitle.tr()), | ||||
|             ) | ||||
|           : null, | ||||
|       body: body, | ||||
|       extendBody: true, | ||||
|       extendBodyBehindAppBar: true, | ||||
|       backgroundColor: Theme.of(context).scaffoldBackgroundColor, | ||||
|       body: SizedBox.expand( | ||||
|         child: AppBackground( | ||||
|           child: Column( | ||||
|             children: [ | ||||
|               IgnorePointer(child: SizedBox(height: appBar != null ? appBarHeight + safeTop : 0)), | ||||
|               if (body != null) Expanded(child: body!), | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|       appBar: appBar, | ||||
|       bottomNavigationBar: bottomNavigationBar, | ||||
|       bottomSheet: bottomSheet, | ||||
|       drawer: drawer, | ||||
|       endDrawer: endDrawer, | ||||
|       floatingActionButton: floatingActionButton, | ||||
|       floatingActionButtonAnimator: floatingActionButtonAnimator, | ||||
|       floatingActionButtonLocation: floatingActionButtonLocation, | ||||
|       onDrawerChanged: onDrawerChanged, | ||||
|       onEndDrawerChanged: onEndDrawerChanged, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class PageBackButton extends StatelessWidget { | ||||
|   const PageBackButton({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return BackButton( | ||||
|       onPressed: () { | ||||
|         GoRouter.of(context).pop(); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -101,16 +141,16 @@ class AppRootScaffold extends StatelessWidget { | ||||
|  | ||||
|     final safeTop = MediaQuery.of(context).padding.top; | ||||
|  | ||||
|     return AppBackground( | ||||
|       isRoot: true, | ||||
|       child: Scaffold( | ||||
|     return Scaffold( | ||||
|       key: globalRootScaffoldKey, | ||||
|       backgroundColor: Theme.of(context).colorScheme.surface, | ||||
|       body: Stack( | ||||
|         children: [ | ||||
|           Column( | ||||
|             children: [ | ||||
|               if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) | ||||
|                   Container( | ||||
|                 WindowTitleBarBox( | ||||
|                   child: Container( | ||||
|                     decoration: BoxDecoration( | ||||
|                       border: Border( | ||||
|                         bottom: BorderSide( | ||||
| @@ -119,22 +159,18 @@ class AppRootScaffold extends StatelessWidget { | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                     child: MoveWindow( | ||||
|                       child: Row( | ||||
|                         crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                         mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start, | ||||
|                         children: [ | ||||
|                         WindowTitleBarBox( | ||||
|                           child: MoveWindow( | ||||
|                             child: Text( | ||||
|                           Text( | ||||
|                             'Solar Network', | ||||
|                             style: GoogleFonts.spaceGrotesk(), | ||||
|                           ).padding(horizontal: 12, vertical: 5), | ||||
|                           ), | ||||
|                         ), | ||||
|                           if (!Platform.isMacOS) | ||||
|                           Expanded( | ||||
|                             child: WindowTitleBarBox( | ||||
|                               child: Row( | ||||
|                             Row( | ||||
|                               mainAxisSize: MainAxisSize.min, | ||||
|                               children: [ | ||||
|                                 Expanded(child: MoveWindow()), | ||||
|                                 Row( | ||||
| @@ -146,11 +182,11 @@ class AppRootScaffold extends StatelessWidget { | ||||
|                                 ), | ||||
|                               ], | ||||
|                             ), | ||||
|                             ), | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               Expanded(child: innerWidget), | ||||
|             ], | ||||
|           ), | ||||
| @@ -161,7 +197,6 @@ class AppRootScaffold extends StatelessWidget { | ||||
|       drawer: !isExpandedDrawer ? AppNavigationDrawer() : null, | ||||
|       drawerEdgeDragWidth: isPopable ? 0 : null, | ||||
|       bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -15,10 +15,14 @@ class NotifyIndicator extends StatelessWidget { | ||||
|     final ua = context.read<UserProvider>(); | ||||
|     final nty = context.watch<NotificationProvider>(); | ||||
|  | ||||
|     final show = nty.notifications.isNotEmpty && ua.isAuthorized; | ||||
|  | ||||
|     return ListenableBuilder( | ||||
|         listenable: nty, | ||||
|         builder: (context, _) { | ||||
|           return GestureDetector( | ||||
|           return IgnorePointer( | ||||
|             ignoring: !show, | ||||
|             child: GestureDetector( | ||||
|               child: Material( | ||||
|                 elevation: 2, | ||||
|                 shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), | ||||
| @@ -45,13 +49,14 @@ class NotifyIndicator extends StatelessWidget { | ||||
|                         ], | ||||
|                       ).padding(horizontal: 8, vertical: 4) | ||||
|                     : const SizedBox.shrink(), | ||||
|             ).opacity(nty.notifications.isNotEmpty && ua.isAuthorized ? 1 : 0, animate: true).animate( | ||||
|               ).opacity(show ? 1 : 0, animate: true).animate( | ||||
|                     const Duration(milliseconds: 300), | ||||
|                     Curves.easeInOut, | ||||
|                   ), | ||||
|               onTap: () { | ||||
|                 nty.clear(); | ||||
|               }, | ||||
|             ), | ||||
|           ); | ||||
|         }); | ||||
|   } | ||||
|   | ||||
| @@ -203,6 +203,8 @@ class PostItem extends StatelessWidget { | ||||
|         ?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article') | ||||
|         .toList(); | ||||
|  | ||||
|     final cfg = context.read<ConfigProvider>(); | ||||
|  | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.center, | ||||
|       children: [ | ||||
| @@ -256,13 +258,12 @@ class PostItem extends StatelessWidget { | ||||
|           AttachmentList( | ||||
|             data: displayableAttachments!, | ||||
|             bordered: true, | ||||
|             gridded: true, | ||||
|             maxHeight: showFullPost ? null : 480, | ||||
|             minWidth: 640, | ||||
|             maxWidth: MediaQuery.of(context).size.width - 20, | ||||
|             fit: showFullPost ? BoxFit.cover : BoxFit.contain, | ||||
|             padding: const EdgeInsets.symmetric(horizontal: 12), | ||||
|           ), | ||||
|         if (data.body['content'] != null) | ||||
|         if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true)) | ||||
|           LinkPreviewWidget( | ||||
|             text: data.body['content'], | ||||
|           ).padding(horizontal: 4), | ||||
| @@ -344,7 +345,7 @@ class PostShareImageWidget extends StatelessWidget { | ||||
|           if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false)) | ||||
|             StyledWidget(AttachmentList( | ||||
|               data: data.preload!.attachments!, | ||||
|               gridded: true, | ||||
|               columned: true, | ||||
|             )).padding(horizontal: 16, bottom: 8), | ||||
|           Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| @@ -12,6 +13,7 @@ import 'package:surface/widgets/dialog.dart'; | ||||
| class PostReactionPopup extends StatefulWidget { | ||||
|   final SnPost data; | ||||
|   final Function(Map<String, int> value, int attr, int delta)? onChanged; | ||||
|  | ||||
|   const PostReactionPopup({super.key, required this.data, this.onChanged}); | ||||
|  | ||||
|   @override | ||||
| @@ -59,6 +61,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> { | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|       HapticFeedback.mediumImpact(); | ||||
|     } catch (err) { | ||||
|       // ignore: use_build_context_synchronously | ||||
|       if (context.mounted) context.showErrorDialog(err); | ||||
| @@ -84,9 +87,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> { | ||||
|             children: [ | ||||
|               const Icon(Symbols.mood, size: 24), | ||||
|               const Gap(16), | ||||
|               Text('postReactions') | ||||
|                   .tr() | ||||
|                   .textStyle(Theme.of(context).textTheme.titleLarge!), | ||||
|               Text('postReactions').tr().textStyle(Theme.of(context).textTheme.titleLarge!), | ||||
|             ], | ||||
|           ).padding(horizontal: 20, top: 16, bottom: 12), | ||||
|           Container( | ||||
| @@ -102,9 +103,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> { | ||||
|                 Text('postReactionDownvote').plural(widget.data.totalDownvote), | ||||
|                 const Gap(24), | ||||
|                 Icon( | ||||
|                   widget.data.totalUpvote >= widget.data.totalDownvote | ||||
|                       ? Symbols.trending_up | ||||
|                       : Symbols.trending_down, | ||||
|                   widget.data.totalUpvote >= widget.data.totalDownvote ? Symbols.trending_up : Symbols.trending_down, | ||||
|                   size: 16, | ||||
|                 ), | ||||
|                 const Gap(8), | ||||
|   | ||||
| @@ -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.2.2+54 | ||||
| version: 2.2.2+55 | ||||
|  | ||||
| environment: | ||||
|   sdk: ^3.5.4 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user