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.", |   "settingsColorSchemeDescription": "Set the application primary color.", | ||||||
|   "settingsColorSeed": "Color Seed", |   "settingsColorSeed": "Color Seed", | ||||||
|   "settingsColorSeedDescription": "Select one of the present color schemes.", |   "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", |   "settingsNetwork": "Network", | ||||||
|   "settingsNetworkServer": "HyperNet Server", |   "settingsNetworkServer": "HyperNet Server", | ||||||
|   "settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.", |   "settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.", | ||||||
|   | |||||||
| @@ -191,6 +191,13 @@ | |||||||
|   "settingsColorSchemeDescription": "设置应用主题色。", |   "settingsColorSchemeDescription": "设置应用主题色。", | ||||||
|   "settingsColorSeed": "预设色彩主题", |   "settingsColorSeed": "预设色彩主题", | ||||||
|   "settingsColorSeedDescription": "选择一个预设色彩主题。", |   "settingsColorSeedDescription": "选择一个预设色彩主题。", | ||||||
|  |   "settingsFeatures": "功能", | ||||||
|  |   "settingsNotifyWithHaptic": "新通知时振动", | ||||||
|  |   "settingsNotifyWithHapticDescription": "在应用在前台时收到新通知出现时出发轻量的振动。", | ||||||
|  |   "settingsExpandPostLink": "展开帖子链接", | ||||||
|  |   "settingsExpandPostLinkDescription": "在帖子列表中展开显示帖子中的链接。", | ||||||
|  |   "settingsExpandChatLink": "展开聊天链接", | ||||||
|  |   "settingsExpandChatLinkDescription": "在聊天信息中展开显示内容中的链接。", | ||||||
|   "settingsNetwork": "网络", |   "settingsNetwork": "网络", | ||||||
|   "settingsNetworkServer": "HyperNet 服务器", |   "settingsNetworkServer": "HyperNet 服务器", | ||||||
|   "settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。", |   "settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。", | ||||||
|   | |||||||
| @@ -191,6 +191,9 @@ | |||||||
|   "settingsColorSchemeDescription": "設置應用主題色。", |   "settingsColorSchemeDescription": "設置應用主題色。", | ||||||
|   "settingsColorSeed": "預設色彩主題", |   "settingsColorSeed": "預設色彩主題", | ||||||
|   "settingsColorSeedDescription": "選擇一個預設色彩主題。", |   "settingsColorSeedDescription": "選擇一個預設色彩主題。", | ||||||
|  |   "settingsFeatures": "功能", | ||||||
|  |   "settingsNotifyWithHaptic": "新通知時振動", | ||||||
|  |   "settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。", | ||||||
|   "settingsNetwork": "網絡", |   "settingsNetwork": "網絡", | ||||||
|   "settingsNetworkServer": "HyperNet 服務器", |   "settingsNetworkServer": "HyperNet 服務器", | ||||||
|   "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", |   "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", | ||||||
| @@ -213,8 +216,9 @@ | |||||||
|   "sensitiveContentCollapsed": "敏感內容已摺疊。", |   "sensitiveContentCollapsed": "敏感內容已摺疊。", | ||||||
|   "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", |   "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", | ||||||
|   "sensitiveContentReveal": "顯示內容", |   "sensitiveContentReveal": "顯示內容", | ||||||
|   "serverConnecting": "正在連接服務器…", |   "serverConnecting": "正在連接…", | ||||||
|   "serverDisconnected": "已與服務器斷開連接", |   "serverDisconnected": "已斷開連接", | ||||||
|  |   "serverConnected": "已連接", | ||||||
|   "fieldChatAlias": "頻道別名", |   "fieldChatAlias": "頻道別名", | ||||||
|   "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。", |   "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。", | ||||||
|   "fieldChatName": "名稱", |   "fieldChatName": "名稱", | ||||||
| @@ -292,6 +296,7 @@ | |||||||
|   "addAttachmentFromCameraPhoto": "拍攝照片", |   "addAttachmentFromCameraPhoto": "拍攝照片", | ||||||
|   "addAttachmentFromCameraVideo": "拍攝視頻", |   "addAttachmentFromCameraVideo": "拍攝視頻", | ||||||
|   "addAttachmentFromRandomId": "通過訪問 ID 鏈接", |   "addAttachmentFromRandomId": "通過訪問 ID 鏈接", | ||||||
|  |   "attachmentDetailInfo": "附件詳細信息", | ||||||
|   "attachmentPastedImage": "粘貼的圖片", |   "attachmentPastedImage": "粘貼的圖片", | ||||||
|   "attachmentInsertLink": "插入連接", |   "attachmentInsertLink": "插入連接", | ||||||
|   "attachmentSetAsPostThumbnail": "設置為帖子縮略圖", |   "attachmentSetAsPostThumbnail": "設置為帖子縮略圖", | ||||||
|   | |||||||
| @@ -191,6 +191,9 @@ | |||||||
|   "settingsColorSchemeDescription": "設置應用主題色。", |   "settingsColorSchemeDescription": "設置應用主題色。", | ||||||
|   "settingsColorSeed": "預設色彩主題", |   "settingsColorSeed": "預設色彩主題", | ||||||
|   "settingsColorSeedDescription": "選擇一個預設色彩主題。", |   "settingsColorSeedDescription": "選擇一個預設色彩主題。", | ||||||
|  |   "settingsFeatures": "功能", | ||||||
|  |   "settingsNotifyWithHaptic": "新通知時振動", | ||||||
|  |   "settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。", | ||||||
|   "settingsNetwork": "網絡", |   "settingsNetwork": "網絡", | ||||||
|   "settingsNetworkServer": "HyperNet 服務器", |   "settingsNetworkServer": "HyperNet 服務器", | ||||||
|   "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", |   "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", | ||||||
| @@ -213,8 +216,9 @@ | |||||||
|   "sensitiveContentCollapsed": "敏感內容已摺疊。", |   "sensitiveContentCollapsed": "敏感內容已摺疊。", | ||||||
|   "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", |   "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", | ||||||
|   "sensitiveContentReveal": "顯示內容", |   "sensitiveContentReveal": "顯示內容", | ||||||
|   "serverConnecting": "正在連接服務器…", |   "serverConnecting": "正在連接…", | ||||||
|   "serverDisconnected": "已與服務器斷開連接", |   "serverDisconnected": "已斷開連接", | ||||||
|  |   "serverConnected": "已連接", | ||||||
|   "fieldChatAlias": "頻道別名", |   "fieldChatAlias": "頻道別名", | ||||||
|   "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。", |   "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。", | ||||||
|   "fieldChatName": "名稱", |   "fieldChatName": "名稱", | ||||||
| @@ -292,6 +296,7 @@ | |||||||
|   "addAttachmentFromCameraPhoto": "拍攝照片", |   "addAttachmentFromCameraPhoto": "拍攝照片", | ||||||
|   "addAttachmentFromCameraVideo": "拍攝視頻", |   "addAttachmentFromCameraVideo": "拍攝視頻", | ||||||
|   "addAttachmentFromRandomId": "通過訪問 ID 鏈接", |   "addAttachmentFromRandomId": "通過訪問 ID 鏈接", | ||||||
|  |   "attachmentDetailInfo": "附件詳細信息", | ||||||
|   "attachmentPastedImage": "粘貼的圖片", |   "attachmentPastedImage": "粘貼的圖片", | ||||||
|   "attachmentInsertLink": "插入連接", |   "attachmentInsertLink": "插入連接", | ||||||
|   "attachmentSetAsPostThumbnail": "設置為帖子縮略圖", |   "attachmentSetAsPostThumbnail": "設置為帖子縮略圖", | ||||||
|   | |||||||
| @@ -260,7 +260,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> { | |||||||
|     try { |     try { | ||||||
|       final cfg = context.read<ConfigProvider>(); |       final cfg = context.read<ConfigProvider>(); | ||||||
|       WidgetsBinding.instance.addPostFrameCallback((_) { |       WidgetsBinding.instance.addPostFrameCallback((_) { | ||||||
|         cfg.calcDrawerSize(context); |         cfg.calcDrawerSize(context, withMediaQuery: true); | ||||||
|       }); |       }); | ||||||
|       final home = context.read<HomeWidgetProvider>(); |       final home = context.read<HomeWidgetProvider>(); | ||||||
|       await home.initialize(); |       await home.initialize(); | ||||||
|   | |||||||
| @@ -14,6 +14,9 @@ const kAppbarTransparentStoreKey = 'app_bar_transparent'; | |||||||
| const kAppBackgroundStoreKey = 'app_has_background'; | const kAppBackgroundStoreKey = 'app_has_background'; | ||||||
| const kAppColorSchemeStoreKey = 'app_color_scheme'; | const kAppColorSchemeStoreKey = 'app_color_scheme'; | ||||||
| const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse'; | 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 = { | const Map<String, FilterQuality> kImageQualityLevel = { | ||||||
|   'settingsImageQualityLowest': FilterQuality.none, |   'settingsImageQualityLowest': FilterQuality.none, | ||||||
| @@ -38,14 +41,22 @@ class ConfigProvider extends ChangeNotifier { | |||||||
|   bool drawerIsCollapsed = false; |   bool drawerIsCollapsed = false; | ||||||
|   bool drawerIsExpanded = false; |   bool drawerIsExpanded = false; | ||||||
|  |  | ||||||
|   void calcDrawerSize(BuildContext context) { |   void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) { | ||||||
|     final rpb = ResponsiveBreakpoints.of(context); |     bool newDrawerIsCollapsed = false; | ||||||
|     final newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE); |     bool newDrawerIsExpanded = false; | ||||||
|     final newDrawerIsExpanded = rpb.largerThan(TABLET) |     if (withMediaQuery) { | ||||||
|         ? (prefs.getBool(kAppDrawerPreferCollapse) ?? false) |       newDrawerIsCollapsed = MediaQuery.of(context).size.width < 450; | ||||||
|             ? false |       newDrawerIsExpanded = MediaQuery.of(context).size.width >= 451; | ||||||
|             : true |     } else { | ||||||
|         : false; |       final rpb = ResponsiveBreakpoints.of(context); | ||||||
|  |       newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE); | ||||||
|  |       newDrawerIsCollapsed = rpb.largerThan(TABLET) | ||||||
|  |           ? (prefs.getBool(kAppDrawerPreferCollapse) ?? false) | ||||||
|  |               ? false | ||||||
|  |               : true | ||||||
|  |           : false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (newDrawerIsExpanded != drawerIsExpanded || newDrawerIsCollapsed != drawerIsCollapsed) { |     if (newDrawerIsExpanded != drawerIsExpanded || newDrawerIsCollapsed != drawerIsCollapsed) { | ||||||
|       drawerIsExpanded = newDrawerIsExpanded; |       drawerIsExpanded = newDrawerIsExpanded; | ||||||
|       drawerIsCollapsed = newDrawerIsCollapsed; |       drawerIsCollapsed = newDrawerIsCollapsed; | ||||||
|   | |||||||
| @@ -4,8 +4,10 @@ import 'dart:io'; | |||||||
| import 'package:firebase_messaging/firebase_messaging.dart'; | import 'package:firebase_messaging/firebase_messaging.dart'; | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter/services.dart'; | ||||||
| import 'package:flutter_udid/flutter_udid.dart'; | import 'package:flutter_udid/flutter_udid.dart'; | ||||||
| import 'package:provider/provider.dart'; | import 'package:provider/provider.dart'; | ||||||
|  | import 'package:surface/providers/config.dart'; | ||||||
| import 'package:surface/providers/sn_network.dart'; | import 'package:surface/providers/sn_network.dart'; | ||||||
| import 'package:surface/providers/userinfo.dart'; | import 'package:surface/providers/userinfo.dart'; | ||||||
| import 'package:surface/providers/websocket.dart'; | import 'package:surface/providers/websocket.dart'; | ||||||
| @@ -15,11 +17,13 @@ class NotificationProvider extends ChangeNotifier { | |||||||
|   late final SnNetworkProvider _sn; |   late final SnNetworkProvider _sn; | ||||||
|   late final UserProvider _ua; |   late final UserProvider _ua; | ||||||
|   late final WebSocketProvider _ws; |   late final WebSocketProvider _ws; | ||||||
|  |   late final ConfigProvider _cfg; | ||||||
|  |  | ||||||
|   NotificationProvider(BuildContext context) { |   NotificationProvider(BuildContext context) { | ||||||
|     _sn = context.read<SnNetworkProvider>(); |     _sn = context.read<SnNetworkProvider>(); | ||||||
|     _ua = context.read<UserProvider>(); |     _ua = context.read<UserProvider>(); | ||||||
|     _ws = context.read<WebSocketProvider>(); |     _ws = context.read<WebSocketProvider>(); | ||||||
|  |     _cfg = context.read<ConfigProvider>(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<void> registerPushNotifications() async { |   Future<void> registerPushNotifications() async { | ||||||
| @@ -75,6 +79,8 @@ class NotificationProvider extends ChangeNotifier { | |||||||
|         final notification = SnNotification.fromJson(event.payload!); |         final notification = SnNotification.fromJson(event.payload!); | ||||||
|         notifications.add(notification); |         notifications.add(notification); | ||||||
|         notifyListeners(); |         notifyListeners(); | ||||||
|  |         final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true; | ||||||
|  |         if (doHaptic) HapticFeedback.lightImpact(); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|   | |||||||
							
								
								
									
										447
									
								
								lib/router.dart
									
									
									
									
									
								
							
							
						
						
									
										447
									
								
								lib/router.dart
									
									
									
									
									
								
							| @@ -34,255 +34,222 @@ import 'package:surface/widgets/about.dart'; | |||||||
| import 'package:surface/widgets/navigation/app_background.dart'; | import 'package:surface/widgets/navigation/app_background.dart'; | ||||||
| import 'package:surface/widgets/navigation/app_scaffold.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 = [ | final _appRoutes = [ | ||||||
|   ShellRoute( |   GoRoute( | ||||||
|     builder: (context, state, child) => AppPageScaffold( |     path: '/', | ||||||
|       body: child, |     name: 'home', | ||||||
|       showAppBar: false, |     pageBuilder: (context, state) => CustomTransitionPage( | ||||||
|  |       transitionsBuilder: _fadeThroughTransition, | ||||||
|  |       child: const HomeScreen(), | ||||||
|  |     ), | ||||||
|  |   ), | ||||||
|  |   GoRoute( | ||||||
|  |     path: '/posts', | ||||||
|  |     name: 'explore', | ||||||
|  |     pageBuilder: (context, state) => CustomTransitionPage( | ||||||
|  |       transitionsBuilder: _fadeThroughTransition, | ||||||
|  |       child: const ExploreScreen(), | ||||||
|     ), |     ), | ||||||
|     routes: [ |     routes: [ | ||||||
|       GoRoute( |       GoRoute( | ||||||
|         path: '/', |         path: '/write/:mode', | ||||||
|         name: 'home', |         name: 'postEditor', | ||||||
|         pageBuilder: (context, state) => NoTransitionPage( |         builder: (context, state) => PostEditorScreen( | ||||||
|           child: const HomeScreen(), |           mode: state.pathParameters['mode']!, | ||||||
|  |           postEditId: int.tryParse( | ||||||
|  |             state.uri.queryParameters['editing'] ?? '', | ||||||
|  |           ), | ||||||
|  |           postReplyId: int.tryParse( | ||||||
|  |             state.uri.queryParameters['replying'] ?? '', | ||||||
|  |           ), | ||||||
|  |           postRepostId: int.tryParse( | ||||||
|  |             state.uri.queryParameters['reposting'] ?? '', | ||||||
|  |           ), | ||||||
|  |           extraProps: state.extra as PostEditorExtraProps?, | ||||||
|         ), |         ), | ||||||
|       ), |       ), | ||||||
|       GoRoute( |       GoRoute( | ||||||
|         path: '/posts', |         path: '/search', | ||||||
|         name: 'explore', |         name: 'postSearch', | ||||||
|         pageBuilder: (context, state) => NoTransitionPage( |         builder: (context, state) => PostSearchScreen( | ||||||
|           child: const ExploreScreen(), |           initialTags: state.uri.queryParameters['tags']?.split(','), | ||||||
|         ), |           initialCategories: state.uri.queryParameters['categories']?.split(','), | ||||||
|         routes: [ |  | ||||||
|           GoRoute( |  | ||||||
|             path: '/write/:mode', |  | ||||||
|             name: 'postEditor', |  | ||||||
|             builder: (context, state) => AppBackground( |  | ||||||
|               child: PostEditorScreen( |  | ||||||
|                 mode: state.pathParameters['mode']!, |  | ||||||
|                 postEditId: int.tryParse( |  | ||||||
|                   state.uri.queryParameters['editing'] ?? '', |  | ||||||
|                 ), |  | ||||||
|                 postReplyId: int.tryParse( |  | ||||||
|                   state.uri.queryParameters['replying'] ?? '', |  | ||||||
|                 ), |  | ||||||
|                 postRepostId: int.tryParse( |  | ||||||
|                   state.uri.queryParameters['reposting'] ?? '', |  | ||||||
|                 ), |  | ||||||
|                 extraProps: state.extra as PostEditorExtraProps?, |  | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|           GoRoute( |  | ||||||
|             path: '/search', |  | ||||||
|             name: 'postSearch', |  | ||||||
|             builder: (context, state) => AppBackground( |  | ||||||
|               child: 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']!), |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|           GoRoute( |  | ||||||
|             path: '/:slug', |  | ||||||
|             name: 'postDetail', |  | ||||||
|             builder: (context, state) => AppBackground( |  | ||||||
|               child: PostDetailScreen( |  | ||||||
|                 slug: state.pathParameters['slug']!, |  | ||||||
|                 preload: state.extra as SnPost?, |  | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|         ], |  | ||||||
|       ), |  | ||||||
|       GoRoute( |  | ||||||
|         path: '/account', |  | ||||||
|         name: 'account', |  | ||||||
|         pageBuilder: (context, state) => NoTransitionPage( |  | ||||||
|           child: const AccountScreen(), |  | ||||||
|         ), |  | ||||||
|         routes: [], |  | ||||||
|       ), |  | ||||||
|       GoRoute( |  | ||||||
|         path: '/chat', |  | ||||||
|         name: 'chat', |  | ||||||
|         pageBuilder: (context, state) => NoTransitionPage( |  | ||||||
|           child: const ChatScreen(), |  | ||||||
|         ), |  | ||||||
|         routes: [ |  | ||||||
|           GoRoute( |  | ||||||
|             path: '/:scope/:alias', |  | ||||||
|             name: 'chatRoom', |  | ||||||
|             builder: (context, state) => AppBackground( |  | ||||||
|               child: ChatRoomScreen( |  | ||||||
|                 scope: state.pathParameters['scope']!, |  | ||||||
|                 alias: state.pathParameters['alias']!, |  | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|           GoRoute( |  | ||||||
|             path: '/:scope/:alias/call', |  | ||||||
|             name: 'chatCallRoom', |  | ||||||
|             builder: (context, state) => AppBackground( |  | ||||||
|               child: CallRoomScreen( |  | ||||||
|                 scope: state.pathParameters['scope']!, |  | ||||||
|                 alias: state.pathParameters['alias']!, |  | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|           GoRoute( |  | ||||||
|             path: '/:scope/:alias/detail', |  | ||||||
|             name: 'channelDetail', |  | ||||||
|             builder: (context, state) => AppBackground( |  | ||||||
|               child: ChannelDetailScreen( |  | ||||||
|                 scope: state.pathParameters['scope']!, |  | ||||||
|                 alias: state.pathParameters['alias']!, |  | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|           GoRoute( |  | ||||||
|             path: '/manage', |  | ||||||
|             name: 'chatManage', |  | ||||||
|             pageBuilder: (context, state) => CustomTransitionPage( |  | ||||||
|               child: ChatManageScreen( |  | ||||||
|                 editingChannelAlias: state.uri.queryParameters['editing'], |  | ||||||
|               ), |  | ||||||
|               transitionsBuilder: (context, animation, secondaryAnimation, child) { |  | ||||||
|                 return FadeThroughTransition( |  | ||||||
|                   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( |  | ||||||
|           child: const RealmScreen(), |  | ||||||
|         ), |  | ||||||
|         routes: [ |  | ||||||
|           GoRoute( |  | ||||||
|             path: '/manage', |  | ||||||
|             name: 'realmManage', |  | ||||||
|             pageBuilder: (context, state) => CustomTransitionPage( |  | ||||||
|               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, |  | ||||||
|                   ), |  | ||||||
|                 ); |  | ||||||
|               }, |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|         ], |  | ||||||
|       ), |  | ||||||
|       GoRoute( |  | ||||||
|         path: '/album', |  | ||||||
|         name: 'album', |  | ||||||
|         pageBuilder: (context, state) => NoTransitionPage( |  | ||||||
|           child: const AlbumScreen(), |  | ||||||
|         ), |         ), | ||||||
|       ), |       ), | ||||||
|       GoRoute( |       GoRoute( | ||||||
|         path: '/friend', |         path: '/publishers/:name', | ||||||
|         name: 'friend', |         name: 'postPublisher', | ||||||
|         pageBuilder: (context, state) => NoTransitionPage( |         builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!), | ||||||
|           child: const FriendScreen(), |  | ||||||
|         ), |  | ||||||
|       ), |       ), | ||||||
|       GoRoute( |       GoRoute( | ||||||
|         path: '/notification', |         path: '/:slug', | ||||||
|         name: 'notification', |         name: 'postDetail', | ||||||
|         pageBuilder: (context, state) => NoTransitionPage( |         builder: (context, state) => PostDetailScreen( | ||||||
|           child: const NotificationScreen(), |           slug: state.pathParameters['slug']!, | ||||||
|  |           preload: state.extra as SnPost?, | ||||||
|         ), |         ), | ||||||
|       ), |       ), | ||||||
|     ], |     ], | ||||||
|   ), |   ), | ||||||
|   ShellRoute( |   GoRoute( | ||||||
|     builder: (context, state, child) => AppPageScaffold(body: child), |     path: '/account', | ||||||
|  |     name: 'account', | ||||||
|  |     pageBuilder: (context, state) => CustomTransitionPage( | ||||||
|  |       transitionsBuilder: _fadeThroughTransition, | ||||||
|  |       child: const AccountScreen(), | ||||||
|  |     ), | ||||||
|  |   ), | ||||||
|  |   GoRoute( | ||||||
|  |     path: '/chat', | ||||||
|  |     name: 'chat', | ||||||
|  |     pageBuilder: (context, state) => CustomTransitionPage( | ||||||
|  |       transitionsBuilder: _fadeThroughTransition, | ||||||
|  |       child: const ChatScreen(), | ||||||
|  |     ), | ||||||
|     routes: [ |     routes: [ | ||||||
|       GoRoute( |       GoRoute( | ||||||
|         path: '/auth/login', |         path: '/:scope/:alias', | ||||||
|         name: 'authLogin', |         name: 'chatRoom', | ||||||
|         builder: (context, state) => const AppBackground( |  | ||||||
|           child: LoginScreen(), |  | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|       GoRoute( |  | ||||||
|         path: '/auth/register', |  | ||||||
|         name: 'authRegister', |  | ||||||
|         builder: (context, state) => const AppBackground( |  | ||||||
|           child: RegisterScreen(), |  | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|       GoRoute( |  | ||||||
|         path: '/reports', |  | ||||||
|         name: 'abuseReport', |  | ||||||
|         builder: (context, state) => const AppBackground( |  | ||||||
|           child: AbuseReportScreen(), |  | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|       GoRoute( |  | ||||||
|         path: '/account/profile/edit', |  | ||||||
|         name: 'accountProfileEdit', |  | ||||||
|         builder: (context, state) => const AppBackground( |  | ||||||
|           child: ProfileEditScreen(), |  | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|       GoRoute( |  | ||||||
|         path: '/account/publishers', |  | ||||||
|         name: 'accountPublishers', |  | ||||||
|         builder: (context, state) => const AppBackground( |  | ||||||
|           child: PublisherScreen(), |  | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|       GoRoute( |  | ||||||
|         path: '/account/publishers/new', |  | ||||||
|         name: 'accountPublisherNew', |  | ||||||
|         builder: (context, state) => const AppBackground( |  | ||||||
|           child: AccountPublisherNewScreen(), |  | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|       GoRoute( |  | ||||||
|         path: '/account/publishers/edit/:name', |  | ||||||
|         name: 'accountPublisherEdit', |  | ||||||
|         builder: (context, state) => AppBackground( |         builder: (context, state) => AppBackground( | ||||||
|           child: AccountPublisherEditScreen( |           child: ChatRoomScreen( | ||||||
|             name: state.pathParameters['name']!, |             scope: state.pathParameters['scope']!, | ||||||
|  |             alias: state.pathParameters['alias']!, | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |       GoRoute( | ||||||
|  |         path: '/:scope/:alias/call', | ||||||
|  |         name: 'chatCallRoom', | ||||||
|  |         builder: (context, state) => AppBackground( | ||||||
|  |           child: CallRoomScreen( | ||||||
|  |             scope: state.pathParameters['scope']!, | ||||||
|  |             alias: state.pathParameters['alias']!, | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |       GoRoute( | ||||||
|  |         path: '/:scope/:alias/detail', | ||||||
|  |         name: 'channelDetail', | ||||||
|  |         builder: (context, state) => AppBackground( | ||||||
|  |           child: ChannelDetailScreen( | ||||||
|  |             scope: state.pathParameters['scope']!, | ||||||
|  |             alias: state.pathParameters['alias']!, | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |       GoRoute( | ||||||
|  |         path: '/manage', | ||||||
|  |         name: 'chatManage', | ||||||
|  |         pageBuilder: (context, state) => CustomTransitionPage( | ||||||
|  |           child: ChatManageScreen( | ||||||
|  |             editingChannelAlias: state.uri.queryParameters['editing'], | ||||||
|  |           ), | ||||||
|  |           transitionsBuilder: (context, animation, secondaryAnimation, child) { | ||||||
|  |             return FadeThroughTransition( | ||||||
|  |               animation: animation, | ||||||
|  |               secondaryAnimation: secondaryAnimation, | ||||||
|  |               fillColor: Colors.transparent, | ||||||
|  |               child: child, | ||||||
|  |             ); | ||||||
|  |           }, | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ], | ||||||
|  |   ), | ||||||
|  |   GoRoute( | ||||||
|  |     path: '/realm', | ||||||
|  |     name: 'realm', | ||||||
|  |     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'], | ||||||
|           ), |           ), | ||||||
|         ), |         ), | ||||||
|       ), |       ), | ||||||
|     ], |     ], | ||||||
|   ), |   ), | ||||||
|  |   GoRoute( | ||||||
|  |     path: '/album', | ||||||
|  |     name: 'album', | ||||||
|  |     pageBuilder: (context, state) => CustomTransitionPage( | ||||||
|  |       transitionsBuilder: _fadeThroughTransition, | ||||||
|  |       child: const AlbumScreen(), | ||||||
|  |     ), | ||||||
|  |   ), | ||||||
|  |   GoRoute( | ||||||
|  |     path: '/friend', | ||||||
|  |     name: 'friend', | ||||||
|  |     pageBuilder: (context, state) => NoTransitionPage( | ||||||
|  |       child: const FriendScreen(), | ||||||
|  |     ), | ||||||
|  |   ), | ||||||
|  |   GoRoute( | ||||||
|  |     path: '/notification', | ||||||
|  |     name: 'notification', | ||||||
|  |     pageBuilder: (context, state) => NoTransitionPage( | ||||||
|  |       child: const NotificationScreen(), | ||||||
|  |     ), | ||||||
|  |   ), | ||||||
|  |   GoRoute( | ||||||
|  |     path: '/auth/login', | ||||||
|  |     name: 'authLogin', | ||||||
|  |     builder: (context, state) => LoginScreen(), | ||||||
|  |   ), | ||||||
|  |   GoRoute( | ||||||
|  |     path: '/auth/register', | ||||||
|  |     name: 'authRegister', | ||||||
|  |     builder: (context, state) => RegisterScreen(), | ||||||
|  |   ), | ||||||
|  |   GoRoute( | ||||||
|  |     path: '/reports', | ||||||
|  |     name: 'abuseReport', | ||||||
|  |     builder: (context, state) => AbuseReportScreen(), | ||||||
|  |   ), | ||||||
|  |   GoRoute( | ||||||
|  |     path: '/account/profile/edit', | ||||||
|  |     name: 'accountProfileEdit', | ||||||
|  |     builder: (context, state) => ProfileEditScreen(), | ||||||
|  |   ), | ||||||
|  |   GoRoute( | ||||||
|  |     path: '/account/publishers', | ||||||
|  |     name: 'accountPublishers', | ||||||
|  |     builder: (context, state) => PublisherScreen(), | ||||||
|  |   ), | ||||||
|  |   GoRoute( | ||||||
|  |     path: '/account/publishers/new', | ||||||
|  |     name: 'accountPublisherNew', | ||||||
|  |     builder: (context, state) => AccountPublisherNewScreen(), | ||||||
|  |   ), | ||||||
|  |   GoRoute( | ||||||
|  |     path: '/account/publishers/edit/:name', | ||||||
|  |     name: 'accountPublisherEdit', | ||||||
|  |     builder: (context, state) => AccountPublisherEditScreen( | ||||||
|  |       name: state.pathParameters['name']!, | ||||||
|  |     ), | ||||||
|  |   ), | ||||||
|   GoRoute( |   GoRoute( | ||||||
|     path: '/account/:name', |     path: '/account/:name', | ||||||
|     name: 'accountProfilePage', |     name: 'accountProfilePage', | ||||||
| @@ -290,29 +257,15 @@ final _appRoutes = [ | |||||||
|       child: UserScreen(name: state.pathParameters['name']!), |       child: UserScreen(name: state.pathParameters['name']!), | ||||||
|     ), |     ), | ||||||
|   ), |   ), | ||||||
|   ShellRoute( |   GoRoute( | ||||||
|     builder: (context, state, child) => AppPageScaffold(body: child), |     path: '/settings', | ||||||
|     routes: [ |     name: 'settings', | ||||||
|       GoRoute( |     builder: (context, state) => SettingsScreen(), | ||||||
|         path: '/settings', |  | ||||||
|         name: 'settings', |  | ||||||
|         builder: (context, state) => const AppBackground( |  | ||||||
|           child: SettingsScreen(), |  | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|     ], |  | ||||||
|   ), |   ), | ||||||
|   ShellRoute( |   GoRoute( | ||||||
|     builder: (context, state, child) => AppPageScaffold(body: child), |     path: '/about', | ||||||
|     routes: [ |     name: 'about', | ||||||
|       GoRoute( |     builder: (context, state) => AboutScreen(), | ||||||
|         path: '/about', |  | ||||||
|         name: 'about', |  | ||||||
|         builder: (context, state) => const AppBackground( |  | ||||||
|           child: AboutScreen(), |  | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|     ], |  | ||||||
|   ), |   ), | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import 'package:provider/provider.dart'; | |||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| import 'package:surface/providers/sn_network.dart'; | import 'package:surface/providers/sn_network.dart'; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
|  | import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||||
|  |  | ||||||
| import '../types/account.dart'; | import '../types/account.dart'; | ||||||
|  |  | ||||||
| @@ -56,7 +57,11 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return Scaffold( |     return AppScaffold( | ||||||
|  |       appBar: AppBar( | ||||||
|  |         leading: const PageBackButton(), | ||||||
|  |         title: Text('screenAbuseReport').tr(), | ||||||
|  |       ), | ||||||
|       body: Column( |       body: Column( | ||||||
|         children: [ |         children: [ | ||||||
|           ListTile( |           ListTile( | ||||||
| @@ -73,6 +78,7 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> { | |||||||
|           else |           else | ||||||
|             Expanded( |             Expanded( | ||||||
|               child: ListView.builder( |               child: ListView.builder( | ||||||
|  |                 padding: EdgeInsets.only(top: 8), | ||||||
|                 itemCount: _reports.length, |                 itemCount: _reports.length, | ||||||
|                 itemBuilder: (context, idx) { |                 itemBuilder: (context, idx) { | ||||||
|                   return ListTile( |                   return ListTile( | ||||||
|   | |||||||
| @@ -12,6 +12,7 @@ import 'package:surface/providers/websocket.dart'; | |||||||
| import 'package:surface/widgets/account/account_image.dart'; | import 'package:surface/widgets/account/account_image.dart'; | ||||||
| import 'package:surface/widgets/app_bar_leading.dart'; | import 'package:surface/widgets/app_bar_leading.dart'; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
|  | import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||||
|  |  | ||||||
| class AccountScreen extends StatelessWidget { | class AccountScreen extends StatelessWidget { | ||||||
|   const AccountScreen({super.key}); |   const AccountScreen({super.key}); | ||||||
| @@ -20,7 +21,7 @@ class AccountScreen extends StatelessWidget { | |||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     final ua = context.watch<UserProvider>(); |     final ua = context.watch<UserProvider>(); | ||||||
|  |  | ||||||
|     return Scaffold( |     return AppScaffold( | ||||||
|       appBar: AppBar( |       appBar: AppBar( | ||||||
|         leading: AutoAppBarLeading(), |         leading: AutoAppBarLeading(), | ||||||
|         title: Text("screenAccount").tr(), |         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/account/account_image.dart'; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
| import 'package:surface/widgets/loading_indicator.dart'; | import 'package:surface/widgets/loading_indicator.dart'; | ||||||
|  | import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||||
| import 'package:surface/widgets/universal_image.dart'; | import 'package:surface/widgets/universal_image.dart'; | ||||||
|  |  | ||||||
| class ProfileEditScreen extends StatefulWidget { | class ProfileEditScreen extends StatefulWidget { | ||||||
| @@ -81,8 +82,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | |||||||
|             onDateTimeChanged: (DateTime newDate) { |             onDateTimeChanged: (DateTime newDate) { | ||||||
|               setState(() { |               setState(() { | ||||||
|                 _birthday = newDate; |                 _birthday = newDate; | ||||||
|                 _birthdayController.text = |                 _birthdayController.text = DateFormat(_kDateFormat).format(_birthday!); | ||||||
|                     DateFormat(_kDateFormat).format(_birthday!); |  | ||||||
|               }); |               }); | ||||||
|             }, |             }, | ||||||
|           ), |           ), | ||||||
| @@ -96,11 +96,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | |||||||
|     if (image == null) return; |     if (image == null) return; | ||||||
|     if (!mounted) return; |     if (!mounted) return; | ||||||
|  |  | ||||||
|     final ImageProvider imageProvider = |     final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); | ||||||
|         kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); |     final aspectRatios = | ||||||
|     final aspectRatios = place == 'banner' |         place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)]; | ||||||
|         ? [CropAspectRatio(width: 16, height: 7)] |  | ||||||
|         : [CropAspectRatio(width: 1, height: 1)]; |  | ||||||
|     final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) |     final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) | ||||||
|         ? await showCupertinoImageCropper( |         ? await showCupertinoImageCropper( | ||||||
|             // ignore: use_build_context_synchronously |             // ignore: use_build_context_synchronously | ||||||
| @@ -122,10 +120,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | |||||||
|  |  | ||||||
|     setState(() => _isBusy = true); |     setState(() => _isBusy = true); | ||||||
|  |  | ||||||
|     final rawBytes = |     final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List(); | ||||||
|         (await result.uiImage.toByteData(format: ImageByteFormat.png))! |  | ||||||
|             .buffer |  | ||||||
|             .asUint8List(); |  | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       final attachment = await attach.directUploadOne( |       final attachment = await attach.directUploadOne( | ||||||
| @@ -212,136 +207,141 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | |||||||
|  |  | ||||||
|     final sn = context.read<SnNetworkProvider>(); |     final sn = context.read<SnNetworkProvider>(); | ||||||
|  |  | ||||||
|     return SingleChildScrollView( |     return AppScaffold( | ||||||
|       child: Column( |       appBar: AppBar( | ||||||
|         crossAxisAlignment: CrossAxisAlignment.start, |         leading: const PageBackButton(), | ||||||
|         children: [ |         title: Text('screenAccountProfileEdit').tr(), | ||||||
|           LoadingIndicator(isActive: _isBusy), |       ), | ||||||
|           const Gap(24), |       body: SingleChildScrollView( | ||||||
|           Stack( |         child: Column( | ||||||
|             clipBehavior: Clip.none, |           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|             children: [ |           children: [ | ||||||
|               Material( |             LoadingIndicator(isActive: _isBusy), | ||||||
|                 elevation: 0, |             const Gap(24), | ||||||
|                 child: InkWell( |             Stack( | ||||||
|                   child: ClipRRect( |               clipBehavior: Clip.none, | ||||||
|                     borderRadius: const BorderRadius.all(Radius.circular(8)), |               children: [ | ||||||
|                     child: AspectRatio( |                 Material( | ||||||
|                       aspectRatio: 16 / 9, |                   elevation: 0, | ||||||
|                       child: Container( |                   child: InkWell( | ||||||
|                         color: |                     child: ClipRRect( | ||||||
|                             Theme.of(context).colorScheme.surfaceContainerHigh, |                       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||||
|                         child: _banner != null |                       child: AspectRatio( | ||||||
|                             ? AutoResizeUniversalImage( |                         aspectRatio: 16 / 9, | ||||||
|                                 sn.getAttachmentUrl(_banner!), |                         child: Container( | ||||||
|                                 fit: BoxFit.cover, |                           color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||||
|                               ) |                           child: _banner != null | ||||||
|                             : const SizedBox.shrink(), |                               ? AutoResizeUniversalImage( | ||||||
|  |                                   sn.getAttachmentUrl(_banner!), | ||||||
|  |                                   fit: BoxFit.cover, | ||||||
|  |                                 ) | ||||||
|  |                               : const SizedBox.shrink(), | ||||||
|  |                         ), | ||||||
|                       ), |                       ), | ||||||
|                     ), |                     ), | ||||||
|                   ), |  | ||||||
|                   onTap: () { |  | ||||||
|                     _updateImage('banner'); |  | ||||||
|                   }, |  | ||||||
|                 ), |  | ||||||
|               ), |  | ||||||
|               Positioned( |  | ||||||
|                 bottom: -28, |  | ||||||
|                 left: 16, |  | ||||||
|                 child: Material( |  | ||||||
|                   elevation: 2, |  | ||||||
|                   borderRadius: const BorderRadius.all(Radius.circular(40)), |  | ||||||
|                   child: InkWell( |  | ||||||
|                     child: AccountImage(content: _avatar, radius: 40), |  | ||||||
|                     onTap: () { |                     onTap: () { | ||||||
|                       _updateImage('avatar'); |                       _updateImage('banner'); | ||||||
|                     }, |                     }, | ||||||
|                   ), |                   ), | ||||||
|                 ), |                 ), | ||||||
|               ), |                 Positioned( | ||||||
|             ], |                   bottom: -28, | ||||||
|           ).padding(horizontal: padding), |                   left: 16, | ||||||
|           const Gap(8 + 28), |                   child: Material( | ||||||
|           Column( |                     elevation: 2, | ||||||
|             children: [ |                     borderRadius: const BorderRadius.all(Radius.circular(40)), | ||||||
|               TextField( |                     child: InkWell( | ||||||
|                 readOnly: true, |                       child: AccountImage(content: _avatar, radius: 40), | ||||||
|                 controller: _usernameController, |                       onTap: () { | ||||||
|                 decoration: InputDecoration( |                         _updateImage('avatar'); | ||||||
|                   border: const UnderlineInputBorder(), |                       }, | ||||||
|                   labelText: 'fieldUsername'.tr(), |  | ||||||
|                   helperText: 'fieldUsernameCannotEditHint'.tr(), |  | ||||||
|                 ), |  | ||||||
|               ), |  | ||||||
|               const Gap(4), |  | ||||||
|               TextField( |  | ||||||
|                 controller: _nicknameController, |  | ||||||
|                 decoration: InputDecoration( |  | ||||||
|                   border: const UnderlineInputBorder(), |  | ||||||
|                   labelText: 'fieldNickname'.tr(), |  | ||||||
|                 ), |  | ||||||
|               ), |  | ||||||
|               const Gap(4), |  | ||||||
|               Row( |  | ||||||
|                 children: [ |  | ||||||
|                   Flexible( |  | ||||||
|                     flex: 1, |  | ||||||
|                     child: TextField( |  | ||||||
|                       controller: _firstNameController, |  | ||||||
|                       decoration: InputDecoration( |  | ||||||
|                         border: const UnderlineInputBorder(), |  | ||||||
|                         labelText: 'fieldFirstName'.tr(), |  | ||||||
|                       ), |  | ||||||
|                     ), |                     ), | ||||||
|                   ), |                   ), | ||||||
|                   const Gap(8), |                 ), | ||||||
|                   Flexible( |               ], | ||||||
|                     flex: 1, |             ).padding(horizontal: padding), | ||||||
|                     child: TextField( |             const Gap(8 + 28), | ||||||
|                       controller: _lastNameController, |             Column( | ||||||
|                       decoration: InputDecoration( |               children: [ | ||||||
|                         border: const UnderlineInputBorder(), |                 TextField( | ||||||
|                         labelText: 'fieldLastName'.tr(), |                   readOnly: true, | ||||||
|  |                   controller: _usernameController, | ||||||
|  |                   decoration: InputDecoration( | ||||||
|  |                     border: const UnderlineInputBorder(), | ||||||
|  |                     labelText: 'fieldUsername'.tr(), | ||||||
|  |                     helperText: 'fieldUsernameCannotEditHint'.tr(), | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |                 const Gap(4), | ||||||
|  |                 TextField( | ||||||
|  |                   controller: _nicknameController, | ||||||
|  |                   decoration: InputDecoration( | ||||||
|  |                     border: const UnderlineInputBorder(), | ||||||
|  |                     labelText: 'fieldNickname'.tr(), | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |                 const Gap(4), | ||||||
|  |                 Row( | ||||||
|  |                   children: [ | ||||||
|  |                     Flexible( | ||||||
|  |                       flex: 1, | ||||||
|  |                       child: TextField( | ||||||
|  |                         controller: _firstNameController, | ||||||
|  |                         decoration: InputDecoration( | ||||||
|  |                           border: const UnderlineInputBorder(), | ||||||
|  |                           labelText: 'fieldFirstName'.tr(), | ||||||
|  |                         ), | ||||||
|                       ), |                       ), | ||||||
|                     ), |                     ), | ||||||
|  |                     const Gap(8), | ||||||
|  |                     Flexible( | ||||||
|  |                       flex: 1, | ||||||
|  |                       child: TextField( | ||||||
|  |                         controller: _lastNameController, | ||||||
|  |                         decoration: InputDecoration( | ||||||
|  |                           border: const UnderlineInputBorder(), | ||||||
|  |                           labelText: 'fieldLastName'.tr(), | ||||||
|  |                         ), | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|  |                   ], | ||||||
|  |                 ), | ||||||
|  |                 const Gap(4), | ||||||
|  |                 TextField( | ||||||
|  |                   controller: _descriptionController, | ||||||
|  |                   keyboardType: TextInputType.multiline, | ||||||
|  |                   maxLines: null, | ||||||
|  |                   minLines: 3, | ||||||
|  |                   decoration: InputDecoration( | ||||||
|  |                     border: const UnderlineInputBorder(), | ||||||
|  |                     labelText: 'fieldDescription'.tr(), | ||||||
|                   ), |                   ), | ||||||
|                 ], |  | ||||||
|               ), |  | ||||||
|               const Gap(4), |  | ||||||
|               TextField( |  | ||||||
|                 controller: _descriptionController, |  | ||||||
|                 keyboardType: TextInputType.multiline, |  | ||||||
|                 maxLines: null, |  | ||||||
|                 minLines: 3, |  | ||||||
|                 decoration: InputDecoration( |  | ||||||
|                   border: const UnderlineInputBorder(), |  | ||||||
|                   labelText: 'fieldDescription'.tr(), |  | ||||||
|                 ), |                 ), | ||||||
|               ), |                 const Gap(4), | ||||||
|               const Gap(4), |                 TextField( | ||||||
|               TextField( |                   controller: _birthdayController, | ||||||
|                 controller: _birthdayController, |                   readOnly: true, | ||||||
|                 readOnly: true, |                   decoration: InputDecoration( | ||||||
|                 decoration: InputDecoration( |                     border: const UnderlineInputBorder(), | ||||||
|                   border: const UnderlineInputBorder(), |                     labelText: 'fieldBirthday'.tr(), | ||||||
|                   labelText: 'fieldBirthday'.tr(), |                   ), | ||||||
|  |                   onTap: () => _selectBirthday(), | ||||||
|                 ), |                 ), | ||||||
|                 onTap: () => _selectBirthday(), |               ], | ||||||
|               ), |             ).padding(horizontal: padding + 8), | ||||||
|             ], |             const Gap(12), | ||||||
|           ).padding(horizontal: padding + 8), |             Row( | ||||||
|           const Gap(12), |               mainAxisAlignment: MainAxisAlignment.end, | ||||||
|           Row( |               children: [ | ||||||
|             mainAxisAlignment: MainAxisAlignment.end, |                 ElevatedButton.icon( | ||||||
|             children: [ |                   onPressed: _isBusy ? null : _updateUserInfo, | ||||||
|               ElevatedButton.icon( |                   icon: const Icon(Symbols.save), | ||||||
|                 onPressed: _isBusy ? null : _updateUserInfo, |                   label: Text('apply').tr(), | ||||||
|                 icon: const Icon(Symbols.save), |                 ), | ||||||
|                 label: Text('apply').tr(), |               ], | ||||||
|               ), |             ).padding(horizontal: padding), | ||||||
|             ], |           ], | ||||||
|           ).padding(horizontal: padding), |         ), | ||||||
|         ], |  | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -241,6 +241,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | |||||||
|     final sn = context.read<SnNetworkProvider>(); |     final sn = context.read<SnNetworkProvider>(); | ||||||
|  |  | ||||||
|     return Scaffold( |     return Scaffold( | ||||||
|  |       backgroundColor: Colors.transparent, | ||||||
|       body: CustomScrollView( |       body: CustomScrollView( | ||||||
|         controller: _scrollController, |         controller: _scrollController, | ||||||
|         slivers: [ |         slivers: [ | ||||||
| @@ -594,7 +595,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | |||||||
|                 subtitle: Text('@${ele.name}'), |                 subtitle: Text('@${ele.name}'), | ||||||
|                 trailing: const Icon(Symbols.chevron_right), |                 trailing: const Icon(Symbols.chevron_right), | ||||||
|                 onTap: () { |                 onTap: () { | ||||||
|                   GoRouter.of(context).pushNamed( |                   GoRouter.of(context).goNamed( | ||||||
|                     'postPublisher', |                     'postPublisher', | ||||||
|                     pathParameters: {'name': ele.name}, |                     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/account/account_image.dart'; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
| import 'package:surface/widgets/loading_indicator.dart'; | import 'package:surface/widgets/loading_indicator.dart'; | ||||||
|  | import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||||
| import 'package:surface/widgets/universal_image.dart'; | import 'package:surface/widgets/universal_image.dart'; | ||||||
|  |  | ||||||
| class AccountPublisherEditScreen extends StatefulWidget { | class AccountPublisherEditScreen extends StatefulWidget { | ||||||
| @@ -176,7 +177,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> | |||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     final sn = context.read<SnNetworkProvider>(); |     final sn = context.read<SnNetworkProvider>(); | ||||||
|  |  | ||||||
|     return Scaffold( |     return AppScaffold( | ||||||
|       body: SingleChildScrollView( |       body: SingleChildScrollView( | ||||||
|         child: Column( |         child: Column( | ||||||
|           children: [ |           children: [ | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ import 'package:surface/providers/userinfo.dart'; | |||||||
| import 'package:surface/types/realm.dart'; | import 'package:surface/types/realm.dart'; | ||||||
| import 'package:surface/widgets/account/account_image.dart'; | import 'package:surface/widgets/account/account_image.dart'; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
|  | import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||||
|  |  | ||||||
| class AccountPublisherNewScreen extends StatefulWidget { | class AccountPublisherNewScreen extends StatefulWidget { | ||||||
|   const AccountPublisherNewScreen({super.key}); |   const AccountPublisherNewScreen({super.key}); | ||||||
| @@ -24,7 +25,11 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return Scaffold( |     return  AppScaffold( | ||||||
|  |       appBar: AppBar( | ||||||
|  |         leading: const PageBackButton(), | ||||||
|  |         title: Text('screenAccountPublisherNew').tr(), | ||||||
|  |       ), | ||||||
|       body: SingleChildScrollView( |       body: SingleChildScrollView( | ||||||
|         child: Column( |         child: Column( | ||||||
|           children: [ |           children: [ | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ import 'package:surface/types/post.dart'; | |||||||
| import 'package:surface/widgets/account/account_image.dart'; | import 'package:surface/widgets/account/account_image.dart'; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
| import 'package:surface/widgets/loading_indicator.dart'; | import 'package:surface/widgets/loading_indicator.dart'; | ||||||
|  | import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||||
|  |  | ||||||
| class PublisherScreen extends StatefulWidget { | class PublisherScreen extends StatefulWidget { | ||||||
|   const PublisherScreen({super.key}); |   const PublisherScreen({super.key}); | ||||||
| @@ -32,8 +33,7 @@ class _PublisherScreenState extends State<PublisherScreen> { | |||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       final resp = await sn.client.get('/cgi/co/publishers/me'); |       final resp = await sn.client.get('/cgi/co/publishers/me'); | ||||||
|       final List<SnPublisher> out = List<SnPublisher>.from( |       final List<SnPublisher> out = List<SnPublisher>.from(resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []); | ||||||
|           resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []); |  | ||||||
|  |  | ||||||
|       if (!mounted) return; |       if (!mounted) return; | ||||||
|  |  | ||||||
| @@ -53,7 +53,11 @@ class _PublisherScreenState extends State<PublisherScreen> { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return Scaffold( |     return AppScaffold( | ||||||
|  |       appBar: AppBar( | ||||||
|  |         leading: const PageBackButton(), | ||||||
|  |         title: Text('screenAccountPublishers').tr(), | ||||||
|  |       ), | ||||||
|       body: Column( |       body: Column( | ||||||
|         children: [ |         children: [ | ||||||
|           ListTile( |           ListTile( | ||||||
| @@ -62,9 +66,7 @@ class _PublisherScreenState extends State<PublisherScreen> { | |||||||
|             contentPadding: const EdgeInsets.symmetric(horizontal: 24), |             contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||||
|             leading: const Icon(Symbols.add_circle), |             leading: const Icon(Symbols.add_circle), | ||||||
|             onTap: () { |             onTap: () { | ||||||
|               GoRouter.of(context) |               GoRouter.of(context).pushNamed('accountPublisherNew').then((value) { | ||||||
|                   .pushNamed('accountPublisherNew') |  | ||||||
|                   .then((value) { |  | ||||||
|                 if (value == true) { |                 if (value == true) { | ||||||
|                   _publishers.clear(); |                   _publishers.clear(); | ||||||
|                   _fetchPublishers(); |                   _fetchPublishers(); | ||||||
| @@ -75,48 +77,52 @@ class _PublisherScreenState extends State<PublisherScreen> { | |||||||
|           const Divider(height: 1), |           const Divider(height: 1), | ||||||
|           LoadingIndicator(isActive: _isBusy), |           LoadingIndicator(isActive: _isBusy), | ||||||
|           Expanded( |           Expanded( | ||||||
|             child: RefreshIndicator( |             child: MediaQuery.removePadding( | ||||||
|               onRefresh: () { |               context: context, | ||||||
|                 _publishers.clear(); |               removeTop: true, | ||||||
|                 return _fetchPublishers(); |               child: RefreshIndicator( | ||||||
|               }, |                 onRefresh: () { | ||||||
|               child: ListView.builder( |                   _publishers.clear(); | ||||||
|                 itemCount: _publishers.length, |                   return _fetchPublishers(); | ||||||
|                 itemBuilder: (context, idx) { |  | ||||||
|                   final publisher = _publishers[idx]; |  | ||||||
|                   return ListTile( |  | ||||||
|                     title: Text(publisher.nick), |  | ||||||
|                     subtitle: Text('@${publisher.name}'), |  | ||||||
|                     contentPadding: const EdgeInsets.symmetric(horizontal: 16), |  | ||||||
|                     leading: AccountImage(content: publisher.avatar), |  | ||||||
|                     trailing: PopupMenuButton( |  | ||||||
|                       itemBuilder: (BuildContext context) => [ |  | ||||||
|                         PopupMenuItem( |  | ||||||
|                           child: Row( |  | ||||||
|                             children: [ |  | ||||||
|                               const Icon(Symbols.edit), |  | ||||||
|                               const Gap(16), |  | ||||||
|                               Text('edit').tr(), |  | ||||||
|                             ], |  | ||||||
|                           ), |  | ||||||
|                           onTap: () { |  | ||||||
|                             GoRouter.of(context).pushNamed( |  | ||||||
|                               'accountPublisherEdit', |  | ||||||
|                               pathParameters: { |  | ||||||
|                                 'name': publisher.name, |  | ||||||
|                               }, |  | ||||||
|                             ).then((value) { |  | ||||||
|                               if (value == true) { |  | ||||||
|                                 _publishers.clear(); |  | ||||||
|                                 _fetchPublishers(); |  | ||||||
|                               } |  | ||||||
|                             }); |  | ||||||
|                           }, |  | ||||||
|                         ), |  | ||||||
|                       ], |  | ||||||
|                     ), |  | ||||||
|                   ); |  | ||||||
|                 }, |                 }, | ||||||
|  |                 child: ListView.builder( | ||||||
|  |                   itemCount: _publishers.length, | ||||||
|  |                   itemBuilder: (context, idx) { | ||||||
|  |                     final publisher = _publishers[idx]; | ||||||
|  |                     return ListTile( | ||||||
|  |                       title: Text(publisher.nick), | ||||||
|  |                       subtitle: Text('@${publisher.name}'), | ||||||
|  |                       contentPadding: const EdgeInsets.symmetric(horizontal: 16), | ||||||
|  |                       leading: AccountImage(content: publisher.avatar), | ||||||
|  |                       trailing: PopupMenuButton( | ||||||
|  |                         itemBuilder: (BuildContext context) => [ | ||||||
|  |                           PopupMenuItem( | ||||||
|  |                             child: Row( | ||||||
|  |                               children: [ | ||||||
|  |                                 const Icon(Symbols.edit), | ||||||
|  |                                 const Gap(16), | ||||||
|  |                                 Text('edit').tr(), | ||||||
|  |                               ], | ||||||
|  |                             ), | ||||||
|  |                             onTap: () { | ||||||
|  |                               GoRouter.of(context).pushNamed( | ||||||
|  |                                 'accountPublisherEdit', | ||||||
|  |                                 pathParameters: { | ||||||
|  |                                   'name': publisher.name, | ||||||
|  |                                 }, | ||||||
|  |                               ).then((value) { | ||||||
|  |                                 if (value == true) { | ||||||
|  |                                   _publishers.clear(); | ||||||
|  |                                   _fetchPublishers(); | ||||||
|  |                                 } | ||||||
|  |                               }); | ||||||
|  |                             }, | ||||||
|  |                           ), | ||||||
|  |                         ], | ||||||
|  |                       ), | ||||||
|  |                     ); | ||||||
|  |                   }, | ||||||
|  |                 ), | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|   | |||||||
| @@ -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_zoom.dart'; | ||||||
| import 'package:surface/widgets/attachment/attachment_item.dart'; | import 'package:surface/widgets/attachment/attachment_item.dart'; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
|  | import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||||
| import 'package:uuid/uuid.dart'; | import 'package:uuid/uuid.dart'; | ||||||
|  |  | ||||||
| class AlbumScreen extends StatefulWidget { | class AlbumScreen extends StatefulWidget { | ||||||
| @@ -82,7 +83,7 @@ class _AlbumScreenState extends State<AlbumScreen> { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return Scaffold( |     return AppScaffold( | ||||||
|       body: CustomScrollView( |       body: CustomScrollView( | ||||||
|         controller: _scrollController, |         controller: _scrollController, | ||||||
|         slivers: [ |         slivers: [ | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import 'package:surface/providers/sn_network.dart'; | |||||||
| import 'package:surface/providers/userinfo.dart'; | import 'package:surface/providers/userinfo.dart'; | ||||||
| import 'package:surface/types/auth.dart'; | import 'package:surface/types/auth.dart'; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
|  | import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||||
| import 'package:url_launcher/url_launcher_string.dart'; | import 'package:url_launcher/url_launcher_string.dart'; | ||||||
|  |  | ||||||
| import '../../providers/websocket.dart'; | import '../../providers/websocket.dart'; | ||||||
| @@ -35,67 +36,73 @@ class _LoginScreenState extends State<LoginScreen> { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return Theme( |     return AppScaffold( | ||||||
|       data: Theme.of(context).copyWith(canvasColor: Colors.transparent), |       appBar: AppBar( | ||||||
|       child: SingleChildScrollView( |         leading: const PageBackButton(), | ||||||
|         child: PageTransitionSwitcher( |         title: Text('screenAuthLogin').tr(), | ||||||
|           transitionBuilder: ( |       ), | ||||||
|             Widget child, |       body: Theme( | ||||||
|             Animation<double> primaryAnimation, |         data: Theme.of(context).copyWith(canvasColor: Colors.transparent), | ||||||
|             Animation<double> secondaryAnimation, |         child: SingleChildScrollView( | ||||||
|           ) { |           child: PageTransitionSwitcher( | ||||||
|             return SharedAxisTransition( |             transitionBuilder: ( | ||||||
|               animation: primaryAnimation, |               Widget child, | ||||||
|               secondaryAnimation: secondaryAnimation, |               Animation<double> primaryAnimation, | ||||||
|               transitionType: SharedAxisTransitionType.horizontal, |               Animation<double> secondaryAnimation, | ||||||
|               child: Container( |             ) { | ||||||
|                 constraints: BoxConstraints(maxWidth: 380), |               return SharedAxisTransition( | ||||||
|                 child: child, |                 animation: primaryAnimation, | ||||||
|               ), |                 secondaryAnimation: secondaryAnimation, | ||||||
|             ); |                 transitionType: SharedAxisTransitionType.horizontal, | ||||||
|           }, |                 child: Container( | ||||||
|           child: switch (_period % 3) { |                   constraints: BoxConstraints(maxWidth: 380), | ||||||
|             1 => _LoginPickerScreen( |                   child: child, | ||||||
|                 key: const ValueKey(1), |                 ), | ||||||
|                 ticket: _currentTicket, |               ); | ||||||
|                 factors: _factors, |             }, | ||||||
|                 onTicket: (p0) => setState(() { |             child: switch (_period % 3) { | ||||||
|                   _currentTicket = p0; |               1 => _LoginPickerScreen( | ||||||
|                 }), |                   key: const ValueKey(1), | ||||||
|                 onPickFactor: (p0) => setState(() { |                   ticket: _currentTicket, | ||||||
|                   _factorPicked = p0; |                   factors: _factors, | ||||||
|                 }), |                   onTicket: (p0) => setState(() { | ||||||
|                 onNext: () => setState(() { |                     _currentTicket = p0; | ||||||
|                   _period++; |                   }), | ||||||
|                 }), |                   onPickFactor: (p0) => setState(() { | ||||||
|               ), |                     _factorPicked = p0; | ||||||
|             2 => _LoginCheckScreen( |                   }), | ||||||
|                 key: const ValueKey(2), |                   onNext: () => setState(() { | ||||||
|                 ticket: _currentTicket, |                     _period++; | ||||||
|                 factor: _factorPicked, |                   }), | ||||||
|                 onTicket: (p0) => setState(() { |                 ), | ||||||
|                   _currentTicket = p0; |               2 => _LoginCheckScreen( | ||||||
|                 }), |                   key: const ValueKey(2), | ||||||
|                 onNext: () => setState(() { |                   ticket: _currentTicket, | ||||||
|                   _period = 1; |                   factor: _factorPicked, | ||||||
|                 }), |                   onTicket: (p0) => setState(() { | ||||||
|               ), |                     _currentTicket = p0; | ||||||
|             _ => _LoginLookupScreen( |                   }), | ||||||
|                 key: const ValueKey(0), |                   onNext: () => setState(() { | ||||||
|                 ticket: _currentTicket, |                     _period = 1; | ||||||
|                 onTicket: (p0) => setState(() { |                   }), | ||||||
|                   _currentTicket = p0; |                 ), | ||||||
|                 }), |               _ => _LoginLookupScreen( | ||||||
|                 onFactor: (p0) => setState(() { |                   key: const ValueKey(0), | ||||||
|                   _factors = p0; |                   ticket: _currentTicket, | ||||||
|                 }), |                   onTicket: (p0) => setState(() { | ||||||
|                 onNext: () => setState(() { |                     _currentTicket = p0; | ||||||
|                   _period++; |                   }), | ||||||
|                 }), |                   onFactor: (p0) => setState(() { | ||||||
|               ), |                     _factors = p0; | ||||||
|           }, |                   }), | ||||||
|         ).padding(all: 24), |                   onNext: () => setState(() { | ||||||
|       ).center(), |                     _period++; | ||||||
|  |                   }), | ||||||
|  |                 ), | ||||||
|  |             }, | ||||||
|  |           ).padding(all: 24), | ||||||
|  |         ).center(), | ||||||
|  |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -441,7 +448,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> { | |||||||
|  |  | ||||||
|       widget.onNext(); |       widget.onNext(); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       if(mounted) context.showErrorDialog(err); |       if (mounted) context.showErrorDialog(err); | ||||||
|       return; |       return; | ||||||
|     } finally { |     } finally { | ||||||
|       setState(() => _isBusy = false); |       setState(() => _isBusy = false); | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import 'package:provider/provider.dart'; | |||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| import 'package:surface/providers/sn_network.dart'; | import 'package:surface/providers/sn_network.dart'; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
|  | import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||||
| import 'package:url_launcher/url_launcher_string.dart'; | import 'package:url_launcher/url_launcher_string.dart'; | ||||||
|  |  | ||||||
| class RegisterScreen extends StatefulWidget { | class RegisterScreen extends StatefulWidget { | ||||||
| @@ -54,175 +55,178 @@ class _RegisterScreenState extends State<RegisterScreen> { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return StyledWidget(Container( |     return AppScaffold( | ||||||
|       constraints: const BoxConstraints(maxWidth: 380), |       appBar: AppBar( | ||||||
|       child: SingleChildScrollView( |         leading: const PageBackButton(), | ||||||
|         child: Column( |         title: Text('screenAuthRegister').tr(), | ||||||
|           crossAxisAlignment: CrossAxisAlignment.start, |       ), | ||||||
|           children: [ |       body: StyledWidget(Container( | ||||||
|             Align( |         constraints: const BoxConstraints(maxWidth: 380), | ||||||
|               alignment: Alignment.centerLeft, |         child: SingleChildScrollView( | ||||||
|               child: CircleAvatar( |           child: Column( | ||||||
|                 radius: 26, |             crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|                 child: const Icon( |             children: [ | ||||||
|                   Symbols.person_add, |               Align( | ||||||
|                   size: 28, |                 alignment: Alignment.centerLeft, | ||||||
|                 ), |                 child: CircleAvatar( | ||||||
|               ).padding(bottom: 8), |                   radius: 26, | ||||||
|             ), |                   child: const Icon( | ||||||
|             Text( |                     Symbols.person_add, | ||||||
|               'screenAuthRegister', |                     size: 28, | ||||||
|               style: const TextStyle( |                   ), | ||||||
|                 fontSize: 28, |                 ).padding(bottom: 8), | ||||||
|                 fontWeight: FontWeight.w900, |  | ||||||
|               ), |               ), | ||||||
|             ).tr().padding(left: 4, bottom: 16), |               Text( | ||||||
|             Form( |                 'screenAuthRegister', | ||||||
|               key: _formKey, |                 style: const TextStyle( | ||||||
|               autovalidateMode: AutovalidateMode.onUserInteraction, |                   fontSize: 28, | ||||||
|               child: Column( |                   fontWeight: FontWeight.w900, | ||||||
|                 children: [ |                 ), | ||||||
|                   TextFormField( |               ).tr().padding(left: 4, bottom: 16), | ||||||
|                     validator: (value) { |               Form( | ||||||
|                       if (value == null || value.length < 4 || value.length > 32) { |                 key: _formKey, | ||||||
|                         return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]); |                 autovalidateMode: AutovalidateMode.onUserInteraction, | ||||||
|                       } |                 child: Column( | ||||||
|                       if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) { |                   children: [ | ||||||
|                         return 'fieldUsernameAlphanumOnly'.tr(); |                     TextFormField( | ||||||
|                       } |                       validator: (value) { | ||||||
|                       return null; |                         if (value == null || value.length < 4 || value.length > 32) { | ||||||
|                     }, |                           return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]); | ||||||
|                     autocorrect: false, |                         } | ||||||
|                     enableSuggestions: false, |                         if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) { | ||||||
|                     controller: _usernameController, |                           return 'fieldUsernameAlphanumOnly'.tr(); | ||||||
|                     autofillHints: const [AutofillHints.username], |                         } | ||||||
|                     decoration: InputDecoration( |                         return null; | ||||||
|                       isDense: true, |                       }, | ||||||
|                       border: const UnderlineInputBorder(), |                       autocorrect: false, | ||||||
|                       labelText: 'fieldUsername'.tr(), |                       enableSuggestions: false, | ||||||
|                     ), |                       controller: _usernameController, | ||||||
|                     onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), |                       autofillHints: const [AutofillHints.username], | ||||||
|                   ), |                       decoration: InputDecoration( | ||||||
|                   const Gap(12), |                         isDense: true, | ||||||
|                   TextFormField( |                         border: const UnderlineInputBorder(), | ||||||
|                     validator: (value) { |                         labelText: 'fieldUsername'.tr(), | ||||||
|                       if (value == null || value.length < 4 || value.length > 32) { |  | ||||||
|                         return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]); |  | ||||||
|                       } |  | ||||||
|                       return null; |  | ||||||
|                     }, |  | ||||||
|                     autocorrect: false, |  | ||||||
|                     enableSuggestions: false, |  | ||||||
|                     controller: _nicknameController, |  | ||||||
|                     autofillHints: const [AutofillHints.nickname], |  | ||||||
|                     decoration: InputDecoration( |  | ||||||
|                       isDense: true, |  | ||||||
|                       border: const UnderlineInputBorder(), |  | ||||||
|                       labelText: 'fieldNickname'.tr(), |  | ||||||
|                     ), |  | ||||||
|                     onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), |  | ||||||
|                   ), |  | ||||||
|                   const Gap(12), |  | ||||||
|                   TextFormField( |  | ||||||
|                     validator: (value) { |  | ||||||
|                       if (value == null || value.isEmpty) { |  | ||||||
|                         return 'fieldCannotBeEmpty'.tr(); |  | ||||||
|                       } |  | ||||||
|                       if (!EmailValidator.validate(value)) { |  | ||||||
|                         return 'fieldEmailAddressMustBeValid'.tr(); |  | ||||||
|                       } |  | ||||||
|                       return null; |  | ||||||
|                     }, |  | ||||||
|                     autocorrect: false, |  | ||||||
|                     enableSuggestions: false, |  | ||||||
|                     controller: _emailController, |  | ||||||
|                     autofillHints: const [AutofillHints.email], |  | ||||||
|                     decoration: InputDecoration( |  | ||||||
|                       isDense: true, |  | ||||||
|                       border: const UnderlineInputBorder(), |  | ||||||
|                       labelText: 'fieldEmail'.tr(), |  | ||||||
|                     ), |  | ||||||
|                     onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), |  | ||||||
|                   ), |  | ||||||
|                   const Gap(12), |  | ||||||
|                   TextFormField( |  | ||||||
|                     validator: (value) { |  | ||||||
|                       if (value == null || value.isEmpty) { |  | ||||||
|                         return 'fieldCannotBeEmpty'.tr(); |  | ||||||
|                       } |  | ||||||
|                       return null; |  | ||||||
|                     }, |  | ||||||
|                     obscureText: true, |  | ||||||
|                     autocorrect: false, |  | ||||||
|                     enableSuggestions: false, |  | ||||||
|                     autofillHints: const [AutofillHints.password], |  | ||||||
|                     controller: _passwordController, |  | ||||||
|                     decoration: InputDecoration( |  | ||||||
|                       isDense: true, |  | ||||||
|                       border: const UnderlineInputBorder(), |  | ||||||
|                       labelText: 'fieldPassword'.tr(), |  | ||||||
|                     ), |  | ||||||
|                     onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), |  | ||||||
|                   ), |  | ||||||
|                 ], |  | ||||||
|               ).padding(horizontal: 7), |  | ||||||
|             ), |  | ||||||
|             const Gap(16), |  | ||||||
|             Align( |  | ||||||
|               alignment: Alignment.centerRight, |  | ||||||
|               child: StyledWidget( |  | ||||||
|                 Container( |  | ||||||
|                   constraints: const BoxConstraints(maxWidth: 290), |  | ||||||
|                   child: Column( |  | ||||||
|                     crossAxisAlignment: CrossAxisAlignment.end, |  | ||||||
|                     children: [ |  | ||||||
|                       Text( |  | ||||||
|                         'termAcceptNextWithAgree'.tr(), |  | ||||||
|                         textAlign: TextAlign.end, |  | ||||||
|                         style: Theme.of(context).textTheme.bodySmall!.copyWith( |  | ||||||
|                           color: Theme.of(context) |  | ||||||
|                               .colorScheme |  | ||||||
|                               .onSurface |  | ||||||
|                               .withAlpha((255 * 0.75).round()), |  | ||||||
|                         ), |  | ||||||
|                       ), |                       ), | ||||||
|                       Material( |                       onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|                         color: Colors.transparent, |                     ), | ||||||
|                         child: InkWell( |                     const Gap(12), | ||||||
|                           child: Row( |                     TextFormField( | ||||||
|                             mainAxisSize: MainAxisSize.min, |                       validator: (value) { | ||||||
|                             children: [ |                         if (value == null || value.length < 4 || value.length > 32) { | ||||||
|                               Text('termAcceptLink'.tr()), |                           return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]); | ||||||
|                               const Gap(4), |                         } | ||||||
|                               const Icon(Symbols.launch, size: 14), |                         return null; | ||||||
|                             ], |                       }, | ||||||
|  |                       autocorrect: false, | ||||||
|  |                       enableSuggestions: false, | ||||||
|  |                       controller: _nicknameController, | ||||||
|  |                       autofillHints: const [AutofillHints.nickname], | ||||||
|  |                       decoration: InputDecoration( | ||||||
|  |                         isDense: true, | ||||||
|  |                         border: const UnderlineInputBorder(), | ||||||
|  |                         labelText: 'fieldNickname'.tr(), | ||||||
|  |                       ), | ||||||
|  |                       onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|  |                     ), | ||||||
|  |                     const Gap(12), | ||||||
|  |                     TextFormField( | ||||||
|  |                       validator: (value) { | ||||||
|  |                         if (value == null || value.isEmpty) { | ||||||
|  |                           return 'fieldCannotBeEmpty'.tr(); | ||||||
|  |                         } | ||||||
|  |                         if (!EmailValidator.validate(value)) { | ||||||
|  |                           return 'fieldEmailAddressMustBeValid'.tr(); | ||||||
|  |                         } | ||||||
|  |                         return null; | ||||||
|  |                       }, | ||||||
|  |                       autocorrect: false, | ||||||
|  |                       enableSuggestions: false, | ||||||
|  |                       controller: _emailController, | ||||||
|  |                       autofillHints: const [AutofillHints.email], | ||||||
|  |                       decoration: InputDecoration( | ||||||
|  |                         isDense: true, | ||||||
|  |                         border: const UnderlineInputBorder(), | ||||||
|  |                         labelText: 'fieldEmail'.tr(), | ||||||
|  |                       ), | ||||||
|  |                       onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|  |                     ), | ||||||
|  |                     const Gap(12), | ||||||
|  |                     TextFormField( | ||||||
|  |                       validator: (value) { | ||||||
|  |                         if (value == null || value.isEmpty) { | ||||||
|  |                           return 'fieldCannotBeEmpty'.tr(); | ||||||
|  |                         } | ||||||
|  |                         return null; | ||||||
|  |                       }, | ||||||
|  |                       obscureText: true, | ||||||
|  |                       autocorrect: false, | ||||||
|  |                       enableSuggestions: false, | ||||||
|  |                       autofillHints: const [AutofillHints.password], | ||||||
|  |                       controller: _passwordController, | ||||||
|  |                       decoration: InputDecoration( | ||||||
|  |                         isDense: true, | ||||||
|  |                         border: const UnderlineInputBorder(), | ||||||
|  |                         labelText: 'fieldPassword'.tr(), | ||||||
|  |                       ), | ||||||
|  |                       onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|  |                     ), | ||||||
|  |                   ], | ||||||
|  |                 ).padding(horizontal: 7), | ||||||
|  |               ), | ||||||
|  |               const Gap(16), | ||||||
|  |               Align( | ||||||
|  |                 alignment: Alignment.centerRight, | ||||||
|  |                 child: StyledWidget( | ||||||
|  |                   Container( | ||||||
|  |                     constraints: const BoxConstraints(maxWidth: 290), | ||||||
|  |                     child: Column( | ||||||
|  |                       crossAxisAlignment: CrossAxisAlignment.end, | ||||||
|  |                       children: [ | ||||||
|  |                         Text( | ||||||
|  |                           'termAcceptNextWithAgree'.tr(), | ||||||
|  |                           textAlign: TextAlign.end, | ||||||
|  |                           style: Theme.of(context).textTheme.bodySmall!.copyWith( | ||||||
|  |                                 color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()), | ||||||
|  |                               ), | ||||||
|  |                         ), | ||||||
|  |                         Material( | ||||||
|  |                           color: Colors.transparent, | ||||||
|  |                           child: InkWell( | ||||||
|  |                             child: Row( | ||||||
|  |                               mainAxisSize: MainAxisSize.min, | ||||||
|  |                               children: [ | ||||||
|  |                                 Text('termAcceptLink'.tr()), | ||||||
|  |                                 const Gap(4), | ||||||
|  |                                 const Icon(Symbols.launch, size: 14), | ||||||
|  |                               ], | ||||||
|  |                             ), | ||||||
|  |                             onTap: () { | ||||||
|  |                               launchUrlString('https://solsynth.dev/terms'); | ||||||
|  |                             }, | ||||||
|                           ), |                           ), | ||||||
|                           onTap: () { |  | ||||||
|                             launchUrlString('https://solsynth.dev/terms'); |  | ||||||
|                           }, |  | ||||||
|                         ), |                         ), | ||||||
|                       ), |                       ], | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 ).padding(horizontal: 16), | ||||||
|  |               ), | ||||||
|  |               Align( | ||||||
|  |                 alignment: Alignment.centerRight, | ||||||
|  |                 child: TextButton( | ||||||
|  |                   onPressed: () => _performAction(context), | ||||||
|  |                   child: Row( | ||||||
|  |                     mainAxisSize: MainAxisSize.min, | ||||||
|  |                     children: [ | ||||||
|  |                       Text('next').tr(), | ||||||
|  |                       const Icon(Symbols.chevron_right), | ||||||
|                     ], |                     ], | ||||||
|                   ), |                   ), | ||||||
|                 ), |                 ), | ||||||
|               ).padding(horizontal: 16), |  | ||||||
|             ), |  | ||||||
|             Align( |  | ||||||
|               alignment: Alignment.centerRight, |  | ||||||
|               child: TextButton( |  | ||||||
|                 onPressed: () => _performAction(context), |  | ||||||
|                 child: Row( |  | ||||||
|                   mainAxisSize: MainAxisSize.min, |  | ||||||
|                   children: [ |  | ||||||
|                     Text('next').tr(), |  | ||||||
|                     const Icon(Symbols.chevron_right), |  | ||||||
|                   ], |  | ||||||
|                 ), |  | ||||||
|               ), |               ), | ||||||
|             ), |             ], | ||||||
|           ], |           ), | ||||||
|         ), |         ), | ||||||
|       ), |       )).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/app_bar_leading.dart'; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
| import 'package:surface/widgets/loading_indicator.dart'; | import 'package:surface/widgets/loading_indicator.dart'; | ||||||
|  | import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||||
| import 'package:surface/widgets/unauthorized_hint.dart'; | import 'package:surface/widgets/unauthorized_hint.dart'; | ||||||
| import 'package:uuid/uuid.dart'; | import 'package:uuid/uuid.dart'; | ||||||
|  |  | ||||||
| @@ -120,7 +121,7 @@ class _ChatScreenState extends State<ChatScreen> { | |||||||
|     final ua = context.read<UserProvider>(); |     final ua = context.read<UserProvider>(); | ||||||
|  |  | ||||||
|     if (!ua.isAuthorized) { |     if (!ua.isAuthorized) { | ||||||
|       return Scaffold( |       return AppScaffold( | ||||||
|         appBar: AppBar( |         appBar: AppBar( | ||||||
|           leading: AutoAppBarLeading(), |           leading: AutoAppBarLeading(), | ||||||
|           title: Text('screenChat').tr(), |           title: Text('screenChat').tr(), | ||||||
| @@ -131,7 +132,7 @@ class _ChatScreenState extends State<ChatScreen> { | |||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return Scaffold( |     return AppScaffold( | ||||||
|       appBar: AppBar( |       appBar: AppBar( | ||||||
|         leading: AutoAppBarLeading(), |         leading: AutoAppBarLeading(), | ||||||
|         title: Text('screenChat').tr(), |         title: Text('screenChat').tr(), | ||||||
| @@ -195,22 +196,58 @@ class _ChatScreenState extends State<ChatScreen> { | |||||||
|         children: [ |         children: [ | ||||||
|           LoadingIndicator(isActive: _isBusy), |           LoadingIndicator(isActive: _isBusy), | ||||||
|           Expanded( |           Expanded( | ||||||
|             child: RefreshIndicator( |             child: MediaQuery.removePadding( | ||||||
|               onRefresh: () => Future.sync(() => _refreshChannels()), |               context: context, | ||||||
|               child: ListView.builder( |               removeTop: true, | ||||||
|                 itemCount: _channels?.length ?? 0, |               child: RefreshIndicator( | ||||||
|                 itemBuilder: (context, idx) { |                 onRefresh: () => Future.sync(() => _refreshChannels()), | ||||||
|                   final channel = _channels![idx]; |                 child: ListView.builder( | ||||||
|                   final lastMessage = _lastMessages?[channel.id]; |                   itemCount: _channels?.length ?? 0, | ||||||
|  |                   itemBuilder: (context, idx) { | ||||||
|  |                     final channel = _channels![idx]; | ||||||
|  |                     final lastMessage = _lastMessages?[channel.id]; | ||||||
|  |  | ||||||
|                   if (channel.type == 1) { |                     if (channel.type == 1) { | ||||||
|                     final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere( |                       final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere( | ||||||
|                           (ele) => ele?.accountId != ua.user?.id, |                             (ele) => ele?.accountId != ua.user?.id, | ||||||
|                           orElse: () => null, |                             orElse: () => null, | ||||||
|                         ); |                           ); | ||||||
|  |  | ||||||
|  |                       return ListTile( | ||||||
|  |                         title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name), | ||||||
|  |                         subtitle: lastMessage != null | ||||||
|  |                             ? Text( | ||||||
|  |                                 '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', | ||||||
|  |                                 maxLines: 1, | ||||||
|  |                                 overflow: TextOverflow.ellipsis, | ||||||
|  |                               ) | ||||||
|  |                             : Text( | ||||||
|  |                                 'channelDirectMessageDescription'.tr(args: [ | ||||||
|  |                                   '@${ud.getAccountFromCache(otherMember?.accountId)?.name}', | ||||||
|  |                                 ]), | ||||||
|  |                                 maxLines: 1, | ||||||
|  |                                 overflow: TextOverflow.ellipsis, | ||||||
|  |                               ), | ||||||
|  |                         contentPadding: const EdgeInsets.symmetric(horizontal: 16), | ||||||
|  |                         leading: AccountImage( | ||||||
|  |                           content: ud.getAccountFromCache(otherMember?.accountId)?.avatar, | ||||||
|  |                         ), | ||||||
|  |                         onTap: () { | ||||||
|  |                           GoRouter.of(context).pushNamed( | ||||||
|  |                             'chatRoom', | ||||||
|  |                             pathParameters: { | ||||||
|  |                               'scope': channel.realm?.alias ?? 'global', | ||||||
|  |                               'alias': channel.alias, | ||||||
|  |                             }, | ||||||
|  |                           ).then((value) { | ||||||
|  |                             if (mounted) _refreshChannels(); | ||||||
|  |                           }); | ||||||
|  |                         }, | ||||||
|  |                       ); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|                     return ListTile( |                     return ListTile( | ||||||
|                       title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name), |                       title: Text(channel.name), | ||||||
|                       subtitle: lastMessage != null |                       subtitle: lastMessage != null | ||||||
|                           ? Text( |                           ? Text( | ||||||
|                               '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', |                               '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', | ||||||
| @@ -218,15 +255,14 @@ class _ChatScreenState extends State<ChatScreen> { | |||||||
|                               overflow: TextOverflow.ellipsis, |                               overflow: TextOverflow.ellipsis, | ||||||
|                             ) |                             ) | ||||||
|                           : Text( |                           : Text( | ||||||
|                               'channelDirectMessageDescription'.tr(args: [ |                               channel.description, | ||||||
|                                 '@${ud.getAccountFromCache(otherMember?.accountId)?.name}', |  | ||||||
|                               ]), |  | ||||||
|                               maxLines: 1, |                               maxLines: 1, | ||||||
|                               overflow: TextOverflow.ellipsis, |                               overflow: TextOverflow.ellipsis, | ||||||
|                             ), |                             ), | ||||||
|                       contentPadding: const EdgeInsets.symmetric(horizontal: 16), |                       contentPadding: const EdgeInsets.symmetric(horizontal: 16), | ||||||
|                       leading: AccountImage( |                       leading: AccountImage( | ||||||
|                         content: ud.getAccountFromCache(otherMember?.accountId)?.avatar, |                         content: null, | ||||||
|  |                         fallbackWidget: const Icon(Symbols.chat, size: 20), | ||||||
|                       ), |                       ), | ||||||
|                       onTap: () { |                       onTap: () { | ||||||
|                         GoRouter.of(context).pushNamed( |                         GoRouter.of(context).pushNamed( | ||||||
| @@ -236,43 +272,12 @@ class _ChatScreenState extends State<ChatScreen> { | |||||||
|                             'alias': channel.alias, |                             'alias': channel.alias, | ||||||
|                           }, |                           }, | ||||||
|                         ).then((value) { |                         ).then((value) { | ||||||
|                           if (mounted) _refreshChannels(); |                           if (value == true) _refreshChannels(); | ||||||
|                         }); |                         }); | ||||||
|                       }, |                       }, | ||||||
|                     ); |                     ); | ||||||
|                   } |                   }, | ||||||
|  |                 ), | ||||||
|                   return ListTile( |  | ||||||
|                     title: Text(channel.name), |  | ||||||
|                     subtitle: lastMessage != null |  | ||||||
|                         ? Text( |  | ||||||
|                             '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', |  | ||||||
|                             maxLines: 1, |  | ||||||
|                             overflow: TextOverflow.ellipsis, |  | ||||||
|                           ) |  | ||||||
|                         : Text( |  | ||||||
|                             channel.description, |  | ||||||
|                             maxLines: 1, |  | ||||||
|                             overflow: TextOverflow.ellipsis, |  | ||||||
|                           ), |  | ||||||
|                     contentPadding: const EdgeInsets.symmetric(horizontal: 16), |  | ||||||
|                     leading: AccountImage( |  | ||||||
|                       content: null, |  | ||||||
|                       fallbackWidget: const Icon(Symbols.chat, size: 20), |  | ||||||
|                     ), |  | ||||||
|                     onTap: () { |  | ||||||
|                       GoRouter.of(context).pushNamed( |  | ||||||
|                         'chatRoom', |  | ||||||
|                         pathParameters: { |  | ||||||
|                           'scope': channel.realm?.alias ?? 'global', |  | ||||||
|                           'alias': channel.alias, |  | ||||||
|                         }, |  | ||||||
|                       ).then((value) { |  | ||||||
|                         if (value == true) _refreshChannels(); |  | ||||||
|                       }); |  | ||||||
|                     }, |  | ||||||
|                   ); |  | ||||||
|                 }, |  | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|   | |||||||
| @@ -9,10 +9,12 @@ import 'package:styled_widget/styled_widget.dart'; | |||||||
| import 'package:surface/providers/chat_call.dart'; | import 'package:surface/providers/chat_call.dart'; | ||||||
| import 'package:surface/widgets/chat/call/call_controls.dart'; | import 'package:surface/widgets/chat/call/call_controls.dart'; | ||||||
| import 'package:surface/widgets/chat/call/call_participant.dart'; | import 'package:surface/widgets/chat/call/call_participant.dart'; | ||||||
|  | import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||||
|  |  | ||||||
| class CallRoomScreen extends StatefulWidget { | class CallRoomScreen extends StatefulWidget { | ||||||
|   final String scope; |   final String scope; | ||||||
|   final String alias; |   final String alias; | ||||||
|  |  | ||||||
|   const CallRoomScreen({super.key, required this.scope, required this.alias}); |   const CallRoomScreen({super.key, required this.scope, required this.alias}); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -35,8 +37,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> { | |||||||
|     return Stack( |     return Stack( | ||||||
|       children: [ |       children: [ | ||||||
|         Container( |         Container( | ||||||
|           color: |           color: Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75), | ||||||
|               Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75), |  | ||||||
|           child: call.focusTrack != null |           child: call.focusTrack != null | ||||||
|               ? InteractiveParticipantWidget( |               ? InteractiveParticipantWidget( | ||||||
|                   isFixedAvatar: false, |                   isFixedAvatar: false, | ||||||
| @@ -71,8 +72,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> { | |||||||
|                       color: Theme.of(context).cardColor, |                       color: Theme.of(context).cardColor, | ||||||
|                       participant: track, |                       participant: track, | ||||||
|                       onTap: () { |                       onTap: () { | ||||||
|                         if (track.participant.sid != |                         if (track.participant.sid != call.focusTrack?.participant.sid) { | ||||||
|                             call.focusTrack?.participant.sid) { |  | ||||||
|                           call.setFocusTrack(track); |                           call.setFocusTrack(track); | ||||||
|                         } |                         } | ||||||
|                       }, |                       }, | ||||||
| @@ -114,14 +114,10 @@ class _CallRoomScreenState extends State<CallRoomScreen> { | |||||||
|             child: ClipRRect( |             child: ClipRRect( | ||||||
|               borderRadius: const BorderRadius.all(Radius.circular(8)), |               borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||||
|               child: InteractiveParticipantWidget( |               child: InteractiveParticipantWidget( | ||||||
|                 color: Theme.of(context) |                 color: Theme.of(context).colorScheme.surfaceContainerHigh.withOpacity(0.75), | ||||||
|                     .colorScheme |  | ||||||
|                     .surfaceContainerHigh |  | ||||||
|                     .withOpacity(0.75), |  | ||||||
|                 participant: track, |                 participant: track, | ||||||
|                 onTap: () { |                 onTap: () { | ||||||
|                   if (track.participant.sid != |                   if (track.participant.sid != call.focusTrack?.participant.sid) { | ||||||
|                       call.focusTrack?.participant.sid) { |  | ||||||
|                     call.setFocusTrack(track); |                     call.setFocusTrack(track); | ||||||
|                   } |                   } | ||||||
|                 }, |                 }, | ||||||
| @@ -152,157 +148,134 @@ class _CallRoomScreenState extends State<CallRoomScreen> { | |||||||
|     return ListenableBuilder( |     return ListenableBuilder( | ||||||
|         listenable: call, |         listenable: call, | ||||||
|         builder: (context, _) { |         builder: (context, _) { | ||||||
|           return Scaffold( |           return AppScaffold( | ||||||
|             appBar: AppBar( |             appBar: AppBar( | ||||||
|               title: RichText( |               title: RichText( | ||||||
|                 textAlign: TextAlign.center, |                 textAlign: TextAlign.center, | ||||||
|                 text: TextSpan(children: [ |                 text: TextSpan(children: [ | ||||||
|                   TextSpan( |                   TextSpan( | ||||||
|                     text: 'call'.tr(), |                     text: 'call'.tr(), | ||||||
|                     style: Theme.of(context) |                     style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.white), | ||||||
|                         .textTheme |  | ||||||
|                         .titleLarge! |  | ||||||
|                         .copyWith(color: Colors.white), |  | ||||||
|                   ), |                   ), | ||||||
|                   const TextSpan(text: '\n'), |                   const TextSpan(text: '\n'), | ||||||
|                   TextSpan( |                   TextSpan( | ||||||
|                     text: call.lastDuration.toString(), |                     text: call.lastDuration.toString(), | ||||||
|                     style: Theme.of(context) |                     style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.white), | ||||||
|                         .textTheme |  | ||||||
|                         .bodySmall! |  | ||||||
|                         .copyWith(color: Colors.white), |  | ||||||
|                   ), |                   ), | ||||||
|                 ]), |                 ]), | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|             body: SafeArea( |             body: GestureDetector( | ||||||
|               child: GestureDetector( |               behavior: HitTestBehavior.translucent, | ||||||
|                 behavior: HitTestBehavior.translucent, |               child: Column( | ||||||
|                 child: Column( |                 children: [ | ||||||
|                   children: [ |                   SizedBox( | ||||||
|  |                     width: MediaQuery.of(context).size.width, | ||||||
|  |                     height: 64, | ||||||
|  |                     child: Row( | ||||||
|  |                       mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||||
|  |                       crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |                       children: [ | ||||||
|  |                         Builder(builder: (context) { | ||||||
|  |                           final call = context.read<ChatCallProvider>(); | ||||||
|  |                           final connectionQuality = | ||||||
|  |                               call.room.localParticipant?.connectionQuality ?? livekit.ConnectionQuality.unknown; | ||||||
|  |                           return Expanded( | ||||||
|  |                             child: Column( | ||||||
|  |                               mainAxisSize: MainAxisSize.min, | ||||||
|  |                               crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                               children: [ | ||||||
|  |                                 Row( | ||||||
|  |                                   children: [ | ||||||
|  |                                     Text( | ||||||
|  |                                       call.channel?.name ?? 'unknown'.tr(), | ||||||
|  |                                       style: const TextStyle( | ||||||
|  |                                         fontWeight: FontWeight.bold, | ||||||
|  |                                       ), | ||||||
|  |                                     ), | ||||||
|  |                                     const Gap(6), | ||||||
|  |                                     Text(call.lastDuration.toString()) | ||||||
|  |                                   ], | ||||||
|  |                                 ), | ||||||
|  |                                 Row( | ||||||
|  |                                   children: [ | ||||||
|  |                                     Text( | ||||||
|  |                                       { | ||||||
|  |                                         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) | ||||||
|  |                                       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, | ||||||
|  |                                         }[connectionQuality], | ||||||
|  |                                         color: { | ||||||
|  |                                           livekit.ConnectionQuality.excellent: Colors.green, | ||||||
|  |                                           livekit.ConnectionQuality.good: Colors.orange, | ||||||
|  |                                           livekit.ConnectionQuality.poor: Colors.red, | ||||||
|  |                                         }[connectionQuality], | ||||||
|  |                                         size: 16, | ||||||
|  |                                       ) | ||||||
|  |                                     else | ||||||
|  |                                       const SizedBox( | ||||||
|  |                                         width: 12, | ||||||
|  |                                         height: 12, | ||||||
|  |                                         child: CircularProgressIndicator( | ||||||
|  |                                           color: Colors.white, | ||||||
|  |                                           strokeWidth: 2, | ||||||
|  |                                         ), | ||||||
|  |                                       ).padding(all: 3), | ||||||
|  |                                   ], | ||||||
|  |                                 ), | ||||||
|  |                               ], | ||||||
|  |                             ), | ||||||
|  |                           ); | ||||||
|  |                         }), | ||||||
|  |                         Row( | ||||||
|  |                           children: [ | ||||||
|  |                             IconButton( | ||||||
|  |                               icon: _layoutMode == 0 ? const Icon(Icons.view_list) : const Icon(Icons.grid_view), | ||||||
|  |                               onPressed: () { | ||||||
|  |                                 _switchLayout(); | ||||||
|  |                               }, | ||||||
|  |                             ), | ||||||
|  |                           ], | ||||||
|  |                         ), | ||||||
|  |                       ], | ||||||
|  |                     ).padding(left: 20, right: 16), | ||||||
|  |                   ), | ||||||
|  |                   Expanded( | ||||||
|  |                     child: Material( | ||||||
|  |                       color: Theme.of(context).colorScheme.surfaceContainerLow, | ||||||
|  |                       child: Builder( | ||||||
|  |                         builder: (context) { | ||||||
|  |                           switch (_layoutMode) { | ||||||
|  |                             case 1: | ||||||
|  |                               return _buildGridLayout(); | ||||||
|  |                             default: | ||||||
|  |                               return _buildListLayout(); | ||||||
|  |                           } | ||||||
|  |                         }, | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                   if (call.room.localParticipant != null) | ||||||
|                     SizedBox( |                     SizedBox( | ||||||
|                       width: MediaQuery.of(context).size.width, |                       width: MediaQuery.of(context).size.width, | ||||||
|                       height: 64, |                       child: ControlsWidget( | ||||||
|                       child: Row( |                         call.room, | ||||||
|                         mainAxisAlignment: MainAxisAlignment.spaceBetween, |                         call.room.localParticipant!, | ||||||
|                         crossAxisAlignment: CrossAxisAlignment.center, |  | ||||||
|                         children: [ |  | ||||||
|                           Builder(builder: (context) { |  | ||||||
|                             final call = context.read<ChatCallProvider>(); |  | ||||||
|                             final connectionQuality = |  | ||||||
|                                 call.room.localParticipant?.connectionQuality ?? |  | ||||||
|                                     livekit.ConnectionQuality.unknown; |  | ||||||
|                             return Expanded( |  | ||||||
|                               child: Column( |  | ||||||
|                                 mainAxisSize: MainAxisSize.min, |  | ||||||
|                                 crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|                                 children: [ |  | ||||||
|                                   Row( |  | ||||||
|                                     children: [ |  | ||||||
|                                       Text( |  | ||||||
|                                         call.channel?.name ?? 'unknown'.tr(), |  | ||||||
|                                         style: const TextStyle( |  | ||||||
|                                           fontWeight: FontWeight.bold, |  | ||||||
|                                         ), |  | ||||||
|                                       ), |  | ||||||
|                                       const Gap(6), |  | ||||||
|                                       Text(call.lastDuration.toString()) |  | ||||||
|                                     ], |  | ||||||
|                                   ), |  | ||||||
|                                   Row( |  | ||||||
|                                     children: [ |  | ||||||
|                                       Text( |  | ||||||
|                                         { |  | ||||||
|                                           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) |  | ||||||
|                                         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, |  | ||||||
|                                           }[connectionQuality], |  | ||||||
|                                           color: { |  | ||||||
|                                             livekit.ConnectionQuality.excellent: |  | ||||||
|                                                 Colors.green, |  | ||||||
|                                             livekit.ConnectionQuality.good: |  | ||||||
|                                                 Colors.orange, |  | ||||||
|                                             livekit.ConnectionQuality.poor: |  | ||||||
|                                                 Colors.red, |  | ||||||
|                                           }[connectionQuality], |  | ||||||
|                                           size: 16, |  | ||||||
|                                         ) |  | ||||||
|                                       else |  | ||||||
|                                         const SizedBox( |  | ||||||
|                                           width: 12, |  | ||||||
|                                           height: 12, |  | ||||||
|                                           child: CircularProgressIndicator( |  | ||||||
|                                             color: Colors.white, |  | ||||||
|                                             strokeWidth: 2, |  | ||||||
|                                           ), |  | ||||||
|                                         ).padding(all: 3), |  | ||||||
|                                     ], |  | ||||||
|                                   ), |  | ||||||
|                                 ], |  | ||||||
|                               ), |  | ||||||
|                             ); |  | ||||||
|                           }), |  | ||||||
|                           Row( |  | ||||||
|                             children: [ |  | ||||||
|                               IconButton( |  | ||||||
|                                 icon: _layoutMode == 0 |  | ||||||
|                                     ? const Icon(Icons.view_list) |  | ||||||
|                                     : const Icon(Icons.grid_view), |  | ||||||
|                                 onPressed: () { |  | ||||||
|                                   _switchLayout(); |  | ||||||
|                                 }, |  | ||||||
|                               ), |  | ||||||
|                             ], |  | ||||||
|                           ), |  | ||||||
|                         ], |  | ||||||
|                       ).padding(left: 20, right: 16), |  | ||||||
|                     ), |  | ||||||
|                     Expanded( |  | ||||||
|                       child: Material( |  | ||||||
|                         color: |  | ||||||
|                             Theme.of(context).colorScheme.surfaceContainerLow, |  | ||||||
|                         child: Builder( |  | ||||||
|                           builder: (context) { |  | ||||||
|                             switch (_layoutMode) { |  | ||||||
|                               case 1: |  | ||||||
|                                 return _buildGridLayout(); |  | ||||||
|                               default: |  | ||||||
|                                 return _buildListLayout(); |  | ||||||
|                             } |  | ||||||
|                           }, |  | ||||||
|                         ), |  | ||||||
|                       ), |                       ), | ||||||
|                     ), |                     ), | ||||||
|                     if (call.room.localParticipant != null) |                 ], | ||||||
|                       SizedBox( |  | ||||||
|                         width: MediaQuery.of(context).size.width, |  | ||||||
|                         child: ControlsWidget( |  | ||||||
|                           call.room, |  | ||||||
|                           call.room.localParticipant!, |  | ||||||
|                         ), |  | ||||||
|                       ), |  | ||||||
|                   ], |  | ||||||
|                 ), |  | ||||||
|                 onTap: () {}, |  | ||||||
|               ), |               ), | ||||||
|  |               onTap: () {}, | ||||||
|             ), |             ), | ||||||
|           ); |           ); | ||||||
|         }); |         }); | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ import 'package:surface/types/chat.dart'; | |||||||
| import 'package:surface/widgets/account/account_image.dart'; | import 'package:surface/widgets/account/account_image.dart'; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
| import 'package:surface/widgets/loading_indicator.dart'; | import 'package:surface/widgets/loading_indicator.dart'; | ||||||
|  | import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||||
|  |  | ||||||
| class ChannelDetailScreen extends StatefulWidget { | class ChannelDetailScreen extends StatefulWidget { | ||||||
| @@ -189,7 +190,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | |||||||
|  |  | ||||||
|     final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id; |     final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id; | ||||||
|  |  | ||||||
|     return Scaffold( |     return AppScaffold( | ||||||
|       appBar: AppBar( |       appBar: AppBar( | ||||||
|         title: _channel != null ? Text(_channel!.name) : Text('loading').tr(), |         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/account/account_image.dart'; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
| import 'package:surface/widgets/loading_indicator.dart'; | import 'package:surface/widgets/loading_indicator.dart'; | ||||||
|  | import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||||
| import 'package:uuid/uuid.dart'; | import 'package:uuid/uuid.dart'; | ||||||
|  |  | ||||||
| class ChatManageScreen extends StatefulWidget { | class ChatManageScreen extends StatefulWidget { | ||||||
| @@ -121,7 +122,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return Scaffold( |     return AppScaffold( | ||||||
|       appBar: AppBar( |       appBar: AppBar( | ||||||
|         title: widget.editingChannelAlias != null |         title: widget.editingChannelAlias != null | ||||||
|             ? Text('screenChatManage').tr() |             ? 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/chat/chat_typing_indicator.dart'; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
| import 'package:surface/widgets/loading_indicator.dart'; | import 'package:surface/widgets/loading_indicator.dart'; | ||||||
|  | import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||||
|  |  | ||||||
| import '../../providers/user_directory.dart'; | import '../../providers/user_directory.dart'; | ||||||
| @@ -211,7 +212,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> { | |||||||
|     final call = context.watch<ChatCallProvider>(); |     final call = context.watch<ChatCallProvider>(); | ||||||
|     final ud = context.read<UserDirectoryProvider>(); |     final ud = context.read<UserDirectoryProvider>(); | ||||||
|  |  | ||||||
|     return Scaffold( |     return AppScaffold( | ||||||
|       appBar: AppBar( |       appBar: AppBar( | ||||||
|         title: Text( |         title: Text( | ||||||
|           _channel?.type == 1 |           _channel?.type == 1 | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ import 'package:surface/screens/post/post_detail.dart'; | |||||||
| import 'package:surface/types/post.dart'; | import 'package:surface/types/post.dart'; | ||||||
| import 'package:surface/widgets/app_bar_leading.dart'; | import 'package:surface/widgets/app_bar_leading.dart'; | ||||||
| import 'package:surface/widgets/dialog.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_item.dart'; | ||||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||||
|  |  | ||||||
| @@ -95,7 +96,7 @@ class _ExploreScreenState extends State<ExploreScreen> { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return Scaffold( |     return AppScaffold( | ||||||
|       floatingActionButtonLocation: ExpandableFab.location, |       floatingActionButtonLocation: ExpandableFab.location, | ||||||
|       floatingActionButton: ExpandableFab( |       floatingActionButton: ExpandableFab( | ||||||
|         key: _fabKey, |         key: _fabKey, | ||||||
| @@ -212,7 +213,7 @@ class _ExploreScreenState extends State<ExploreScreen> { | |||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|             const SliverGap(8), |             const SliverGap(12), | ||||||
|             SliverInfiniteList( |             SliverInfiniteList( | ||||||
|               itemCount: _posts.length, |               itemCount: _posts.length, | ||||||
|               isLoading: _isBusy, |               isLoading: _isBusy, | ||||||
| @@ -242,10 +243,10 @@ class _ExploreScreenState extends State<ExploreScreen> { | |||||||
|                     ), |                     ), | ||||||
|                     openColor: Colors.transparent, |                     openColor: Colors.transparent, | ||||||
|                     openElevation: 0, |                     openElevation: 0, | ||||||
|                     closedColor: Theme.of(context).colorScheme.surface, |                     closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.75), | ||||||
|                     transitionType: ContainerTransitionType.fade, |                     transitionType: ContainerTransitionType.fade, | ||||||
|                     closedShape: const RoundedRectangleBorder( |                     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/app_bar_leading.dart'; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
| import 'package:surface/widgets/loading_indicator.dart'; | import 'package:surface/widgets/loading_indicator.dart'; | ||||||
|  | import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||||
|  |  | ||||||
| import '../providers/userinfo.dart'; | import '../providers/userinfo.dart'; | ||||||
| import '../widgets/unauthorized_hint.dart'; | import '../widgets/unauthorized_hint.dart'; | ||||||
| @@ -180,7 +181,7 @@ class _FriendScreenState extends State<FriendScreen> { | |||||||
|     final ua = context.read<UserProvider>(); |     final ua = context.read<UserProvider>(); | ||||||
|  |  | ||||||
|     if (!ua.isAuthorized) { |     if (!ua.isAuthorized) { | ||||||
|       return Scaffold( |       return AppScaffold( | ||||||
|         appBar: AppBar( |         appBar: AppBar( | ||||||
|           leading: AutoAppBarLeading(), |           leading: AutoAppBarLeading(), | ||||||
|           title: Text('screenFriend').tr(), |           title: Text('screenFriend').tr(), | ||||||
| @@ -191,7 +192,7 @@ class _FriendScreenState extends State<FriendScreen> { | |||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return Scaffold( |     return AppScaffold( | ||||||
|       appBar: AppBar( |       appBar: AppBar( | ||||||
|         leading: AutoAppBarLeading(), |         leading: AutoAppBarLeading(), | ||||||
|         title: Text('screenFriend').tr(), |         title: Text('screenFriend').tr(), | ||||||
| @@ -233,52 +234,56 @@ class _FriendScreenState extends State<FriendScreen> { | |||||||
|           if (_requests.isNotEmpty || _blocks.isNotEmpty) |           if (_requests.isNotEmpty || _blocks.isNotEmpty) | ||||||
|             const Divider(height: 1), |             const Divider(height: 1), | ||||||
|           Expanded( |           Expanded( | ||||||
|             child: RefreshIndicator( |             child: MediaQuery.removePadding( | ||||||
|               onRefresh: () => Future.wait([ |               context: context, | ||||||
|                 _fetchRelations(), |               removeTop: true, | ||||||
|                 _fetchRequests(), |               child: RefreshIndicator( | ||||||
|               ]), |                 onRefresh: () => Future.wait([ | ||||||
|               child: ListView.builder( |                   _fetchRelations(), | ||||||
|                 itemCount: _relations.length, |                   _fetchRequests(), | ||||||
|                 itemBuilder: (context, index) { |                 ]), | ||||||
|                   final relation = _relations[index]; |                 child: ListView.builder( | ||||||
|                   final other = relation.related; |                   itemCount: _relations.length, | ||||||
|                   return ListTile( |                   itemBuilder: (context, index) { | ||||||
|                     contentPadding: const EdgeInsets.only(right: 24, left: 16), |                     final relation = _relations[index]; | ||||||
|                     leading: AccountImage(content: other?.avatar), |                     final other = relation.related; | ||||||
|                     title: Text(other?.nick ?? 'unknown'), |                     return ListTile( | ||||||
|                     subtitle: Text(other?.nick ?? 'unknown'), |                       contentPadding: const EdgeInsets.only(right: 24, left: 16), | ||||||
|                     trailing: SizedBox( |                       leading: AccountImage(content: other?.avatar), | ||||||
|                       height: 48, |                       title: Text(other?.nick ?? 'unknown'), | ||||||
|                       width: 120, |                       subtitle: Text(other?.nick ?? 'unknown'), | ||||||
|                       child: Column( |                       trailing: SizedBox( | ||||||
|                         mainAxisSize: MainAxisSize.min, |                         height: 48, | ||||||
|                         mainAxisAlignment: MainAxisAlignment.center, |                         width: 120, | ||||||
|                         crossAxisAlignment: CrossAxisAlignment.end, |                         child: Column( | ||||||
|                         children: [ |                           mainAxisSize: MainAxisSize.min, | ||||||
|                           Row( |                           mainAxisAlignment: MainAxisAlignment.center, | ||||||
|                             mainAxisAlignment: MainAxisAlignment.end, |                           crossAxisAlignment: CrossAxisAlignment.end, | ||||||
|                             children: [ |                           children: [ | ||||||
|                               InkWell( |                             Row( | ||||||
|                                 onTap: _isUpdating |                               mainAxisAlignment: MainAxisAlignment.end, | ||||||
|                                     ? null |                               children: [ | ||||||
|                                     : () => _changeRelation(relation, 2), |                                 InkWell( | ||||||
|                                 child: Text('friendBlock').tr(), |                                   onTap: _isUpdating | ||||||
|                               ), |                                       ? null | ||||||
|                               const Gap(8), |                                       : () => _changeRelation(relation, 2), | ||||||
|                               InkWell( |                                   child: Text('friendBlock').tr(), | ||||||
|                                 onTap: _isUpdating |                                 ), | ||||||
|                                     ? null |                                 const Gap(8), | ||||||
|                                     : () => _deleteRelation(relation), |                                 InkWell( | ||||||
|                                 child: Text('friendDeleteAction').tr(), |                                   onTap: _isUpdating | ||||||
|                               ), |                                       ? null | ||||||
|                             ], |                                       : () => _deleteRelation(relation), | ||||||
|                           ), |                                   child: Text('friendDeleteAction').tr(), | ||||||
|                         ], |                                 ), | ||||||
|  |                               ], | ||||||
|  |                             ), | ||||||
|  |                           ], | ||||||
|  |                         ), | ||||||
|                       ), |                       ), | ||||||
|                     ), |                     ); | ||||||
|                   ); |                   }, | ||||||
|                 }, |                 ), | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|   | |||||||
| @@ -25,6 +25,7 @@ import 'package:surface/types/check_in.dart'; | |||||||
| import 'package:surface/types/post.dart'; | import 'package:surface/types/post.dart'; | ||||||
| import 'package:surface/widgets/app_bar_leading.dart'; | import 'package:surface/widgets/app_bar_leading.dart'; | ||||||
| import 'package:surface/widgets/dialog.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_item.dart'; | ||||||
|  |  | ||||||
| class HomeScreenDashEntry { | class HomeScreenDashEntry { | ||||||
| @@ -67,7 +68,7 @@ class _HomeScreenState extends State<HomeScreen> { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return Scaffold( |     return AppScaffold( | ||||||
|       appBar: AppBar( |       appBar: AppBar( | ||||||
|         leading: AutoAppBarLeading(), |         leading: AutoAppBarLeading(), | ||||||
|         title: Text("screenHome").tr(), |         title: Text("screenHome").tr(), | ||||||
| @@ -387,6 +388,8 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> { | |||||||
|                         Text( |                         Text( | ||||||
|                           'dailyCheckInNone', |                           'dailyCheckInNone', | ||||||
|                           style: Theme.of(context).textTheme.bodyLarge, |                           style: Theme.of(context).textTheme.bodyLarge, | ||||||
|  |                           maxLines: 2, | ||||||
|  |                           overflow: TextOverflow.ellipsis, | ||||||
|                         ).tr(), |                         ).tr(), | ||||||
|                       ], |                       ], | ||||||
|                     ) |                     ) | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ import 'package:surface/widgets/app_bar_leading.dart'; | |||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
| import 'package:surface/widgets/loading_indicator.dart'; | import 'package:surface/widgets/loading_indicator.dart'; | ||||||
| import 'package:surface/widgets/markdown_content.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:surface/widgets/post/post_item.dart'; | ||||||
| import 'package:very_good_infinite_list/very_good_infinite_list.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>(); |     final ua = context.read<UserProvider>(); | ||||||
|  |  | ||||||
|     if (!ua.isAuthorized) { |     if (!ua.isAuthorized) { | ||||||
|       return Scaffold( |       return AppScaffold( | ||||||
|         appBar: AppBar( |         appBar: AppBar( | ||||||
|           leading: AutoAppBarLeading(), |           leading: AutoAppBarLeading(), | ||||||
|           title: Text('screenNotification').tr(), |           title: Text('screenNotification').tr(), | ||||||
| @@ -148,7 +149,7 @@ class _NotificationScreenState extends State<NotificationScreen> { | |||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return Scaffold( |     return AppScaffold( | ||||||
|       appBar: AppBar( |       appBar: AppBar( | ||||||
|         leading: AutoAppBarLeading(), |         leading: AutoAppBarLeading(), | ||||||
|         title: Text('screenNotification').tr(), |         title: Text('screenNotification').tr(), | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ import 'package:surface/types/post.dart'; | |||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
| import 'package:surface/widgets/loading_indicator.dart'; | import 'package:surface/widgets/loading_indicator.dart'; | ||||||
| import 'package:surface/widgets/navigation/app_background.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_comment_list.dart'; | ||||||
| import 'package:surface/widgets/post/post_item.dart'; | import 'package:surface/widgets/post/post_item.dart'; | ||||||
| import 'package:surface/widgets/post/post_mini_editor.dart'; | import 'package:surface/widgets/post/post_mini_editor.dart'; | ||||||
| @@ -67,7 +68,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> { | |||||||
|  |  | ||||||
|     return AppBackground( |     return AppBackground( | ||||||
|       isRoot: widget.onBack != null, |       isRoot: widget.onBack != null, | ||||||
|       child: Scaffold( |       child: AppScaffold( | ||||||
|         appBar: AppBar( |         appBar: AppBar( | ||||||
|           leading: BackButton( |           leading: BackButton( | ||||||
|             onPressed: () { |             onPressed: () { | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ import 'package:surface/providers/sn_network.dart'; | |||||||
| import 'package:surface/types/post.dart'; | import 'package:surface/types/post.dart'; | ||||||
| import 'package:surface/widgets/account/account_image.dart'; | import 'package:surface/widgets/account/account_image.dart'; | ||||||
| import 'package:surface/widgets/loading_indicator.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_item.dart'; | ||||||
| import 'package:surface/widgets/post/post_media_pending_list.dart'; | import 'package:surface/widgets/post/post_media_pending_list.dart'; | ||||||
| import 'package:surface/widgets/post/post_meta_editor.dart'; | import 'package:surface/widgets/post/post_meta_editor.dart'; | ||||||
| @@ -128,7 +129,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | |||||||
|     return ListenableBuilder( |     return ListenableBuilder( | ||||||
|       listenable: _writeController, |       listenable: _writeController, | ||||||
|       builder: (context, _) { |       builder: (context, _) { | ||||||
|         return Scaffold( |         return AppScaffold( | ||||||
|           appBar: AppBar( |           appBar: AppBar( | ||||||
|             leading: BackButton( |             leading: BackButton( | ||||||
|               onPressed: () { |               onPressed: () { | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import 'package:styled_widget/styled_widget.dart'; | |||||||
| import 'package:surface/providers/post.dart'; | import 'package:surface/providers/post.dart'; | ||||||
| import 'package:surface/types/post.dart'; | import 'package:surface/types/post.dart'; | ||||||
| import 'package:surface/widgets/dialog.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_item.dart'; | ||||||
| import 'package:surface/widgets/post/post_tags_field.dart'; | import 'package:surface/widgets/post/post_tags_field.dart'; | ||||||
| import 'package:very_good_infinite_list/very_good_infinite_list.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( |       appBar: AppBar( | ||||||
|         title: Text('screenPostSearch').tr(), |         title: Text('screenPostSearch').tr(), | ||||||
|         actions: [ |         actions: [ | ||||||
|   | |||||||
| @@ -17,6 +17,7 @@ import 'package:surface/types/post.dart'; | |||||||
| import 'package:surface/types/realm.dart'; | import 'package:surface/types/realm.dart'; | ||||||
| import 'package:surface/widgets/account/account_image.dart'; | import 'package:surface/widgets/account/account_image.dart'; | ||||||
| import 'package:surface/widgets/dialog.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_item.dart'; | ||||||
| import 'package:surface/widgets/universal_image.dart'; | import 'package:surface/widgets/universal_image.dart'; | ||||||
| import 'package:very_good_infinite_list/very_good_infinite_list.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>(); |     final sn = context.read<SnNetworkProvider>(); | ||||||
|  |  | ||||||
|     return Scaffold( |     return AppScaffold( | ||||||
|       body: NestedScrollView( |       body: NestedScrollView( | ||||||
|         controller: _scrollController, |         controller: _scrollController, | ||||||
|         headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { |         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/app_bar_leading.dart'; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
| import 'package:surface/widgets/loading_indicator.dart'; | import 'package:surface/widgets/loading_indicator.dart'; | ||||||
|  | import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||||
| import 'package:surface/widgets/unauthorized_hint.dart'; | import 'package:surface/widgets/unauthorized_hint.dart'; | ||||||
| import 'package:surface/widgets/universal_image.dart'; | import 'package:surface/widgets/universal_image.dart'; | ||||||
|  |  | ||||||
| @@ -83,7 +84,7 @@ class _RealmScreenState extends State<RealmScreen> { | |||||||
|     final ua = context.read<UserProvider>(); |     final ua = context.read<UserProvider>(); | ||||||
|  |  | ||||||
|     if (!ua.isAuthorized) { |     if (!ua.isAuthorized) { | ||||||
|       return Scaffold( |       return AppScaffold( | ||||||
|         appBar: AppBar( |         appBar: AppBar( | ||||||
|           leading: AutoAppBarLeading(), |           leading: AutoAppBarLeading(), | ||||||
|           title: Text('screenRealm').tr(), |           title: Text('screenRealm').tr(), | ||||||
| @@ -94,7 +95,7 @@ class _RealmScreenState extends State<RealmScreen> { | |||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return Scaffold( |     return AppScaffold( | ||||||
|       appBar: AppBar( |       appBar: AppBar( | ||||||
|         leading: AutoAppBarLeading(), |         leading: AutoAppBarLeading(), | ||||||
|         title: Text('screenRealm').tr(), |         title: Text('screenRealm').tr(), | ||||||
| @@ -118,113 +119,61 @@ class _RealmScreenState extends State<RealmScreen> { | |||||||
|         children: [ |         children: [ | ||||||
|           LoadingIndicator(isActive: _isBusy), |           LoadingIndicator(isActive: _isBusy), | ||||||
|           Expanded( |           Expanded( | ||||||
|             child: RefreshIndicator( |             child: MediaQuery.removePadding( | ||||||
|               onRefresh: _fetchRealms, |               context: context, | ||||||
|               child: ListView.builder( |               removeTop: true, | ||||||
|                 itemCount: _realms?.length ?? 0, |               child: RefreshIndicator( | ||||||
|                 itemBuilder: (context, idx) { |                 onRefresh: _fetchRealms, | ||||||
|                   final realm = _realms![idx]; |                 child: ListView.builder( | ||||||
|                   if (_isCompactView) { |                   itemCount: _realms?.length ?? 0, | ||||||
|                     return ListTile( |                   itemBuilder: (context, idx) { | ||||||
|                       contentPadding: const EdgeInsets.symmetric(horizontal: 16), |                     final realm = _realms![idx]; | ||||||
|                       leading: AccountImage( |                     if (_isCompactView) { | ||||||
|                         content: realm.avatar, |                       return ListTile( | ||||||
|                         fallbackWidget: const Icon(Symbols.group, size: 20), |                         contentPadding: const EdgeInsets.symmetric(horizontal: 16), | ||||||
|                       ), |                         leading: AccountImage( | ||||||
|                       title: Text(realm.name), |                           content: realm.avatar, | ||||||
|                       subtitle: Text( |                           fallbackWidget: const Icon(Symbols.group, size: 20), | ||||||
|                         realm.description, |                         ), | ||||||
|                         maxLines: 1, |                         title: Text(realm.name), | ||||||
|                         overflow: TextOverflow.ellipsis, |                         subtitle: Text( | ||||||
|                       ), |                           realm.description, | ||||||
|                       trailing: PopupMenuButton( |                           maxLines: 1, | ||||||
|                         itemBuilder: (BuildContext context) => [ |                           overflow: TextOverflow.ellipsis, | ||||||
|                           PopupMenuItem( |                         ), | ||||||
|                             child: Row( |                         trailing: PopupMenuButton( | ||||||
|                               children: [ |                           itemBuilder: (BuildContext context) => [ | ||||||
|                                 const Icon(Symbols.edit), |                             PopupMenuItem( | ||||||
|                                 const Gap(16), |                               child: Row( | ||||||
|                                 Text('edit').tr(), |  | ||||||
|                               ], |  | ||||||
|                             ), |  | ||||||
|                             onTap: () { |  | ||||||
|                               GoRouter.of(context).pushNamed( |  | ||||||
|                                 'realmManage', |  | ||||||
|                                 queryParameters: {'editing': realm.alias}, |  | ||||||
|                               ).then((value) { |  | ||||||
|                                 if (value != null) { |  | ||||||
|                                   _fetchRealms(); |  | ||||||
|                                 } |  | ||||||
|                               }); |  | ||||||
|                             }, |  | ||||||
|                           ), |  | ||||||
|                           PopupMenuItem( |  | ||||||
|                             child: Row( |  | ||||||
|                               children: [ |  | ||||||
|                                 const Icon(Symbols.delete), |  | ||||||
|                                 const Gap(16), |  | ||||||
|                                 Text('delete').tr(), |  | ||||||
|                               ], |  | ||||||
|                             ), |  | ||||||
|                             onTap: () { |  | ||||||
|                               _deleteRealm(realm); |  | ||||||
|                             }, |  | ||||||
|                           ), |  | ||||||
|                         ], |  | ||||||
|                       ), |  | ||||||
|                       onTap: () { |  | ||||||
|                         GoRouter.of(context).pushNamed( |  | ||||||
|                           'realmDetail', |  | ||||||
|                           pathParameters: {'alias': realm.alias}, |  | ||||||
|                         ); |  | ||||||
|                       }, |  | ||||||
|                     ); |  | ||||||
|                   } |  | ||||||
|  |  | ||||||
|                   return Container( |  | ||||||
|                     constraints: BoxConstraints(maxWidth: 640), |  | ||||||
|                     child: Card( |  | ||||||
|                       margin: const EdgeInsets.all(12), |  | ||||||
|                       child: InkWell( |  | ||||||
|                         borderRadius: const BorderRadius.all(Radius.circular(8)), |  | ||||||
|                         child: Column( |  | ||||||
|                           crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|                           children: [ |  | ||||||
|                             AspectRatio( |  | ||||||
|                               aspectRatio: 16 / 7, |  | ||||||
|                               child: Stack( |  | ||||||
|                                 clipBehavior: Clip.none, |  | ||||||
|                                 fit: StackFit.expand, |  | ||||||
|                                 children: [ |                                 children: [ | ||||||
|                                   Container( |                                   const Icon(Symbols.edit), | ||||||
|                                     color: Theme.of(context).colorScheme.surfaceContainer, |                                   const Gap(16), | ||||||
|                                     child: (realm.banner?.isEmpty ?? true) |                                   Text('edit').tr(), | ||||||
|                                         ? const SizedBox.shrink() |  | ||||||
|                                         : AutoResizeUniversalImage( |  | ||||||
|                                             sn.getAttachmentUrl(realm.banner!), |  | ||||||
|                                             fit: BoxFit.cover, |  | ||||||
|                                           ), |  | ||||||
|                                   ), |  | ||||||
|                                   Positioned( |  | ||||||
|                                     bottom: -30, |  | ||||||
|                                     left: 18, |  | ||||||
|                                     child: AccountImage( |  | ||||||
|                                       content: realm.avatar, |  | ||||||
|                                       radius: 24, |  | ||||||
|                                       fallbackWidget: const Icon(Symbols.group, size: 24), |  | ||||||
|                                     ), |  | ||||||
|                                   ), |  | ||||||
|                                 ], |                                 ], | ||||||
|                               ), |                               ), | ||||||
|  |                               onTap: () { | ||||||
|  |                                 GoRouter.of(context).pushNamed( | ||||||
|  |                                   'realmManage', | ||||||
|  |                                   queryParameters: {'editing': realm.alias}, | ||||||
|  |                                 ).then((value) { | ||||||
|  |                                   if (value != null) { | ||||||
|  |                                     _fetchRealms(); | ||||||
|  |                                   } | ||||||
|  |                                 }); | ||||||
|  |                               }, | ||||||
|  |                             ), | ||||||
|  |                             PopupMenuItem( | ||||||
|  |                               child: Row( | ||||||
|  |                                 children: [ | ||||||
|  |                                   const Icon(Symbols.delete), | ||||||
|  |                                   const Gap(16), | ||||||
|  |                                   Text('delete').tr(), | ||||||
|  |                                 ], | ||||||
|  |                               ), | ||||||
|  |                               onTap: () { | ||||||
|  |                                 _deleteRealm(realm); | ||||||
|  |                               }, | ||||||
|                             ), |                             ), | ||||||
|                             const Gap(20 + 12), |  | ||||||
|                             Column( |  | ||||||
|                               crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|                               children: [ |  | ||||||
|                                 Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!), |  | ||||||
|                                 Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!), |  | ||||||
|                               ], |  | ||||||
|                             ).padding(horizontal: 24, bottom: 14), |  | ||||||
|                           ], |                           ], | ||||||
|                         ), |                         ), | ||||||
|                         onTap: () { |                         onTap: () { | ||||||
| @@ -233,10 +182,69 @@ class _RealmScreenState extends State<RealmScreen> { | |||||||
|                             pathParameters: {'alias': realm.alias}, |                             pathParameters: {'alias': realm.alias}, | ||||||
|                           ); |                           ); | ||||||
|                         }, |                         }, | ||||||
|  |                       ); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     return Container( | ||||||
|  |                       constraints: BoxConstraints(maxWidth: 640), | ||||||
|  |                       child: Card( | ||||||
|  |                         margin: const EdgeInsets.all(12), | ||||||
|  |                         child: InkWell( | ||||||
|  |                           borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||||
|  |                           child: Column( | ||||||
|  |                             crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                             children: [ | ||||||
|  |                               AspectRatio( | ||||||
|  |                                 aspectRatio: 16 / 7, | ||||||
|  |                                 child: Stack( | ||||||
|  |                                   clipBehavior: Clip.none, | ||||||
|  |                                   fit: StackFit.expand, | ||||||
|  |                                   children: [ | ||||||
|  |                                     ClipRRect( | ||||||
|  |                                       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||||
|  |                                       child: Container( | ||||||
|  |                                         color: Theme.of(context).colorScheme.surfaceContainer, | ||||||
|  |                                         child: (realm.banner?.isEmpty ?? true) | ||||||
|  |                                             ? const SizedBox.shrink() | ||||||
|  |                                             : AutoResizeUniversalImage( | ||||||
|  |                                                 sn.getAttachmentUrl(realm.banner!), | ||||||
|  |                                                 fit: BoxFit.cover, | ||||||
|  |                                               ), | ||||||
|  |                                       ), | ||||||
|  |                                     ), | ||||||
|  |                                     Positioned( | ||||||
|  |                                       bottom: -30, | ||||||
|  |                                       left: 18, | ||||||
|  |                                       child: AccountImage( | ||||||
|  |                                         content: realm.avatar, | ||||||
|  |                                         radius: 24, | ||||||
|  |                                         fallbackWidget: const Icon(Symbols.group, size: 24), | ||||||
|  |                                       ), | ||||||
|  |                                     ), | ||||||
|  |                                   ], | ||||||
|  |                                 ), | ||||||
|  |                               ), | ||||||
|  |                               const Gap(20 + 12), | ||||||
|  |                               Column( | ||||||
|  |                                 crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                                 children: [ | ||||||
|  |                                   Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!), | ||||||
|  |                                   Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!), | ||||||
|  |                                 ], | ||||||
|  |                               ).padding(horizontal: 24, bottom: 14), | ||||||
|  |                             ], | ||||||
|  |                           ), | ||||||
|  |                           onTap: () { | ||||||
|  |                             GoRouter.of(context).pushNamed( | ||||||
|  |                               'realmDetail', | ||||||
|  |                               pathParameters: {'alias': realm.alias}, | ||||||
|  |                             ); | ||||||
|  |                           }, | ||||||
|  |                         ), | ||||||
|                       ), |                       ), | ||||||
|                     ), |                     ).center(); | ||||||
|                   ).center(); |                   }, | ||||||
|                 }, |                 ), | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|   | |||||||
| @@ -18,6 +18,7 @@ import 'package:surface/types/realm.dart'; | |||||||
| import 'package:surface/widgets/account/account_image.dart'; | import 'package:surface/widgets/account/account_image.dart'; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
| import 'package:surface/widgets/loading_indicator.dart'; | import 'package:surface/widgets/loading_indicator.dart'; | ||||||
|  | import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||||
| import 'package:surface/widgets/universal_image.dart'; | import 'package:surface/widgets/universal_image.dart'; | ||||||
| import 'package:uuid/uuid.dart'; | import 'package:uuid/uuid.dart'; | ||||||
|  |  | ||||||
| @@ -179,7 +180,7 @@ class _RealmManageScreenState extends State<RealmManageScreen> { | |||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     final sn = context.read<SnNetworkProvider>(); |     final sn = context.read<SnNetworkProvider>(); | ||||||
|  |  | ||||||
|     return Scaffold( |     return AppScaffold( | ||||||
|       appBar: AppBar( |       appBar: AppBar( | ||||||
|         title: widget.editingRealmAlias != null |         title: widget.editingRealmAlias != null | ||||||
|             ? Text('screenRealmManage').tr() |             ? 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/sn_network.dart'; | ||||||
| import 'package:surface/providers/user_directory.dart'; | import 'package:surface/providers/user_directory.dart'; | ||||||
| import 'package:surface/providers/userinfo.dart'; | import 'package:surface/providers/userinfo.dart'; | ||||||
|  | import 'package:surface/types/post.dart'; | ||||||
| import 'package:surface/types/realm.dart'; | import 'package:surface/types/realm.dart'; | ||||||
| import 'package:surface/widgets/account/account_image.dart'; | import 'package:surface/widgets/account/account_image.dart'; | ||||||
| import 'package:surface/widgets/dialog.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 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||||
|  |  | ||||||
| import '../../types/post.dart'; |  | ||||||
|  |  | ||||||
| class RealmDetailScreen extends StatefulWidget { | class RealmDetailScreen extends StatefulWidget { | ||||||
|   final String alias; |   final String alias; | ||||||
|  |  | ||||||
| @@ -70,19 +70,11 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> { | |||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return DefaultTabController( |     return DefaultTabController( | ||||||
|       length: 3, |       length: 3, | ||||||
|       child: Scaffold( |       child: AppScaffold( | ||||||
|         body: NestedScrollView( |         body: NestedScrollView( | ||||||
|           headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { |           headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { | ||||||
|             // These are the slivers that show up in the "outer" scroll view. |  | ||||||
|             return <Widget>[ |             return <Widget>[ | ||||||
|               SliverOverlapAbsorber( |               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), |                 handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), | ||||||
|                 sliver: SliverAppBar( |                 sliver: SliverAppBar( | ||||||
|                   title: Text(_realm?.name ?? 'loading'.tr()), |                   title: Text(_realm?.name ?? 'loading'.tr()), | ||||||
| @@ -428,7 +420,7 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> { | |||||||
|  |  | ||||||
|     return Column( |     return Column( | ||||||
|       children: [ |       children: [ | ||||||
|         const Gap(16), |         const Gap(8), | ||||||
|         ListTile( |         ListTile( | ||||||
|           leading: const Icon(Symbols.edit), |           leading: const Icon(Symbols.edit), | ||||||
|           trailing: const Icon(Symbols.chevron_right), |           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/providers/theme.dart'; | ||||||
| import 'package:surface/theme.dart'; | import 'package:surface/theme.dart'; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
|  | import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||||
|  |  | ||||||
| const Map<String, Color> kColorSchemes = { | const Map<String, Color> kColorSchemes = { | ||||||
|   'colorSchemeIndigo': Colors.indigo, |   'colorSchemeIndigo': Colors.indigo, | ||||||
| @@ -67,7 +68,11 @@ class _SettingsScreenState extends State<SettingsScreen> { | |||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     final sn = context.read<SnNetworkProvider>(); |     final sn = context.read<SnNetworkProvider>(); | ||||||
|  |  | ||||||
|     return Scaffold( |     return AppScaffold( | ||||||
|  |       appBar: AppBar( | ||||||
|  |         leading: const PageBackButton(), | ||||||
|  |         title: Text('screenSettings').tr(), | ||||||
|  |       ), | ||||||
|       body: SingleChildScrollView( |       body: SingleChildScrollView( | ||||||
|         child: Column( |         child: Column( | ||||||
|           spacing: 16, |           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( |             Column( | ||||||
|               crossAxisAlignment: CrossAxisAlignment.start, |               crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|               children: [ |               children: [ | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ Future<ThemeSet> createAppThemeSet({Color? seedColorOverride, bool? useMaterial3 | |||||||
|  |  | ||||||
| Future<ThemeData> createAppTheme( | Future<ThemeData> createAppTheme( | ||||||
|   Brightness brightness, { |   Brightness brightness, { | ||||||
|     Color? seedColorOverride, |   Color? seedColorOverride, | ||||||
|   bool? useMaterial3, |   bool? useMaterial3, | ||||||
| }) async { | }) async { | ||||||
|   final prefs = await SharedPreferences.getInstance(); |   final prefs = await SharedPreferences.getInstance(); | ||||||
| @@ -34,9 +34,10 @@ Future<ThemeData> createAppTheme( | |||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false; |   final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false; | ||||||
|  |   final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true); | ||||||
|  |  | ||||||
|   return ThemeData( |   return ThemeData( | ||||||
|     useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true), |     useMaterial3: useM3, | ||||||
|     colorScheme: colorScheme, |     colorScheme: colorScheme, | ||||||
|     brightness: brightness, |     brightness: brightness, | ||||||
|     iconTheme: IconThemeData( |     iconTheme: IconThemeData( | ||||||
| @@ -45,17 +46,19 @@ Future<ThemeData> createAppTheme( | |||||||
|       opticalSize: 20, |       opticalSize: 20, | ||||||
|       color: colorScheme.onSurface, |       color: colorScheme.onSurface, | ||||||
|     ), |     ), | ||||||
|  |     snackBarTheme: SnackBarThemeData( | ||||||
|  |       behavior: useM3 ? SnackBarBehavior.floating : SnackBarBehavior.fixed, | ||||||
|  |     ), | ||||||
|     appBarTheme: AppBarTheme( |     appBarTheme: AppBarTheme( | ||||||
|       centerTitle: true, |       centerTitle: true, | ||||||
|       elevation: hasAppBarBlurry ? 0 : null, |       elevation: hasAppBarBlurry ? 0 : null, | ||||||
|       backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary, |       backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary, | ||||||
|       foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary, |       foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary, | ||||||
|     ), |     ), | ||||||
|     scaffoldBackgroundColor: Colors.transparent, |  | ||||||
|     pageTransitionsTheme: PageTransitionsTheme( |     pageTransitionsTheme: PageTransitionsTheme( | ||||||
|       builders: { |       builders: { | ||||||
|         TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), |         TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), | ||||||
|         TargetPlatform.iOS: ZoomPageTransitionsBuilder(), |         TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), | ||||||
|         TargetPlatform.macOS: ZoomPageTransitionsBuilder(), |         TargetPlatform.macOS: ZoomPageTransitionsBuilder(), | ||||||
|         TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(), |         TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(), | ||||||
|         TargetPlatform.linux: ZoomPageTransitionsBuilder(), |         TargetPlatform.linux: ZoomPageTransitionsBuilder(), | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; | |||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:package_info_plus/package_info_plus.dart'; | import 'package:package_info_plus/package_info_plus.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  | import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||||
| import 'package:url_launcher/url_launcher_string.dart'; | import 'package:url_launcher/url_launcher_string.dart'; | ||||||
|  |  | ||||||
| class AboutScreen extends StatelessWidget { | class AboutScreen extends StatelessWidget { | ||||||
| @@ -12,97 +13,103 @@ class AboutScreen extends StatelessWidget { | |||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     const denseButtonStyle = ButtonStyle(visualDensity: VisualDensity(vertical: -4)); |     const denseButtonStyle = ButtonStyle(visualDensity: VisualDensity(vertical: -4)); | ||||||
|  |  | ||||||
|     return SizedBox( |     return AppScaffold( | ||||||
|       width: double.infinity, |       appBar: AppBar( | ||||||
|       child: Column( |         leading: const PageBackButton(), | ||||||
|         mainAxisAlignment: MainAxisAlignment.center, |         title: Text('screenAbout').tr(), | ||||||
|         children: [ |       ), | ||||||
|           ClipRRect( |       body: SizedBox( | ||||||
|             borderRadius: const BorderRadius.all(Radius.circular(16)), |         width: double.infinity, | ||||||
|             child: Image.asset('assets/icon/icon-light-radius.png', width: 120, height: 120), |         child: Column( | ||||||
|           ), |           mainAxisAlignment: MainAxisAlignment.center, | ||||||
|           const Gap(8), |           children: [ | ||||||
|           Text( |             ClipRRect( | ||||||
|             'Solian', |               borderRadius: const BorderRadius.all(Radius.circular(16)), | ||||||
|             style: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 36), |               child: Image.asset('assets/icon/icon-light-radius.png', width: 120, height: 120), | ||||||
|           ), |             ), | ||||||
|           const Text( |             const Gap(8), | ||||||
|             'The Solar Network', |             Text( | ||||||
|             style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), |               'Solian', | ||||||
|           ), |               style: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 36), | ||||||
|           const Gap(8), |             ), | ||||||
|           FutureBuilder( |             const Text( | ||||||
|             future: PackageInfo.fromPlatform(), |               'The Solar Network', | ||||||
|             builder: (context, snapshot) { |               style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), | ||||||
|               if (!snapshot.hasData) { |             ), | ||||||
|                 return const SizedBox.shrink(); |             const Gap(8), | ||||||
|               } |             FutureBuilder( | ||||||
|  |               future: PackageInfo.fromPlatform(), | ||||||
|  |               builder: (context, snapshot) { | ||||||
|  |                 if (!snapshot.hasData) { | ||||||
|  |                   return const SizedBox.shrink(); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|               return Text( |                 return Text( | ||||||
|                 'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}', |                   'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}', | ||||||
|                 style: const TextStyle(fontFamily: 'monospace'), |                   style: const TextStyle(fontFamily: 'monospace'), | ||||||
|               ); |                 ); | ||||||
|             }, |               }, | ||||||
|           ), |             ), | ||||||
|           Text('Copyright © ${DateTime.now().year} Solsynth LLC'), |             Text('Copyright © ${DateTime.now().year} Solsynth LLC'), | ||||||
|           const Gap(16), |             const Gap(16), | ||||||
|           Container( |             Container( | ||||||
|             constraints: const BoxConstraints(maxWidth: 280), |               constraints: const BoxConstraints(maxWidth: 280), | ||||||
|             child: Wrap( |               child: Wrap( | ||||||
|               spacing: 4, |                 spacing: 4, | ||||||
|               runSpacing: 4, |                 runSpacing: 4, | ||||||
|               alignment: WrapAlignment.center, |                 alignment: WrapAlignment.center, | ||||||
|               children: [ |                 children: [ | ||||||
|                 TextButton( |                   TextButton( | ||||||
|                   style: denseButtonStyle, |                     style: denseButtonStyle, | ||||||
|                   child: Text('appDetails').tr(), |                     child: Text('appDetails').tr(), | ||||||
|                   onPressed: () async { |                     onPressed: () async { | ||||||
|                     final info = await PackageInfo.fromPlatform(); |                       final info = await PackageInfo.fromPlatform(); | ||||||
|  |  | ||||||
|                     if (!context.mounted) return; |                       if (!context.mounted) return; | ||||||
|                     showAboutDialog( |                       showAboutDialog( | ||||||
|                       context: context, |                         context: context, | ||||||
|                       applicationName: 'Solian', |                         applicationName: 'Solian', | ||||||
|                       applicationVersion: '${info.version}+${info.buildNumber}', |                         applicationVersion: '${info.version}+${info.buildNumber}', | ||||||
|                       applicationLegalese: |                         applicationLegalese: | ||||||
|                           'The Solar Network App is an intuitive and open-source social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.', |                             'The Solar Network App is an intuitive and open-source social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.', | ||||||
|                       applicationIcon: ClipRRect( |                         applicationIcon: ClipRRect( | ||||||
|                         borderRadius: const BorderRadius.all(Radius.circular(16)), |                           borderRadius: const BorderRadius.all(Radius.circular(16)), | ||||||
|                         child: Image.asset( |                           child: Image.asset( | ||||||
|                           'assets/icon/icon-light-radius.png', |                             'assets/icon/icon-light-radius.png', | ||||||
|                           width: 60, |                             width: 60, | ||||||
|                           height: 60, |                             height: 60, | ||||||
|  |                           ), | ||||||
|                         ), |                         ), | ||||||
|                       ), |                       ); | ||||||
|                     ); |                     }, | ||||||
|                   }, |                   ), | ||||||
|                 ), |                   TextButton( | ||||||
|                 TextButton( |                     style: denseButtonStyle, | ||||||
|                   style: denseButtonStyle, |                     child: Text('termRelated').tr(), | ||||||
|                   child: Text('termRelated').tr(), |                     onPressed: () { | ||||||
|                   onPressed: () { |                       launchUrlString('https://solsynth.dev/terms'); | ||||||
|                     launchUrlString('https://solsynth.dev/terms'); |                     }, | ||||||
|                   }, |                   ), | ||||||
|                 ), |                   TextButton( | ||||||
|                 TextButton( |                     style: denseButtonStyle, | ||||||
|                   style: denseButtonStyle, |                     child: Text('serviceStatus').tr(), | ||||||
|                   child: Text('serviceStatus').tr(), |                     onPressed: () { | ||||||
|                   onPressed: () { |                       launchUrlString('https://status.solsynth.dev'); | ||||||
|                     launchUrlString('https://status.solsynth.dev'); |                     }, | ||||||
|                   }, |                   ), | ||||||
|                 ), |                 ], | ||||||
|               ], |               ), | ||||||
|  |             ).center(), | ||||||
|  |             const Gap(16), | ||||||
|  |             const Text( | ||||||
|  |               'Open-sourced under AGPLv3', | ||||||
|  |               style: TextStyle( | ||||||
|  |                 fontWeight: FontWeight.w300, | ||||||
|  |                 fontSize: 12, | ||||||
|  |               ), | ||||||
|             ), |             ), | ||||||
|           ).center(), |           ], | ||||||
|           const Gap(16), |         ), | ||||||
|           const Text( |  | ||||||
|             'Open-sourced under AGPLv3', |  | ||||||
|             style: TextStyle( |  | ||||||
|               fontWeight: FontWeight.w300, |  | ||||||
|               fontSize: 12, |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|         ], |  | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ class AttachmentList extends StatefulWidget { | |||||||
|   final List<SnAttachment?> data; |   final List<SnAttachment?> data; | ||||||
|   final bool bordered; |   final bool bordered; | ||||||
|   final bool gridded; |   final bool gridded; | ||||||
|  |   final bool columned; | ||||||
|   final BoxFit fit; |   final BoxFit fit; | ||||||
|   final double? maxHeight; |   final double? maxHeight; | ||||||
|   final double? minWidth; |   final double? minWidth; | ||||||
| @@ -26,6 +27,7 @@ class AttachmentList extends StatefulWidget { | |||||||
|     required this.data, |     required this.data, | ||||||
|     this.bordered = false, |     this.bordered = false, | ||||||
|     this.gridded = false, |     this.gridded = false, | ||||||
|  |     this.columned = false, | ||||||
|     this.fit = BoxFit.cover, |     this.fit = BoxFit.cover, | ||||||
|     this.maxHeight, |     this.maxHeight, | ||||||
|     this.minWidth, |     this.minWidth, | ||||||
| @@ -105,45 +107,10 @@ class _AttachmentListState extends State<AttachmentList> { | |||||||
|           ); |           ); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (widget.gridded) { |         final fullOfImage = | ||||||
|           final fullOfImage = |             widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length; | ||||||
|               widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length; |  | ||||||
|           if(!fullOfImage) { |         if (widget.gridded && 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(), |  | ||||||
|                 ), |  | ||||||
|               ), |  | ||||||
|             ); |  | ||||||
|           } |  | ||||||
|           return Container( |           return Container( | ||||||
|             margin: widget.padding ?? EdgeInsets.zero, |             margin: widget.padding ?? EdgeInsets.zero, | ||||||
|             decoration: BoxDecoration( |             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( |         return Container( | ||||||
|           constraints: BoxConstraints(maxHeight: constraints.maxHeight), |           constraints: BoxConstraints(maxHeight: constraints.maxHeight), | ||||||
|           child: ScrollConfiguration( |           child: ScrollConfiguration( | ||||||
|   | |||||||
| @@ -365,7 +365,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> { | |||||||
|         ), |         ), | ||||||
|         onVerticalDragUpdate: (details) { |         onVerticalDragUpdate: (details) { | ||||||
|           if (_showDetail) return; |           if (_showDetail) return; | ||||||
|           if (details.delta.dy < 0) { |           if (details.delta.dy <= -40) { | ||||||
|             _showDetail = true; |             _showDetail = true; | ||||||
|             showModalBottomSheet( |             showModalBottomSheet( | ||||||
|               context: context, |               context: context, | ||||||
| @@ -415,77 +415,79 @@ class _AttachmentZoomDetailPopup extends StatelessWidget { | |||||||
|             ], |             ], | ||||||
|           ).padding(horizontal: 20, top: 16, bottom: 12), |           ).padding(horizontal: 20, top: 16, bottom: 12), | ||||||
|           Expanded( |           Expanded( | ||||||
|             child: Table( |             child: SingleChildScrollView( | ||||||
|               columnWidths: { |               child: Table( | ||||||
|                 0: IntrinsicColumnWidth(), |                 columnWidths: { | ||||||
|                 1: FlexColumnWidth(), |                   0: IntrinsicColumnWidth(), | ||||||
|               }, |                   1: FlexColumnWidth(), | ||||||
|               children: [ |                 }, | ||||||
|                 TableRow( |                 children: [ | ||||||
|                   children: [ |  | ||||||
|                     TableCell( |  | ||||||
|                       child: Text('attachmentUploadBy').tr().padding(right: 16), |  | ||||||
|                     ), |  | ||||||
|                     TableCell( |  | ||||||
|                       child: Row( |  | ||||||
|                         children: [ |  | ||||||
|                           if (data.accountId > 0) |  | ||||||
|                             AccountImage( |  | ||||||
|                               content: account?.avatar, |  | ||||||
|                               radius: 8, |  | ||||||
|                             ), |  | ||||||
|                           const Gap(8), |  | ||||||
|                           Text(data.accountId > 0 ? account?.nick ?? 'unknown'.tr() : 'unknown'.tr()), |  | ||||||
|                           const Gap(8), |  | ||||||
|                           Text('#${data.accountId}', style: GoogleFonts.robotoMono()).opacity(0.75), |  | ||||||
|                         ], |  | ||||||
|                       ), |  | ||||||
|                     ), |  | ||||||
|                   ], |  | ||||||
|                 ), |  | ||||||
|                 tableGap, |  | ||||||
|                 TableRow( |  | ||||||
|                   children: [ |  | ||||||
|                     TableCell(child: Text('Mimetype').padding(right: 16)), |  | ||||||
|                     TableCell(child: Text(data.mimetype)), |  | ||||||
|                   ], |  | ||||||
|                 ), |  | ||||||
|                 TableRow( |  | ||||||
|                   children: [ |  | ||||||
|                     TableCell(child: Text('Size').padding(right: 16)), |  | ||||||
|                     TableCell( |  | ||||||
|                         child: Row( |  | ||||||
|                       children: [ |  | ||||||
|                         Text(data.size.formatBytes()), |  | ||||||
|                         const Gap(12), |  | ||||||
|                         Text('${data.size} Bytes', style: GoogleFonts.robotoMono()).opacity(0.75), |  | ||||||
|                       ], |  | ||||||
|                     )), |  | ||||||
|                   ], |  | ||||||
|                 ), |  | ||||||
|                 TableRow( |  | ||||||
|                   children: [ |  | ||||||
|                     TableCell(child: Text('Name').padding(right: 16)), |  | ||||||
|                     TableCell(child: Text(data.name)), |  | ||||||
|                   ], |  | ||||||
|                 ), |  | ||||||
|                 if (data.hash.isNotEmpty) |  | ||||||
|                   TableRow( |                   TableRow( | ||||||
|                     children: [ |                     children: [ | ||||||
|                       TableCell(child: Text('Hash').padding(right: 16)), |                       TableCell( | ||||||
|                       TableCell(child: Text(data.hash, style: GoogleFonts.robotoMono(fontSize: 11)).opacity(0.9)), |                         child: Text('attachmentUploadBy').tr().padding(right: 16), | ||||||
|  |                       ), | ||||||
|  |                       TableCell( | ||||||
|  |                         child: Row( | ||||||
|  |                           children: [ | ||||||
|  |                             if (data.accountId > 0) | ||||||
|  |                               AccountImage( | ||||||
|  |                                 content: account?.avatar, | ||||||
|  |                                 radius: 8, | ||||||
|  |                               ), | ||||||
|  |                             const Gap(8), | ||||||
|  |                             Text(data.accountId > 0 ? account?.nick ?? 'unknown'.tr() : 'unknown'.tr()), | ||||||
|  |                             const Gap(8), | ||||||
|  |                             Text('#${data.accountId}', style: GoogleFonts.robotoMono()).opacity(0.75), | ||||||
|  |                           ], | ||||||
|  |                         ), | ||||||
|  |                       ), | ||||||
|                     ], |                     ], | ||||||
|                   ), |                   ), | ||||||
|                 tableGap, |                   tableGap, | ||||||
|                 ...(data.metadata['exif']?.keys.map((k) => TableRow( |                   TableRow( | ||||||
|  |                     children: [ | ||||||
|  |                       TableCell(child: Text('Mimetype').padding(right: 16)), | ||||||
|  |                       TableCell(child: Text(data.mimetype)), | ||||||
|  |                     ], | ||||||
|  |                   ), | ||||||
|  |                   TableRow( | ||||||
|  |                     children: [ | ||||||
|  |                       TableCell(child: Text('Size').padding(right: 16)), | ||||||
|  |                       TableCell( | ||||||
|  |                           child: Row( | ||||||
|  |                         children: [ | ||||||
|  |                           Text(data.size.formatBytes()), | ||||||
|  |                           const Gap(12), | ||||||
|  |                           Text('${data.size} Bytes', style: GoogleFonts.robotoMono()).opacity(0.75), | ||||||
|  |                         ], | ||||||
|  |                       )), | ||||||
|  |                     ], | ||||||
|  |                   ), | ||||||
|  |                   TableRow( | ||||||
|  |                     children: [ | ||||||
|  |                       TableCell(child: Text('Name').padding(right: 16)), | ||||||
|  |                       TableCell(child: Text(data.name)), | ||||||
|  |                     ], | ||||||
|  |                   ), | ||||||
|  |                   if (data.hash.isNotEmpty) | ||||||
|  |                     TableRow( | ||||||
|                       children: [ |                       children: [ | ||||||
|                         TableCell(child: Text(k).padding(right: 16)), |                         TableCell(child: Text('Hash').padding(right: 16)), | ||||||
|                         TableCell(child: Text(data.metadata['exif'][k].toString())), |                         TableCell(child: Text(data.hash, style: GoogleFonts.robotoMono(fontSize: 11)).opacity(0.9)), | ||||||
|                       ], |                       ], | ||||||
|                     )) ?? |                     ), | ||||||
|                     []), |                   tableGap, | ||||||
|               ], |                   ...(data.metadata['exif']?.keys.map((k) => TableRow( | ||||||
|             ).padding(horizontal: 20, vertical: 8), |                         children: [ | ||||||
|  |                           TableCell(child: Text(k).padding(right: 16)), | ||||||
|  |                           TableCell(child: Text(data.metadata['exif'][k].toString())), | ||||||
|  |                         ], | ||||||
|  |                       )) ?? | ||||||
|  |                       []), | ||||||
|  |                 ], | ||||||
|  |               ).padding(horizontal: 20, vertical: 8), | ||||||
|  |             ), | ||||||
|           ), |           ), | ||||||
|         ], |         ], | ||||||
|       ), |       ), | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import 'package:material_symbols_icons/symbols.dart'; | |||||||
| import 'package:popover/popover.dart'; | import 'package:popover/popover.dart'; | ||||||
| import 'package:provider/provider.dart'; | import 'package:provider/provider.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  | import 'package:surface/providers/config.dart'; | ||||||
| import 'package:surface/providers/user_directory.dart'; | import 'package:surface/providers/user_directory.dart'; | ||||||
| import 'package:surface/providers/userinfo.dart'; | import 'package:surface/providers/userinfo.dart'; | ||||||
| import 'package:surface/types/chat.dart'; | import 'package:surface/types/chat.dart'; | ||||||
| @@ -53,6 +54,8 @@ class ChatMessage extends StatelessWidget { | |||||||
|  |  | ||||||
|     final dateFormatter = DateFormat('MM/dd HH:mm'); |     final dateFormatter = DateFormat('MM/dd HH:mm'); | ||||||
|  |  | ||||||
|  |     final cfg = context.read<ConfigProvider>(); | ||||||
|  |  | ||||||
|     return SwipeTo( |     return SwipeTo( | ||||||
|       key: Key('chat-message-${data.id}'), |       key: Key('chat-message-${data.id}'), | ||||||
|       iconOnLeftSwipe: Symbols.reply, |       iconOnLeftSwipe: Symbols.reply, | ||||||
| @@ -192,7 +195,10 @@ class ChatMessage extends StatelessWidget { | |||||||
|                 ], |                 ], | ||||||
|               ).opacity(isPending ? 0.5 : 1), |               ).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']!), |               LinkPreviewWidget(text: data.body['text']!), | ||||||
|             if (data.preload?.attachments?.isNotEmpty ?? false) |             if (data.preload?.attachments?.isNotEmpty ?? false) | ||||||
|               AttachmentList( |               AttachmentList( | ||||||
|   | |||||||
| @@ -18,45 +18,49 @@ class ConnectionIndicator extends StatelessWidget { | |||||||
|       listenable: ws, |       listenable: ws, | ||||||
|       builder: (context, _) { |       builder: (context, _) { | ||||||
|         final ua = context.read<UserProvider>(); |         final ua = context.read<UserProvider>(); | ||||||
|  |         final show = (ws.isBusy || !ws.isConnected) && ua.isAuthorized; | ||||||
|  |  | ||||||
|         return GestureDetector( |         return IgnorePointer( | ||||||
|           child: Material( |           ignoring: !show, | ||||||
|             elevation: 2, |           child: GestureDetector( | ||||||
|             shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), |             child: Material( | ||||||
|             color: Theme.of(context).colorScheme.secondaryContainer, |               elevation: 2, | ||||||
|             child: ua.isAuthorized |               shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), | ||||||
|                 ? Row( |               color: Theme.of(context).colorScheme.secondaryContainer, | ||||||
|                     mainAxisAlignment: MainAxisAlignment.center, |               child: ua.isAuthorized | ||||||
|                     crossAxisAlignment: CrossAxisAlignment.center, |                   ? Row( | ||||||
|                     children: [ |                       mainAxisAlignment: MainAxisAlignment.center, | ||||||
|                       if (ws.isBusy) |                       crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|                         Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer) |                       children: [ | ||||||
|                       else if (!ws.isConnected) |                         if (ws.isBusy) | ||||||
|                         Text('serverDisconnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer) |                           Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer) | ||||||
|                       else |                         else if (!ws.isConnected) | ||||||
|                         Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer), |                           Text('serverDisconnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer) | ||||||
|                       const Gap(8), |                         else | ||||||
|                       if (ws.isBusy) |                           Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer), | ||||||
|                         const CircularProgressIndicator(strokeWidth: 2.5) |                         const Gap(8), | ||||||
|                             .width(12) |                         if (ws.isBusy) | ||||||
|                             .height(12) |                           const CircularProgressIndicator(strokeWidth: 2.5) | ||||||
|                             .padding(horizontal: 4, right: 4) |                               .width(12) | ||||||
|                       else if (!ws.isConnected) |                               .height(12) | ||||||
|                         const Icon(Symbols.power_off, size: 18) |                               .padding(horizontal: 4, right: 4) | ||||||
|                       else |                         else if (!ws.isConnected) | ||||||
|                         const Icon(Symbols.power, size: 18), |                           const Icon(Symbols.power_off, size: 18) | ||||||
|                     ], |                         else | ||||||
|                   ).padding(horizontal: 8, vertical: 4) |                           const Icon(Symbols.power, size: 18), | ||||||
|                 : const SizedBox.shrink(), |                       ], | ||||||
|           ).opacity((ws.isBusy || !ws.isConnected) && ua.isAuthorized ? 1 : 0, animate: true).animate( |                     ).padding(horizontal: 8, vertical: 4) | ||||||
|                 const Duration(milliseconds: 300), |                   : const SizedBox.shrink(), | ||||||
|                 Curves.easeInOut, |             ).opacity(show ? 1 : 0, animate: true).animate( | ||||||
|               ), |                   const Duration(milliseconds: 300), | ||||||
|           onTap: () { |                   Curves.easeInOut, | ||||||
|             if (!ws.isConnected && !ws.isBusy) { |                 ), | ||||||
|               ws.connect(); |             onTap: () { | ||||||
|             } |               if (!ws.isConnected && !ws.isBusy) { | ||||||
|           }, |                 ws.connect(); | ||||||
|  |               } | ||||||
|  |             }, | ||||||
|  |           ), | ||||||
|         ); |         ); | ||||||
|       }, |       }, | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -7,12 +7,11 @@ import 'package:marquee/marquee.dart'; | |||||||
| import 'package:provider/provider.dart'; | import 'package:provider/provider.dart'; | ||||||
| import 'package:responsive_framework/responsive_framework.dart'; | import 'package:responsive_framework/responsive_framework.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  | import 'package:surface/providers/link_preview.dart'; | ||||||
| import 'package:surface/types/link.dart'; | import 'package:surface/types/link.dart'; | ||||||
| import 'package:surface/widgets/universal_image.dart'; | import 'package:surface/widgets/universal_image.dart'; | ||||||
| import 'package:url_launcher/url_launcher_string.dart'; | import 'package:url_launcher/url_launcher_string.dart'; | ||||||
|  |  | ||||||
| import '../providers/link_preview.dart'; |  | ||||||
|  |  | ||||||
| class LinkPreviewWidget extends StatefulWidget { | class LinkPreviewWidget extends StatefulWidget { | ||||||
|   final String text; |   final String text; | ||||||
|  |  | ||||||
| @@ -81,8 +80,9 @@ class _LinkPreviewEntry extends StatelessWidget { | |||||||
|                   child: AspectRatio( |                   child: AspectRatio( | ||||||
|                     aspectRatio: 16 / 9, |                     aspectRatio: 16 / 9, | ||||||
|                     child: ClipRRect( |                     child: ClipRRect( | ||||||
|  |                       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||||
|                       child: AutoResizeUniversalImage( |                       child: AutoResizeUniversalImage( | ||||||
|                         meta.image!, |                         meta.image!.startsWith('//') ? 'https:${meta.image}' : meta.image!, | ||||||
|                         fit: BoxFit.contain, |                         fit: BoxFit.contain, | ||||||
|                       ), |                       ), | ||||||
|                     ), |                     ), | ||||||
| @@ -94,11 +94,14 @@ class _LinkPreviewEntry extends StatelessWidget { | |||||||
|                   crossAxisAlignment: CrossAxisAlignment.center, |                   crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|                   children: [ |                   children: [ | ||||||
|                     if (meta.icon?.isNotEmpty ?? false) |                     if (meta.icon?.isNotEmpty ?? false) | ||||||
|                       StyledWidget( |                       SizedBox( | ||||||
|                         meta.icon!.endsWith('.svg') |                         width: 36, | ||||||
|                             ? SvgPicture.network(meta.icon!) |                         height: 36, | ||||||
|  |                         child: meta.icon!.endsWith('.svg') | ||||||
|  |                             ? SvgPicture.network(meta.icon!, width: 36, height: 36) | ||||||
|                             : UniversalImage( |                             : UniversalImage( | ||||||
|                                 meta.icon!, |                                 meta.icon!, | ||||||
|  |                                 noErrorWidget: true, | ||||||
|                                 width: 36, |                                 width: 36, | ||||||
|                                 height: 36, |                                 height: 36, | ||||||
|                                 cacheHeight: 36, |                                 cacheHeight: 36, | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| import 'dart:io'; | import 'dart:io'; | ||||||
|  |  | ||||||
| import 'package:bitsdojo_window/bitsdojo_window.dart'; | import 'package:bitsdojo_window/bitsdojo_window.dart'; | ||||||
| import 'package:easy_localization/easy_localization.dart'; |  | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:go_router/go_router.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/config.dart'; | ||||||
| import 'package:surface/providers/navigation.dart'; | import 'package:surface/providers/navigation.dart'; | ||||||
| import 'package:surface/widgets/connection_indicator.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_background.dart'; | ||||||
| import 'package:surface/widgets/navigation/app_bottom_navigation.dart'; | import 'package:surface/widgets/navigation/app_bottom_navigation.dart'; | ||||||
| import 'package:surface/widgets/navigation/app_drawer_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>(); | final globalRootScaffoldKey = GlobalKey<ScaffoldState>(); | ||||||
|  |  | ||||||
| class AppPageScaffold extends StatelessWidget { | class AppScaffold extends StatelessWidget { | ||||||
|   final String? title; |  | ||||||
|   final Widget? body; |   final Widget? body; | ||||||
|   final bool showAppBar; |   final PreferredSizeWidget? bottomNavigationBar; | ||||||
|   final bool showBottomNavigation; |   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, |     super.key, | ||||||
|     this.title, |     this.appBar, | ||||||
|     this.body, |     this.body, | ||||||
|     this.showAppBar = true, |     this.floatingActionButton, | ||||||
|     this.showBottomNavigation = false, |     this.floatingActionButtonLocation, | ||||||
|  |     this.floatingActionButtonAnimator, | ||||||
|  |     this.bottomNavigationBar, | ||||||
|  |     this.bottomSheet, | ||||||
|  |     this.drawer, | ||||||
|  |     this.endDrawer, | ||||||
|  |     this.onDrawerChanged, | ||||||
|  |     this.onEndDrawerChanged, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     final state = GoRouter.maybeOf(context); |     final appBarHeight = appBar?.preferredSize.height ?? 0; | ||||||
|     final routeName = state?.routerDelegate.currentConfiguration.last.route.name; |     final safeTop = MediaQuery.of(context).padding.top; | ||||||
|  |  | ||||||
|     final autoTitle = state != null ? 'screen${routeName?.capitalize()}' : 'screen'; |  | ||||||
|  |  | ||||||
|     return Scaffold( |     return Scaffold( | ||||||
|       appBar: showAppBar |       extendBody: true, | ||||||
|           ? AppBar( |       extendBodyBehindAppBar: true, | ||||||
|               title: Text(title ?? autoTitle.tr()), |       backgroundColor: Theme.of(context).scaffoldBackgroundColor, | ||||||
|             ) |       body: SizedBox.expand( | ||||||
|           : null, |         child: AppBackground( | ||||||
|       body: body, |           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; |     final safeTop = MediaQuery.of(context).padding.top; | ||||||
|  |  | ||||||
|     return AppBackground( |     return Scaffold( | ||||||
|       isRoot: true, |       key: globalRootScaffoldKey, | ||||||
|       child: Scaffold( |       backgroundColor: Theme.of(context).colorScheme.surface, | ||||||
|         key: globalRootScaffoldKey, |       body: Stack( | ||||||
|         body: Stack( |         children: [ | ||||||
|           children: [ |           Column( | ||||||
|             Column( |             children: [ | ||||||
|               children: [ |               if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) | ||||||
|                 if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) |                 WindowTitleBarBox( | ||||||
|                   Container( |                   child: Container( | ||||||
|                     decoration: BoxDecoration( |                     decoration: BoxDecoration( | ||||||
|                       border: Border( |                       border: Border( | ||||||
|                         bottom: BorderSide( |                         bottom: BorderSide( | ||||||
| @@ -119,49 +159,44 @@ class AppRootScaffold extends StatelessWidget { | |||||||
|                         ), |                         ), | ||||||
|                       ), |                       ), | ||||||
|                     ), |                     ), | ||||||
|                     child: Row( |                     child: MoveWindow( | ||||||
|                       crossAxisAlignment: CrossAxisAlignment.center, |                       child: Row( | ||||||
|                       mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start, |                         crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|                       children: [ |                         mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start, | ||||||
|                         WindowTitleBarBox( |                         children: [ | ||||||
|                           child: MoveWindow( |                           Text( | ||||||
|                             child: Text( |                             'Solar Network', | ||||||
|                               'Solar Network', |                             style: GoogleFonts.spaceGrotesk(), | ||||||
|                               style: GoogleFonts.spaceGrotesk(), |                           ).padding(horizontal: 12, vertical: 5), | ||||||
|                             ).padding(horizontal: 12, vertical: 5), |                           if (!Platform.isMacOS) | ||||||
|                           ), |                             Row( | ||||||
|                         ), |                               mainAxisSize: MainAxisSize.min, | ||||||
|                         if (!Platform.isMacOS) |                               children: [ | ||||||
|                           Expanded( |                                 Expanded(child: MoveWindow()), | ||||||
|                             child: WindowTitleBarBox( |                                 Row( | ||||||
|                               child: Row( |                                   children: [ | ||||||
|                                 children: [ |                                     MinimizeWindowButton(colors: windowButtonColor), | ||||||
|                                   Expanded(child: MoveWindow()), |                                     MaximizeWindowButton(colors: windowButtonColor), | ||||||
|                                   Row( |                                     CloseWindowButton(colors: windowButtonColor), | ||||||
|                                     children: [ |                                   ], | ||||||
|                                       MinimizeWindowButton(colors: windowButtonColor), |                                 ), | ||||||
|                                       MaximizeWindowButton(colors: windowButtonColor), |                               ], | ||||||
|                                       CloseWindowButton(colors: windowButtonColor), |  | ||||||
|                                     ], |  | ||||||
|                                   ), |  | ||||||
|                                 ], |  | ||||||
|                               ), |  | ||||||
|                             ), |                             ), | ||||||
|                           ), |                         ], | ||||||
|                       ], |                       ), | ||||||
|                     ), |                     ), | ||||||
|                   ), |                   ), | ||||||
|                 Expanded(child: innerWidget), |                 ), | ||||||
|               ], |               Expanded(child: innerWidget), | ||||||
|             ), |             ], | ||||||
|             Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()), |           ), | ||||||
|             Positioned(top: safeTop > 0 ? safeTop : 16, left: 8, child: ConnectionIndicator()), |           Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()), | ||||||
|           ], |           Positioned(top: safeTop > 0 ? safeTop : 16, left: 8, child: ConnectionIndicator()), | ||||||
|         ), |         ], | ||||||
|         drawer: !isExpandedDrawer ? AppNavigationDrawer() : null, |  | ||||||
|         drawerEdgeDragWidth: isPopable ? 0 : null, |  | ||||||
|         bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null, |  | ||||||
|       ), |       ), | ||||||
|  |       drawer: !isExpandedDrawer ? AppNavigationDrawer() : null, | ||||||
|  |       drawerEdgeDragWidth: isPopable ? 0 : null, | ||||||
|  |       bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -15,43 +15,48 @@ class NotifyIndicator extends StatelessWidget { | |||||||
|     final ua = context.read<UserProvider>(); |     final ua = context.read<UserProvider>(); | ||||||
|     final nty = context.watch<NotificationProvider>(); |     final nty = context.watch<NotificationProvider>(); | ||||||
|  |  | ||||||
|  |     final show = nty.notifications.isNotEmpty && ua.isAuthorized; | ||||||
|  |  | ||||||
|     return ListenableBuilder( |     return ListenableBuilder( | ||||||
|         listenable: nty, |         listenable: nty, | ||||||
|         builder: (context, _) { |         builder: (context, _) { | ||||||
|           return GestureDetector( |           return IgnorePointer( | ||||||
|             child: Material( |             ignoring: !show, | ||||||
|               elevation: 2, |             child: GestureDetector( | ||||||
|               shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), |               child: Material( | ||||||
|               color: Theme.of(context).colorScheme.secondaryContainer, |                 elevation: 2, | ||||||
|               child: ua.isAuthorized |                 shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), | ||||||
|                   ? Row( |                 color: Theme.of(context).colorScheme.secondaryContainer, | ||||||
|                       mainAxisAlignment: MainAxisAlignment.center, |                 child: ua.isAuthorized | ||||||
|                       crossAxisAlignment: CrossAxisAlignment.center, |                     ? Row( | ||||||
|                       children: [ |                         mainAxisAlignment: MainAxisAlignment.center, | ||||||
|                         Text( |                         crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|                           nty.notifications.lastOrNull?.title ?? |                         children: [ | ||||||
|                               'notificationUnreadCount'.plural(nty.notifications.length), |  | ||||||
|                           maxLines: 1, |  | ||||||
|                           overflow: TextOverflow.ellipsis, |  | ||||||
|                         ), |  | ||||||
|                         if (nty.notifications.lastOrNull?.body != null) |  | ||||||
|                           Text( |                           Text( | ||||||
|                             nty.notifications.lastOrNull!.body, |                             nty.notifications.lastOrNull?.title ?? | ||||||
|  |                                 'notificationUnreadCount'.plural(nty.notifications.length), | ||||||
|                             maxLines: 1, |                             maxLines: 1, | ||||||
|                             overflow: TextOverflow.ellipsis, |                             overflow: TextOverflow.ellipsis, | ||||||
|                           ).padding(left: 4), |                           ), | ||||||
|                         const Gap(8), |                           if (nty.notifications.lastOrNull?.body != null) | ||||||
|                         const Icon(Symbols.notifications_unread, size: 18), |                             Text( | ||||||
|                       ], |                               nty.notifications.lastOrNull!.body, | ||||||
|                     ).padding(horizontal: 8, vertical: 4) |                               maxLines: 1, | ||||||
|                   : const SizedBox.shrink(), |                               overflow: TextOverflow.ellipsis, | ||||||
|             ).opacity(nty.notifications.isNotEmpty && ua.isAuthorized ? 1 : 0, animate: true).animate( |                             ).padding(left: 4), | ||||||
|                   const Duration(milliseconds: 300), |                           const Gap(8), | ||||||
|                   Curves.easeInOut, |                           const Icon(Symbols.notifications_unread, size: 18), | ||||||
|                 ), |                         ], | ||||||
|             onTap: () { |                       ).padding(horizontal: 8, vertical: 4) | ||||||
|               nty.clear(); |                     : const SizedBox.shrink(), | ||||||
|             }, |               ).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') |         ?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article') | ||||||
|         .toList(); |         .toList(); | ||||||
|  |  | ||||||
|  |     final cfg = context.read<ConfigProvider>(); | ||||||
|  |  | ||||||
|     return Column( |     return Column( | ||||||
|       crossAxisAlignment: CrossAxisAlignment.center, |       crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|       children: [ |       children: [ | ||||||
| @@ -256,13 +258,12 @@ class PostItem extends StatelessWidget { | |||||||
|           AttachmentList( |           AttachmentList( | ||||||
|             data: displayableAttachments!, |             data: displayableAttachments!, | ||||||
|             bordered: true, |             bordered: true, | ||||||
|             gridded: true, |  | ||||||
|             maxHeight: showFullPost ? null : 480, |             maxHeight: showFullPost ? null : 480, | ||||||
|             minWidth: 640, |             maxWidth: MediaQuery.of(context).size.width - 20, | ||||||
|             fit: showFullPost ? BoxFit.cover : BoxFit.contain, |             fit: showFullPost ? BoxFit.cover : BoxFit.contain, | ||||||
|             padding: const EdgeInsets.symmetric(horizontal: 12), |             padding: const EdgeInsets.symmetric(horizontal: 12), | ||||||
|           ), |           ), | ||||||
|         if (data.body['content'] != null) |         if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true)) | ||||||
|           LinkPreviewWidget( |           LinkPreviewWidget( | ||||||
|             text: data.body['content'], |             text: data.body['content'], | ||||||
|           ).padding(horizontal: 4), |           ).padding(horizontal: 4), | ||||||
| @@ -344,7 +345,7 @@ class PostShareImageWidget extends StatelessWidget { | |||||||
|           if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false)) |           if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false)) | ||||||
|             StyledWidget(AttachmentList( |             StyledWidget(AttachmentList( | ||||||
|               data: data.preload!.attachments!, |               data: data.preload!.attachments!, | ||||||
|               gridded: true, |               columned: true, | ||||||
|             )).padding(horizontal: 16, bottom: 8), |             )).padding(horizontal: 16, bottom: 8), | ||||||
|           Column( |           Column( | ||||||
|             crossAxisAlignment: CrossAxisAlignment.start, |             crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter/services.dart'; | ||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:provider/provider.dart'; | import 'package:provider/provider.dart'; | ||||||
| @@ -12,6 +13,7 @@ import 'package:surface/widgets/dialog.dart'; | |||||||
| class PostReactionPopup extends StatefulWidget { | class PostReactionPopup extends StatefulWidget { | ||||||
|   final SnPost data; |   final SnPost data; | ||||||
|   final Function(Map<String, int> value, int attr, int delta)? onChanged; |   final Function(Map<String, int> value, int attr, int delta)? onChanged; | ||||||
|  |  | ||||||
|   const PostReactionPopup({super.key, required this.data, this.onChanged}); |   const PostReactionPopup({super.key, required this.data, this.onChanged}); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -59,6 +61,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> { | |||||||
|           ); |           ); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  |       HapticFeedback.mediumImpact(); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       // ignore: use_build_context_synchronously |       // ignore: use_build_context_synchronously | ||||||
|       if (context.mounted) context.showErrorDialog(err); |       if (context.mounted) context.showErrorDialog(err); | ||||||
| @@ -84,9 +87,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> { | |||||||
|             children: [ |             children: [ | ||||||
|               const Icon(Symbols.mood, size: 24), |               const Icon(Symbols.mood, size: 24), | ||||||
|               const Gap(16), |               const Gap(16), | ||||||
|               Text('postReactions') |               Text('postReactions').tr().textStyle(Theme.of(context).textTheme.titleLarge!), | ||||||
|                   .tr() |  | ||||||
|                   .textStyle(Theme.of(context).textTheme.titleLarge!), |  | ||||||
|             ], |             ], | ||||||
|           ).padding(horizontal: 20, top: 16, bottom: 12), |           ).padding(horizontal: 20, top: 16, bottom: 12), | ||||||
|           Container( |           Container( | ||||||
| @@ -102,9 +103,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> { | |||||||
|                 Text('postReactionDownvote').plural(widget.data.totalDownvote), |                 Text('postReactionDownvote').plural(widget.data.totalDownvote), | ||||||
|                 const Gap(24), |                 const Gap(24), | ||||||
|                 Icon( |                 Icon( | ||||||
|                   widget.data.totalUpvote >= widget.data.totalDownvote |                   widget.data.totalUpvote >= widget.data.totalDownvote ? Symbols.trending_up : Symbols.trending_down, | ||||||
|                       ? Symbols.trending_up |  | ||||||
|                       : Symbols.trending_down, |  | ||||||
|                   size: 16, |                   size: 16, | ||||||
|                 ), |                 ), | ||||||
|                 const Gap(8), |                 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 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | ||||||
| # In Windows, build-name is used as the major, minor, and patch parts | # In Windows, build-name is used as the major, minor, and patch parts | ||||||
| # of the product and file versions while build-number is used as the build suffix. | # of the product and file versions while build-number is used as the build suffix. | ||||||
| version: 2.2.2+54 | version: 2.2.2+55 | ||||||
|  |  | ||||||
| environment: | environment: | ||||||
|   sdk: ^3.5.4 |   sdk: ^3.5.4 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user