From c82dc7ad8507beabea1145293052010331d8403e Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Tue, 21 Jan 2025 20:35:04 +0800 Subject: [PATCH] :recycle: Refactored background image (skip ci) --- assets/translations/en-US.json | 3 + assets/translations/zh-CN.json | 3 + assets/translations/zh-HK.json | 9 +- assets/translations/zh-TW.json | 9 +- lib/providers/config.dart | 1 + lib/providers/notification.dart | 6 + lib/router.dart | 109 +++--- lib/screens/abuse_report.dart | 8 +- lib/screens/account.dart | 3 +- lib/screens/account/profile_edit.dart | 256 +++++++------- lib/screens/account/profile_page.dart | 2 + .../account/publishers/publisher_edit.dart | 3 +- .../account/publishers/publisher_new.dart | 7 +- .../account/publishers/publishers.dart | 100 +++--- lib/screens/album.dart | 3 +- lib/screens/auth/login.dart | 131 +++---- lib/screens/auth/register.dart | 326 +++++++++--------- lib/screens/chat.dart | 111 +++--- lib/screens/chat/call_room.dart | 3 +- lib/screens/chat/channel_detail.dart | 3 +- lib/screens/chat/manage.dart | 3 +- lib/screens/chat/room.dart | 3 +- lib/screens/explore.dart | 9 +- lib/screens/friend.dart | 5 +- lib/screens/home.dart | 5 +- lib/screens/notification.dart | 5 +- lib/screens/post/post_detail.dart | 3 +- lib/screens/post/post_editor.dart | 3 +- lib/screens/post/post_search.dart | 3 +- lib/screens/post/publisher_page.dart | 3 +- lib/screens/realm.dart | 5 +- lib/screens/realm/manage.dart | 3 +- lib/screens/realm/realm_detail.dart | 11 +- lib/screens/settings.dart | 20 ++ lib/theme.dart | 3 +- lib/widgets/attachment/attachment_zoom.dart | 2 +- lib/widgets/navigation/app_scaffold.dart | 182 +++++++--- 37 files changed, 755 insertions(+), 609 deletions(-) diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 5ec4fc3..1ba01f2 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -193,6 +193,9 @@ "settingsColorSchemeDescription": "Set the application primary color.", "settingsColorSeed": "Color Seed", "settingsColorSeedDescription": "Select one of the present color schemes.", + "settingsFeatures": "Features", + "settingsNotifyWithHaptic": "Haptic when Notified", + "settingsNotifyWithHapticDescription": "Vibrate lightly when a new notification appears in the foreground.", "settingsNetwork": "Network", "settingsNetworkServer": "HyperNet Server", "settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.", diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 9108716..814187b 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -191,6 +191,9 @@ "settingsColorSchemeDescription": "设置应用主题色。", "settingsColorSeed": "预设色彩主题", "settingsColorSeedDescription": "选择一个预设色彩主题。", + "settingsFeatures": "功能", + "settingsNotifyWithHaptic": "新通知时振动", + "settingsNotifyWithHapticDescription": "在应用在前台时收到新通知出现时出发轻量的振动。", "settingsNetwork": "网络", "settingsNetworkServer": "HyperNet 服务器", "settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。", diff --git a/assets/translations/zh-HK.json b/assets/translations/zh-HK.json index d656d83..9020f55 100644 --- a/assets/translations/zh-HK.json +++ b/assets/translations/zh-HK.json @@ -191,6 +191,9 @@ "settingsColorSchemeDescription": "設置應用主題色。", "settingsColorSeed": "預設色彩主題", "settingsColorSeedDescription": "選擇一個預設色彩主題。", + "settingsFeatures": "功能", + "settingsNotifyWithHaptic": "新通知時振動", + "settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。", "settingsNetwork": "網絡", "settingsNetworkServer": "HyperNet 服務器", "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", @@ -213,8 +216,9 @@ "sensitiveContentCollapsed": "敏感內容已摺疊。", "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", "sensitiveContentReveal": "顯示內容", - "serverConnecting": "正在連接服務器…", - "serverDisconnected": "已與服務器斷開連接", + "serverConnecting": "正在連接…", + "serverDisconnected": "已斷開連接", + "serverConnected": "已連接", "fieldChatAlias": "頻道別名", "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。", "fieldChatName": "名稱", @@ -292,6 +296,7 @@ "addAttachmentFromCameraPhoto": "拍攝照片", "addAttachmentFromCameraVideo": "拍攝視頻", "addAttachmentFromRandomId": "通過訪問 ID 鏈接", + "attachmentDetailInfo": "附件詳細信息", "attachmentPastedImage": "粘貼的圖片", "attachmentInsertLink": "插入連接", "attachmentSetAsPostThumbnail": "設置為帖子縮略圖", diff --git a/assets/translations/zh-TW.json b/assets/translations/zh-TW.json index d0bb772..a11513b 100644 --- a/assets/translations/zh-TW.json +++ b/assets/translations/zh-TW.json @@ -191,6 +191,9 @@ "settingsColorSchemeDescription": "設置應用主題色。", "settingsColorSeed": "預設色彩主題", "settingsColorSeedDescription": "選擇一個預設色彩主題。", + "settingsFeatures": "功能", + "settingsNotifyWithHaptic": "新通知時振動", + "settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。", "settingsNetwork": "網絡", "settingsNetworkServer": "HyperNet 服務器", "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", @@ -213,8 +216,9 @@ "sensitiveContentCollapsed": "敏感內容已摺疊。", "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", "sensitiveContentReveal": "顯示內容", - "serverConnecting": "正在連接服務器…", - "serverDisconnected": "已與服務器斷開連接", + "serverConnecting": "正在連接…", + "serverDisconnected": "已斷開連接", + "serverConnected": "已連接", "fieldChatAlias": "頻道別名", "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。", "fieldChatName": "名稱", @@ -292,6 +296,7 @@ "addAttachmentFromCameraPhoto": "拍攝照片", "addAttachmentFromCameraVideo": "拍攝視頻", "addAttachmentFromRandomId": "通過訪問 ID 鏈接", + "attachmentDetailInfo": "附件詳細信息", "attachmentPastedImage": "粘貼的圖片", "attachmentInsertLink": "插入連接", "attachmentSetAsPostThumbnail": "設置為帖子縮略圖", diff --git a/lib/providers/config.dart b/lib/providers/config.dart index b9eb021..c48384a 100644 --- a/lib/providers/config.dart +++ b/lib/providers/config.dart @@ -14,6 +14,7 @@ const kAppbarTransparentStoreKey = 'app_bar_transparent'; const kAppBackgroundStoreKey = 'app_has_background'; const kAppColorSchemeStoreKey = 'app_color_scheme'; const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse'; +const kAppNotifyWithHaptic = 'app_notify_with_haptic'; const Map kImageQualityLevel = { 'settingsImageQualityLowest': FilterQuality.none, diff --git a/lib/providers/notification.dart b/lib/providers/notification.dart index 48d1b93..55197dc 100644 --- a/lib/providers/notification.dart +++ b/lib/providers/notification.dart @@ -4,8 +4,10 @@ import 'dart:io'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_udid/flutter_udid.dart'; import 'package:provider/provider.dart'; +import 'package:surface/providers/config.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/websocket.dart'; @@ -15,11 +17,13 @@ class NotificationProvider extends ChangeNotifier { late final SnNetworkProvider _sn; late final UserProvider _ua; late final WebSocketProvider _ws; + late final ConfigProvider _cfg; NotificationProvider(BuildContext context) { _sn = context.read(); _ua = context.read(); _ws = context.read(); + _cfg = context.read(); } Future registerPushNotifications() async { @@ -75,6 +79,8 @@ class NotificationProvider extends ChangeNotifier { final notification = SnNotification.fromJson(event.payload!); notifications.add(notification); notifyListeners(); + final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true; + if (doHaptic) HapticFeedback.lightImpact(); } }); } diff --git a/lib/router.dart b/lib/router.dart index de65d31..40aab6b 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -36,10 +36,7 @@ import 'package:surface/widgets/navigation/app_scaffold.dart'; final _appRoutes = [ ShellRoute( - builder: (context, state, child) => AppPageScaffold( - body: child, - showAppBar: false, - ), + builder: (context, state, child) => child, routes: [ GoRoute( path: '/', @@ -58,47 +55,39 @@ final _appRoutes = [ 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?, + builder: (context, state) => 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(','), - ), + builder: (context, state) => PostSearchScreen( + initialTags: state.uri.queryParameters['tags']?.split(','), + initialCategories: state.uri.queryParameters['categories']?.split(','), ), ), GoRoute( path: '/publishers/:name', name: 'postPublisher', - builder: (context, state) => AppBackground( - child: PostPublisherScreen(name: state.pathParameters['name']!), - ), + builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!), ), GoRoute( path: '/:slug', name: 'postDetail', - builder: (context, state) => AppBackground( - child: PostDetailScreen( - slug: state.pathParameters['slug']!, - preload: state.extra as SnPost?, - ), + builder: (context, state) => PostDetailScreen( + slug: state.pathParameters['slug']!, + preload: state.extra as SnPost?, ), ), ], @@ -106,7 +95,15 @@ final _appRoutes = [ GoRoute( path: '/account', name: 'account', - pageBuilder: (context, state) => NoTransitionPage( + pageBuilder: (context, state) => CustomTransitionPage( + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeThroughTransition( + animation: animation, + secondaryAnimation: secondaryAnimation, + fillColor: Colors.transparent, + child: child, + ); + }, child: const AccountScreen(), ), routes: [], @@ -114,7 +111,15 @@ final _appRoutes = [ GoRoute( path: '/chat', name: 'chat', - pageBuilder: (context, state) => NoTransitionPage( + pageBuilder: (context, state) => CustomTransitionPage( + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeThroughTransition( + animation: animation, + secondaryAnimation: secondaryAnimation, + fillColor: Colors.transparent, + child: child, + ); + }, child: const ChatScreen(), ), routes: [ @@ -228,57 +233,43 @@ final _appRoutes = [ ], ), ShellRoute( - builder: (context, state, child) => AppPageScaffold(body: child), + builder: (context, state, child) => child, routes: [ GoRoute( path: '/auth/login', name: 'authLogin', - builder: (context, state) => const AppBackground( - child: LoginScreen(), - ), + builder: (context, state) => LoginScreen(), ), GoRoute( path: '/auth/register', name: 'authRegister', - builder: (context, state) => const AppBackground( - child: RegisterScreen(), - ), + builder: (context, state) => RegisterScreen(), ), GoRoute( path: '/reports', name: 'abuseReport', - builder: (context, state) => const AppBackground( - child: AbuseReportScreen(), - ), + builder: (context, state) => AbuseReportScreen(), ), GoRoute( path: '/account/profile/edit', name: 'accountProfileEdit', - builder: (context, state) => const AppBackground( - child: ProfileEditScreen(), - ), + builder: (context, state) => ProfileEditScreen(), ), GoRoute( path: '/account/publishers', name: 'accountPublishers', - builder: (context, state) => const AppBackground( - child: PublisherScreen(), - ), + builder: (context, state) => PublisherScreen(), ), GoRoute( path: '/account/publishers/new', name: 'accountPublisherNew', - builder: (context, state) => const AppBackground( - child: AccountPublisherNewScreen(), - ), + builder: (context, state) => AccountPublisherNewScreen(), ), GoRoute( path: '/account/publishers/edit/:name', name: 'accountPublisherEdit', - builder: (context, state) => AppBackground( - child: AccountPublisherEditScreen( - name: state.pathParameters['name']!, - ), + builder: (context, state) => AccountPublisherEditScreen( + name: state.pathParameters['name']!, ), ), ], @@ -296,9 +287,7 @@ final _appRoutes = [ GoRoute( path: '/settings', name: 'settings', - builder: (context, state) => const AppBackground( - child: SettingsScreen(), - ), + builder: (context, state) => SettingsScreen(), ), ], ), @@ -308,9 +297,7 @@ final _appRoutes = [ GoRoute( path: '/about', name: 'about', - builder: (context, state) => const AppBackground( - child: AboutScreen(), - ), + builder: (context, state) => AboutScreen(), ), ], ), diff --git a/lib/screens/abuse_report.dart b/lib/screens/abuse_report.dart index 078a617..49746c4 100644 --- a/lib/screens/abuse_report.dart +++ b/lib/screens/abuse_report.dart @@ -6,6 +6,7 @@ import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/widgets/dialog.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; import '../types/account.dart'; @@ -56,7 +57,11 @@ class _AbuseReportScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( + return AppScaffold( + appBar: AppBar( + leading: const PageBackButton(), + title: Text('screenAbuseReport').tr(), + ), body: Column( children: [ ListTile( @@ -73,6 +78,7 @@ class _AbuseReportScreenState extends State { else Expanded( child: ListView.builder( + padding: EdgeInsets.only(top: 8), itemCount: _reports.length, itemBuilder: (context, idx) { return ListTile( diff --git a/lib/screens/account.dart b/lib/screens/account.dart index 44ff592..525c893 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -12,6 +12,7 @@ import 'package:surface/providers/websocket.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/dialog.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; class AccountScreen extends StatelessWidget { const AccountScreen({super.key}); @@ -20,7 +21,7 @@ class AccountScreen extends StatelessWidget { Widget build(BuildContext context) { final ua = context.watch(); - return Scaffold( + return AppScaffold( appBar: AppBar( leading: AutoAppBarLeading(), title: Text("screenAccount").tr(), diff --git a/lib/screens/account/profile_edit.dart b/lib/screens/account/profile_edit.dart index 5916f05..cfa98a5 100644 --- a/lib/screens/account/profile_edit.dart +++ b/lib/screens/account/profile_edit.dart @@ -18,6 +18,7 @@ import 'package:surface/providers/userinfo.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/universal_image.dart'; class ProfileEditScreen extends StatefulWidget { @@ -81,8 +82,7 @@ class _ProfileEditScreenState extends State { onDateTimeChanged: (DateTime newDate) { setState(() { _birthday = newDate; - _birthdayController.text = - DateFormat(_kDateFormat).format(_birthday!); + _birthdayController.text = DateFormat(_kDateFormat).format(_birthday!); }); }, ), @@ -96,11 +96,9 @@ class _ProfileEditScreenState extends State { if (image == null) return; if (!mounted) return; - final ImageProvider imageProvider = - kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); - final aspectRatios = place == 'banner' - ? [CropAspectRatio(width: 16, height: 7)] - : [CropAspectRatio(width: 1, height: 1)]; + final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); + final aspectRatios = + place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)]; final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) ? await showCupertinoImageCropper( // ignore: use_build_context_synchronously @@ -122,10 +120,7 @@ class _ProfileEditScreenState extends State { setState(() => _isBusy = true); - final rawBytes = - (await result.uiImage.toByteData(format: ImageByteFormat.png))! - .buffer - .asUint8List(); + final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List(); try { final attachment = await attach.directUploadOne( @@ -212,136 +207,141 @@ class _ProfileEditScreenState extends State { final sn = context.read(); - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LoadingIndicator(isActive: _isBusy), - const Gap(24), - Stack( - clipBehavior: Clip.none, - children: [ - Material( - elevation: 0, - child: InkWell( - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(8)), - child: AspectRatio( - aspectRatio: 16 / 9, - child: Container( - color: - Theme.of(context).colorScheme.surfaceContainerHigh, - child: _banner != null - ? AutoResizeUniversalImage( - sn.getAttachmentUrl(_banner!), - fit: BoxFit.cover, - ) - : const SizedBox.shrink(), + return AppScaffold( + appBar: AppBar( + leading: const PageBackButton(), + title: Text('screenProfileEdit').tr(), + ), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LoadingIndicator(isActive: _isBusy), + const Gap(24), + Stack( + clipBehavior: Clip.none, + children: [ + Material( + elevation: 0, + child: InkWell( + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: AspectRatio( + aspectRatio: 16 / 9, + child: Container( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + child: _banner != null + ? AutoResizeUniversalImage( + sn.getAttachmentUrl(_banner!), + fit: BoxFit.cover, + ) + : const SizedBox.shrink(), + ), ), ), - ), - 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: () { - _updateImage('avatar'); + _updateImage('banner'); }, ), ), - ), - ], - ).padding(horizontal: padding), - const Gap(8 + 28), - Column( - children: [ - TextField( - 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(), - ), + 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: () { + _updateImage('avatar'); + }, ), ), - const Gap(8), - Flexible( - flex: 1, - child: TextField( - controller: _lastNameController, - decoration: InputDecoration( - border: const UnderlineInputBorder(), - labelText: 'fieldLastName'.tr(), + ), + ], + ).padding(horizontal: padding), + const Gap(8 + 28), + Column( + children: [ + TextField( + 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), - TextField( - controller: _birthdayController, - readOnly: true, - decoration: InputDecoration( - border: const UnderlineInputBorder(), - labelText: 'fieldBirthday'.tr(), + const Gap(4), + TextField( + controller: _birthdayController, + readOnly: true, + decoration: InputDecoration( + border: const UnderlineInputBorder(), + labelText: 'fieldBirthday'.tr(), + ), + onTap: () => _selectBirthday(), ), - onTap: () => _selectBirthday(), - ), - ], - ).padding(horizontal: padding + 8), - const Gap(12), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ElevatedButton.icon( - onPressed: _isBusy ? null : _updateUserInfo, - icon: const Icon(Symbols.save), - label: Text('apply').tr(), - ), - ], - ).padding(horizontal: padding), - ], + ], + ).padding(horizontal: padding + 8), + const Gap(12), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton.icon( + onPressed: _isBusy ? null : _updateUserInfo, + icon: const Icon(Symbols.save), + label: Text('apply').tr(), + ), + ], + ).padding(horizontal: padding), + ], + ), ), ); } diff --git a/lib/screens/account/profile_page.dart b/lib/screens/account/profile_page.dart index 5ad3cf6..360491b 100644 --- a/lib/screens/account/profile_page.dart +++ b/lib/screens/account/profile_page.dart @@ -19,6 +19,7 @@ import 'package:surface/types/check_in.dart'; import 'package:surface/types/post.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/dialog.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/universal_image.dart'; const Map kBadgesMeta = { @@ -241,6 +242,7 @@ class _UserScreenState extends State with SingleTickerProviderStateM final sn = context.read(); return Scaffold( + backgroundColor: Colors.transparent, body: CustomScrollView( controller: _scrollController, slivers: [ diff --git a/lib/screens/account/publishers/publisher_edit.dart b/lib/screens/account/publishers/publisher_edit.dart index 488e808..b948d55 100644 --- a/lib/screens/account/publishers/publisher_edit.dart +++ b/lib/screens/account/publishers/publisher_edit.dart @@ -18,6 +18,7 @@ import 'package:surface/types/post.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/universal_image.dart'; class AccountPublisherEditScreen extends StatefulWidget { @@ -176,7 +177,7 @@ class _AccountPublisherEditScreenState extends State Widget build(BuildContext context) { final sn = context.read(); - return Scaffold( + return AppScaffold( body: SingleChildScrollView( child: Column( children: [ diff --git a/lib/screens/account/publishers/publisher_new.dart b/lib/screens/account/publishers/publisher_new.dart index 80239f5..febe118 100644 --- a/lib/screens/account/publishers/publisher_new.dart +++ b/lib/screens/account/publishers/publisher_new.dart @@ -10,6 +10,7 @@ import 'package:surface/providers/userinfo.dart'; import 'package:surface/types/realm.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/dialog.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; class AccountPublisherNewScreen extends StatefulWidget { const AccountPublisherNewScreen({super.key}); @@ -24,7 +25,11 @@ class _AccountPublisherNewScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( + return AppScaffold( + appBar: AppBar( + leading: const PageBackButton(), + title: Text('screenAccountPublisherNew').tr(), + ), body: SingleChildScrollView( child: Column( children: [ diff --git a/lib/screens/account/publishers/publishers.dart b/lib/screens/account/publishers/publishers.dart index 9384821..14720bf 100644 --- a/lib/screens/account/publishers/publishers.dart +++ b/lib/screens/account/publishers/publishers.dart @@ -10,6 +10,7 @@ import 'package:surface/types/post.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; class PublisherScreen extends StatefulWidget { const PublisherScreen({super.key}); @@ -32,8 +33,7 @@ class _PublisherScreenState extends State { try { final resp = await sn.client.get('/cgi/co/publishers/me'); - final List out = List.from( - resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []); + final List out = List.from(resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []); if (!mounted) return; @@ -53,7 +53,11 @@ class _PublisherScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( + return AppScaffold( + appBar: AppBar( + leading: const PageBackButton(), + title: Text('screenAccountPublishers').tr(), + ), body: Column( children: [ ListTile( @@ -62,9 +66,7 @@ class _PublisherScreenState extends State { contentPadding: const EdgeInsets.symmetric(horizontal: 24), leading: const Icon(Symbols.add_circle), onTap: () { - GoRouter.of(context) - .pushNamed('accountPublisherNew') - .then((value) { + GoRouter.of(context).pushNamed('accountPublisherNew').then((value) { if (value == true) { _publishers.clear(); _fetchPublishers(); @@ -75,48 +77,52 @@ class _PublisherScreenState extends State { const Divider(height: 1), LoadingIndicator(isActive: _isBusy), Expanded( - child: RefreshIndicator( - onRefresh: () { - _publishers.clear(); - return _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(); - } - }); - }, - ), - ], - ), - ); + child: MediaQuery.removePadding( + context: context, + removeTop: true, + child: RefreshIndicator( + onRefresh: () { + _publishers.clear(); + return _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(); + } + }); + }, + ), + ], + ), + ); + }, + ), ), ), ), diff --git a/lib/screens/album.dart b/lib/screens/album.dart index 5ab0a43..6215e41 100644 --- a/lib/screens/album.dart +++ b/lib/screens/album.dart @@ -11,6 +11,7 @@ import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/attachment/attachment_zoom.dart'; import 'package:surface/widgets/attachment/attachment_item.dart'; import 'package:surface/widgets/dialog.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:uuid/uuid.dart'; class AlbumScreen extends StatefulWidget { @@ -82,7 +83,7 @@ class _AlbumScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( + return AppScaffold( body: CustomScrollView( controller: _scrollController, slivers: [ diff --git a/lib/screens/auth/login.dart b/lib/screens/auth/login.dart index 46f90a2..8ef3048 100644 --- a/lib/screens/auth/login.dart +++ b/lib/screens/auth/login.dart @@ -9,6 +9,7 @@ import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/userinfo.dart'; import 'package:surface/types/auth.dart'; import 'package:surface/widgets/dialog.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:url_launcher/url_launcher_string.dart'; import '../../providers/websocket.dart'; @@ -35,67 +36,73 @@ class _LoginScreenState extends State { @override Widget build(BuildContext context) { - return Theme( - data: Theme.of(context).copyWith(canvasColor: Colors.transparent), - child: SingleChildScrollView( - child: PageTransitionSwitcher( - transitionBuilder: ( - Widget child, - Animation primaryAnimation, - Animation secondaryAnimation, - ) { - return SharedAxisTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.horizontal, - child: Container( - constraints: BoxConstraints(maxWidth: 380), - child: child, - ), - ); - }, - child: switch (_period % 3) { - 1 => _LoginPickerScreen( - key: const ValueKey(1), - ticket: _currentTicket, - factors: _factors, - onTicket: (p0) => setState(() { - _currentTicket = p0; - }), - onPickFactor: (p0) => setState(() { - _factorPicked = p0; - }), - onNext: () => setState(() { - _period++; - }), - ), - 2 => _LoginCheckScreen( - key: const ValueKey(2), - ticket: _currentTicket, - factor: _factorPicked, - onTicket: (p0) => setState(() { - _currentTicket = p0; - }), - onNext: () => setState(() { - _period = 1; - }), - ), - _ => _LoginLookupScreen( - key: const ValueKey(0), - ticket: _currentTicket, - onTicket: (p0) => setState(() { - _currentTicket = p0; - }), - onFactor: (p0) => setState(() { - _factors = p0; - }), - onNext: () => setState(() { - _period++; - }), - ), - }, - ).padding(all: 24), - ).center(), + return AppScaffold( + appBar: AppBar( + leading: const PageBackButton(), + title: Text('screenAuthLogin').tr(), + ), + body: Theme( + data: Theme.of(context).copyWith(canvasColor: Colors.transparent), + child: SingleChildScrollView( + child: PageTransitionSwitcher( + transitionBuilder: ( + Widget child, + Animation primaryAnimation, + Animation secondaryAnimation, + ) { + return SharedAxisTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.horizontal, + child: Container( + constraints: BoxConstraints(maxWidth: 380), + child: child, + ), + ); + }, + child: switch (_period % 3) { + 1 => _LoginPickerScreen( + key: const ValueKey(1), + ticket: _currentTicket, + factors: _factors, + onTicket: (p0) => setState(() { + _currentTicket = p0; + }), + onPickFactor: (p0) => setState(() { + _factorPicked = p0; + }), + onNext: () => setState(() { + _period++; + }), + ), + 2 => _LoginCheckScreen( + key: const ValueKey(2), + ticket: _currentTicket, + factor: _factorPicked, + onTicket: (p0) => setState(() { + _currentTicket = p0; + }), + onNext: () => setState(() { + _period = 1; + }), + ), + _ => _LoginLookupScreen( + key: const ValueKey(0), + ticket: _currentTicket, + onTicket: (p0) => setState(() { + _currentTicket = p0; + }), + onFactor: (p0) => setState(() { + _factors = p0; + }), + onNext: () => setState(() { + _period++; + }), + ), + }, + ).padding(all: 24), + ).center(), + ), ); } } @@ -441,7 +448,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> { widget.onNext(); } catch (err) { - if(mounted) context.showErrorDialog(err); + if (mounted) context.showErrorDialog(err); return; } finally { setState(() => _isBusy = false); diff --git a/lib/screens/auth/register.dart b/lib/screens/auth/register.dart index ced7a3c..bd33053 100644 --- a/lib/screens/auth/register.dart +++ b/lib/screens/auth/register.dart @@ -8,6 +8,7 @@ import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/widgets/dialog.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:url_launcher/url_launcher_string.dart'; class RegisterScreen extends StatefulWidget { @@ -54,175 +55,178 @@ class _RegisterScreenState extends State { @override Widget build(BuildContext context) { - return StyledWidget(Container( - constraints: const BoxConstraints(maxWidth: 380), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Align( - alignment: Alignment.centerLeft, - child: CircleAvatar( - radius: 26, - child: const Icon( - Symbols.person_add, - size: 28, - ), - ).padding(bottom: 8), - ), - Text( - 'screenAuthRegister', - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.w900, + return AppScaffold( + appBar: AppBar( + leading: const PageBackButton(), + title: Text('screenAuthRegister').tr(), + ), + body: StyledWidget(Container( + constraints: const BoxConstraints(maxWidth: 380), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Align( + alignment: Alignment.centerLeft, + child: CircleAvatar( + radius: 26, + child: const Icon( + Symbols.person_add, + size: 28, + ), + ).padding(bottom: 8), ), - ).tr().padding(left: 4, bottom: 16), - Form( - key: _formKey, - autovalidateMode: AutovalidateMode.onUserInteraction, - child: Column( - children: [ - TextFormField( - validator: (value) { - if (value == null || value.length < 4 || value.length > 32) { - return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]); - } - if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) { - return 'fieldUsernameAlphanumOnly'.tr(); - } - return null; - }, - autocorrect: false, - enableSuggestions: false, - controller: _usernameController, - autofillHints: const [AutofillHints.username], - decoration: InputDecoration( - isDense: true, - border: const UnderlineInputBorder(), - labelText: 'fieldUsername'.tr(), - ), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), - ), - const Gap(12), - TextFormField( - validator: (value) { - 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()), - ), + Text( + 'screenAuthRegister', + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.w900, + ), + ).tr().padding(left: 4, bottom: 16), + Form( + key: _formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + children: [ + TextFormField( + validator: (value) { + if (value == null || value.length < 4 || value.length > 32) { + return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]); + } + if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) { + return 'fieldUsernameAlphanumOnly'.tr(); + } + return null; + }, + autocorrect: false, + enableSuggestions: false, + controller: _usernameController, + autofillHints: const [AutofillHints.username], + decoration: InputDecoration( + isDense: true, + border: const UnderlineInputBorder(), + labelText: 'fieldUsername'.tr(), ), - Material( - color: Colors.transparent, - child: InkWell( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text('termAcceptLink'.tr()), - const Gap(4), - const Icon(Symbols.launch, size: 14), - ], + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + const Gap(12), + TextFormField( + validator: (value) { + 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( + 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(), + ); } } diff --git a/lib/screens/chat.dart b/lib/screens/chat.dart index 79812be..b22e752 100644 --- a/lib/screens/chat.dart +++ b/lib/screens/chat.dart @@ -13,6 +13,7 @@ import 'package:surface/widgets/account/account_select.dart'; import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/unauthorized_hint.dart'; import 'package:uuid/uuid.dart'; @@ -120,7 +121,7 @@ class _ChatScreenState extends State { final ua = context.read(); if (!ua.isAuthorized) { - return Scaffold( + return AppScaffold( appBar: AppBar( leading: AutoAppBarLeading(), title: Text('screenChat').tr(), @@ -131,7 +132,7 @@ class _ChatScreenState extends State { ); } - return Scaffold( + return AppScaffold( appBar: AppBar( leading: AutoAppBarLeading(), title: Text('screenChat').tr(), @@ -195,22 +196,58 @@ class _ChatScreenState extends State { children: [ LoadingIndicator(isActive: _isBusy), Expanded( - child: RefreshIndicator( - onRefresh: () => Future.sync(() => _refreshChannels()), - child: ListView.builder( - itemCount: _channels?.length ?? 0, - itemBuilder: (context, idx) { - final channel = _channels![idx]; - final lastMessage = _lastMessages?[channel.id]; + child: MediaQuery.removePadding( + context: context, + removeTop: true, + child: RefreshIndicator( + onRefresh: () => Future.sync(() => _refreshChannels()), + child: ListView.builder( + itemCount: _channels?.length ?? 0, + itemBuilder: (context, idx) { + final channel = _channels![idx]; + final lastMessage = _lastMessages?[channel.id]; - if (channel.type == 1) { - final otherMember = channel.members?.cast().firstWhere( - (ele) => ele?.accountId != ua.user?.id, - orElse: () => null, - ); + if (channel.type == 1) { + final otherMember = channel.members?.cast().firstWhere( + (ele) => ele?.accountId != ua.user?.id, + 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( - title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name), + title: Text(channel.name), subtitle: lastMessage != null ? Text( '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', @@ -218,15 +255,14 @@ class _ChatScreenState extends State { overflow: TextOverflow.ellipsis, ) : Text( - 'channelDirectMessageDescription'.tr(args: [ - '@${ud.getAccountFromCache(otherMember?.accountId)?.name}', - ]), + channel.description, maxLines: 1, overflow: TextOverflow.ellipsis, ), contentPadding: const EdgeInsets.symmetric(horizontal: 16), leading: AccountImage( - content: ud.getAccountFromCache(otherMember?.accountId)?.avatar, + content: null, + fallbackWidget: const Icon(Symbols.chat, size: 20), ), onTap: () { GoRouter.of(context).pushNamed( @@ -236,43 +272,12 @@ class _ChatScreenState extends State { 'alias': channel.alias, }, ).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(); - }); - }, - ); - }, + }, + ), ), ), ), diff --git a/lib/screens/chat/call_room.dart b/lib/screens/chat/call_room.dart index a9bd1f3..c56e131 100644 --- a/lib/screens/chat/call_room.dart +++ b/lib/screens/chat/call_room.dart @@ -9,6 +9,7 @@ import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/chat_call.dart'; import 'package:surface/widgets/chat/call/call_controls.dart'; import 'package:surface/widgets/chat/call/call_participant.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; class CallRoomScreen extends StatefulWidget { final String scope; @@ -152,7 +153,7 @@ class _CallRoomScreenState extends State { return ListenableBuilder( listenable: call, builder: (context, _) { - return Scaffold( + return AppScaffold( appBar: AppBar( title: RichText( textAlign: TextAlign.center, diff --git a/lib/screens/chat/channel_detail.dart b/lib/screens/chat/channel_detail.dart index 0f13927..2b768e8 100644 --- a/lib/screens/chat/channel_detail.dart +++ b/lib/screens/chat/channel_detail.dart @@ -14,6 +14,7 @@ import 'package:surface/types/chat.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class ChannelDetailScreen extends StatefulWidget { @@ -189,7 +190,7 @@ class _ChannelDetailScreenState extends State { final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id; - return Scaffold( + return AppScaffold( appBar: AppBar( title: _channel != null ? Text(_channel!.name) : Text('loading').tr(), ), diff --git a/lib/screens/chat/manage.dart b/lib/screens/chat/manage.dart index 51ecbe6..ea0ddae 100644 --- a/lib/screens/chat/manage.dart +++ b/lib/screens/chat/manage.dart @@ -12,6 +12,7 @@ import 'package:surface/types/realm.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:uuid/uuid.dart'; class ChatManageScreen extends StatefulWidget { @@ -121,7 +122,7 @@ class _ChatManageScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( + return AppScaffold( appBar: AppBar( title: widget.editingChannelAlias != null ? Text('screenChatManage').tr() diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index c2cb7fe..c9f8f3a 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -20,6 +20,7 @@ import 'package:surface/widgets/chat/chat_message_input.dart'; import 'package:surface/widgets/chat/chat_typing_indicator.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import '../../providers/user_directory.dart'; @@ -211,7 +212,7 @@ class _ChatRoomScreenState extends State { final call = context.watch(); final ud = context.read(); - return Scaffold( + return AppScaffold( appBar: AppBar( title: Text( _channel?.type == 1 diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index 84dde26..6ce9abf 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -13,6 +13,7 @@ import 'package:surface/screens/post/post_detail.dart'; import 'package:surface/types/post.dart'; import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/dialog.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/post/post_item.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart'; @@ -95,7 +96,7 @@ class _ExploreScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( + return AppScaffold( floatingActionButtonLocation: ExpandableFab.location, floatingActionButton: ExpandableFab( key: _fabKey, @@ -212,7 +213,7 @@ class _ExploreScreenState extends State { ), ), ), - const SliverGap(8), + const SliverGap(12), SliverInfiniteList( itemCount: _posts.length, isLoading: _isBusy, @@ -242,10 +243,10 @@ class _ExploreScreenState extends State { ), openColor: Colors.transparent, openElevation: 0, - closedColor: Theme.of(context).colorScheme.surface, + closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.75), transitionType: ContainerTransitionType.fade, closedShape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(8)), + borderRadius: BorderRadius.all(Radius.circular(16)), ), ), ); diff --git a/lib/screens/friend.dart b/lib/screens/friend.dart index c33f741..e15e349 100644 --- a/lib/screens/friend.dart +++ b/lib/screens/friend.dart @@ -11,6 +11,7 @@ import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; import '../providers/userinfo.dart'; import '../widgets/unauthorized_hint.dart'; @@ -180,7 +181,7 @@ class _FriendScreenState extends State { final ua = context.read(); if (!ua.isAuthorized) { - return Scaffold( + return AppScaffold( appBar: AppBar( leading: AutoAppBarLeading(), title: Text('screenFriend').tr(), @@ -191,7 +192,7 @@ class _FriendScreenState extends State { ); } - return Scaffold( + return AppScaffold( appBar: AppBar( leading: AutoAppBarLeading(), title: Text('screenFriend').tr(), diff --git a/lib/screens/home.dart b/lib/screens/home.dart index b015a8f..841d768 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -25,6 +25,7 @@ import 'package:surface/types/check_in.dart'; import 'package:surface/types/post.dart'; import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/dialog.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/post/post_item.dart'; class HomeScreenDashEntry { @@ -67,7 +68,7 @@ class _HomeScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( + return AppScaffold( appBar: AppBar( leading: AutoAppBarLeading(), title: Text("screenHome").tr(), @@ -387,6 +388,8 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> { Text( 'dailyCheckInNone', style: Theme.of(context).textTheme.bodyLarge, + maxLines: 2, + overflow: TextOverflow.ellipsis, ).tr(), ], ) diff --git a/lib/screens/notification.dart b/lib/screens/notification.dart index e265ffb..e5f4cdd 100644 --- a/lib/screens/notification.dart +++ b/lib/screens/notification.dart @@ -14,6 +14,7 @@ import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/markdown_content.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/post/post_item.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart'; @@ -137,7 +138,7 @@ class _NotificationScreenState extends State { final ua = context.read(); if (!ua.isAuthorized) { - return Scaffold( + return AppScaffold( appBar: AppBar( leading: AutoAppBarLeading(), title: Text('screenNotification').tr(), @@ -148,7 +149,7 @@ class _NotificationScreenState extends State { ); } - return Scaffold( + return AppScaffold( appBar: AppBar( leading: AutoAppBarLeading(), title: Text('screenNotification').tr(), diff --git a/lib/screens/post/post_detail.dart b/lib/screens/post/post_detail.dart index edd7531..e472686 100644 --- a/lib/screens/post/post_detail.dart +++ b/lib/screens/post/post_detail.dart @@ -14,6 +14,7 @@ import 'package:surface/types/post.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/navigation/app_background.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/post/post_comment_list.dart'; import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_mini_editor.dart'; @@ -67,7 +68,7 @@ class _PostDetailScreenState extends State { return AppBackground( isRoot: widget.onBack != null, - child: Scaffold( + child: AppScaffold( appBar: AppBar( leading: BackButton( onPressed: () { diff --git a/lib/screens/post/post_editor.dart b/lib/screens/post/post_editor.dart index 295773d..12cc4ff 100644 --- a/lib/screens/post/post_editor.dart +++ b/lib/screens/post/post_editor.dart @@ -13,6 +13,7 @@ import 'package:surface/providers/sn_network.dart'; import 'package:surface/types/post.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/loading_indicator.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_media_pending_list.dart'; import 'package:surface/widgets/post/post_meta_editor.dart'; @@ -128,7 +129,7 @@ class _PostEditorScreenState extends State { return ListenableBuilder( listenable: _writeController, builder: (context, _) { - return Scaffold( + return AppScaffold( appBar: AppBar( leading: BackButton( onPressed: () { diff --git a/lib/screens/post/post_search.dart b/lib/screens/post/post_search.dart index bcda095..a31ffe9 100644 --- a/lib/screens/post/post_search.dart +++ b/lib/screens/post/post_search.dart @@ -8,6 +8,7 @@ import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/post.dart'; import 'package:surface/types/post.dart'; import 'package:surface/widgets/dialog.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_tags_field.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart'; @@ -119,7 +120,7 @@ class _PostSearchScreenState extends State { ), ]; - return Scaffold( + return AppScaffold( appBar: AppBar( title: Text('screenPostSearch').tr(), actions: [ diff --git a/lib/screens/post/publisher_page.dart b/lib/screens/post/publisher_page.dart index 1b73fa7..4ec30a0 100644 --- a/lib/screens/post/publisher_page.dart +++ b/lib/screens/post/publisher_page.dart @@ -17,6 +17,7 @@ import 'package:surface/types/post.dart'; import 'package:surface/types/realm.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/dialog.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/universal_image.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart'; @@ -274,7 +275,7 @@ class _PostPublisherScreenState extends State with SingleTi final sn = context.read(); - return Scaffold( + return AppScaffold( body: NestedScrollView( controller: _scrollController, headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { diff --git a/lib/screens/realm.dart b/lib/screens/realm.dart index 262af89..0a309bf 100644 --- a/lib/screens/realm.dart +++ b/lib/screens/realm.dart @@ -12,6 +12,7 @@ import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/unauthorized_hint.dart'; import 'package:surface/widgets/universal_image.dart'; @@ -83,7 +84,7 @@ class _RealmScreenState extends State { final ua = context.read(); if (!ua.isAuthorized) { - return Scaffold( + return AppScaffold( appBar: AppBar( leading: AutoAppBarLeading(), title: Text('screenRealm').tr(), @@ -94,7 +95,7 @@ class _RealmScreenState extends State { ); } - return Scaffold( + return AppScaffold( appBar: AppBar( leading: AutoAppBarLeading(), title: Text('screenRealm').tr(), diff --git a/lib/screens/realm/manage.dart b/lib/screens/realm/manage.dart index d6ce4a2..3bdc99f 100644 --- a/lib/screens/realm/manage.dart +++ b/lib/screens/realm/manage.dart @@ -18,6 +18,7 @@ import 'package:surface/types/realm.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/universal_image.dart'; import 'package:uuid/uuid.dart'; @@ -179,7 +180,7 @@ class _RealmManageScreenState extends State { Widget build(BuildContext context) { final sn = context.read(); - return Scaffold( + return AppScaffold( appBar: AppBar( title: widget.editingRealmAlias != null ? Text('screenRealmManage').tr() diff --git a/lib/screens/realm/realm_detail.dart b/lib/screens/realm/realm_detail.dart index a032581..d700655 100644 --- a/lib/screens/realm/realm_detail.dart +++ b/lib/screens/realm/realm_detail.dart @@ -11,6 +11,7 @@ import 'package:surface/providers/userinfo.dart'; import 'package:surface/types/realm.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/dialog.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import '../../types/post.dart'; @@ -70,19 +71,11 @@ class _RealmDetailScreenState extends State { Widget build(BuildContext context) { return DefaultTabController( length: 3, - child: Scaffold( + child: AppScaffold( body: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { - // These are the slivers that show up in the "outer" scroll view. return [ SliverOverlapAbsorber( - // This widget takes the overlapping behavior of the SliverAppBar, - // and redirects it to the SliverOverlapInjector below. If it is - // missing, then it is possible for the nested "inner" scroll view - // below to end up under the SliverAppBar even when the inner - // scroll view thinks it has not been scrolled. - // This is not necessary if the "headerSliverBuilder" only builds - // widgets that do not overlap the next sliver. handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), sliver: SliverAppBar( title: Text(_realm?.name ?? 'loading'.tr()), diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 3d448c3..6e34752 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -18,6 +18,7 @@ import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/theme.dart'; import 'package:surface/theme.dart'; import 'package:surface/widgets/dialog.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; const Map kColorSchemes = { 'colorSchemeIndigo': Colors.indigo, @@ -68,6 +69,7 @@ class _SettingsScreenState extends State { final sn = context.read(); return Scaffold( + backgroundColor: Colors.transparent, body: SingleChildScrollView( child: Column( spacing: 16, @@ -255,6 +257,24 @@ class _SettingsScreenState extends State { ), ], ), + 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); + }); + }, + ), + ], + ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/theme.dart b/lib/theme.dart index bff577d..33a4391 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -55,11 +55,10 @@ Future createAppTheme( backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary, foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary, ), - scaffoldBackgroundColor: Colors.transparent, pageTransitionsTheme: PageTransitionsTheme( builders: { TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), - TargetPlatform.iOS: ZoomPageTransitionsBuilder(), + TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), TargetPlatform.macOS: ZoomPageTransitionsBuilder(), TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(), TargetPlatform.linux: ZoomPageTransitionsBuilder(), diff --git a/lib/widgets/attachment/attachment_zoom.dart b/lib/widgets/attachment/attachment_zoom.dart index 1289ff6..35397e7 100644 --- a/lib/widgets/attachment/attachment_zoom.dart +++ b/lib/widgets/attachment/attachment_zoom.dart @@ -365,7 +365,7 @@ class _AttachmentZoomViewState extends State { ), onVerticalDragUpdate: (details) { if (_showDetail) return; - if (details.delta.dy < 0) { + if (details.delta.dy <= -40) { _showDetail = true; showModalBottomSheet( context: context, diff --git a/lib/widgets/navigation/app_scaffold.dart b/lib/widgets/navigation/app_scaffold.dart index 0fa799f..c2a2cdf 100644 --- a/lib/widgets/navigation/app_scaffold.dart +++ b/lib/widgets/navigation/app_scaffold.dart @@ -21,18 +21,90 @@ import 'package:surface/widgets/notify_indicator.dart'; final globalRootScaffoldKey = GlobalKey(); +class AppScaffold extends StatelessWidget { + final Widget? body; + final PreferredSizeWidget? bottomNavigationBar; + final PreferredSizeWidget? bottomSheet; + final Drawer? drawer; + final Widget? endDrawer; + final FloatingActionButtonAnimator? floatingActionButtonAnimator; + final FloatingActionButtonLocation? floatingActionButtonLocation; + final Widget? floatingActionButton; + final AppBar? appBar; + final DrawerCallback? onDrawerChanged; + final DrawerCallback? onEndDrawerChanged; + + const AppScaffold({ + super.key, + this.appBar, + this.body, + this.floatingActionButton, + this.floatingActionButtonLocation, + this.floatingActionButtonAnimator, + this.bottomNavigationBar, + this.bottomSheet, + this.drawer, + this.endDrawer, + this.onDrawerChanged, + this.onEndDrawerChanged, + }); + + @override + Widget build(BuildContext context) { + final appBarHeight = appBar?.preferredSize.height ?? 0; + final safeTop = MediaQuery.of(context).padding.top; + + return Scaffold( + extendBody: true, + extendBodyBehindAppBar: true, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + body: SizedBox.expand( + child: AppBackground( + child: Column( + children: [ + IgnorePointer(child: SizedBox(height: appBar != null ? appBarHeight + safeTop : 0)), + if (body != null) Expanded(child: body!), + ], + ), + ), + ), + appBar: appBar, + bottomNavigationBar: bottomNavigationBar, + bottomSheet: bottomSheet, + drawer: drawer, + endDrawer: endDrawer, + floatingActionButton: floatingActionButton, + floatingActionButtonAnimator: floatingActionButtonAnimator, + floatingActionButtonLocation: floatingActionButtonLocation, + onDrawerChanged: onDrawerChanged, + onEndDrawerChanged: onEndDrawerChanged, + ); + } +} + +class PageBackButton extends StatelessWidget { + const PageBackButton({super.key}); + + @override + Widget build(BuildContext context) { + return BackButton( + onPressed: () { + GoRouter.of(context).pop(); + }, + ); + } +} + class AppPageScaffold extends StatelessWidget { final String? title; final Widget? body; final bool showAppBar; - final bool showBottomNavigation; const AppPageScaffold({ super.key, this.title, this.body, this.showAppBar = true, - this.showBottomNavigation = false, }); @override @@ -42,7 +114,7 @@ class AppPageScaffold extends StatelessWidget { final autoTitle = state != null ? 'screen${routeName?.capitalize()}' : 'screen'; - return Scaffold( + return AppScaffold( appBar: showAppBar ? AppBar( title: Text(title ?? autoTitle.tr()), @@ -101,64 +173,62 @@ class AppRootScaffold extends StatelessWidget { final safeTop = MediaQuery.of(context).padding.top; - return AppBackground( - isRoot: true, - child: Scaffold( - key: globalRootScaffoldKey, - body: Stack( - children: [ - Column( - children: [ - if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) - WindowTitleBarBox( - child: Container( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: Theme.of(context).dividerColor, - width: 1 / devicePixelRatio, - ), - ), - ), - child: MoveWindow( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start, - children: [ - Text( - 'Solar Network', - style: GoogleFonts.spaceGrotesk(), - ).padding(horizontal: 12, vertical: 5), - if (!Platform.isMacOS) - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded(child: MoveWindow()), - Row( - children: [ - MinimizeWindowButton(colors: windowButtonColor), - MaximizeWindowButton(colors: windowButtonColor), - CloseWindowButton(colors: windowButtonColor), - ], - ), - ], - ), - ], + return Scaffold( + key: globalRootScaffoldKey, + backgroundColor: Theme.of(context).colorScheme.surface, + body: Stack( + children: [ + Column( + children: [ + if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) + WindowTitleBarBox( + child: Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor, + width: 1 / devicePixelRatio, ), ), ), + child: MoveWindow( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start, + children: [ + Text( + 'Solar Network', + style: GoogleFonts.spaceGrotesk(), + ).padding(horizontal: 12, vertical: 5), + if (!Platform.isMacOS) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded(child: MoveWindow()), + Row( + children: [ + MinimizeWindowButton(colors: windowButtonColor), + MaximizeWindowButton(colors: windowButtonColor), + CloseWindowButton(colors: windowButtonColor), + ], + ), + ], + ), + ], + ), + ), ), - Expanded(child: innerWidget), - ], - ), - 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, + ), + Expanded(child: innerWidget), + ], + ), + 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, ); } }