From 911439f3afc52149d750ce927dc04ffd966ef341 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 18 May 2025 23:50:50 +0800 Subject: [PATCH] :alembic: Testing desktop layout --- lib/route.dart | 16 ++- lib/route.gr.dart | 16 +++ lib/screens/auth/tabs.dart | 126 ++++++++++++++++++------ lib/screens/chat/chat.dart | 57 +++++++++-- lib/screens/chat/room.dart | 124 ++++++++++++++++------- lib/services/responsive.dart | 18 +++- lib/widgets/chat/message_item.dart | 153 +++++++++++++---------------- 7 files changed, 342 insertions(+), 168 deletions(-) diff --git a/lib/route.dart b/lib/route.dart index 24d7190..b74f616 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -16,7 +16,17 @@ class AppRouter extends RootStackRouter { AutoRoute(page: ExploreRoute.page, path: 'explore'), AutoRoute(page: AccountRoute.page, path: 'account'), AutoRoute(page: RealmListRoute.page, path: 'realms'), - AutoRoute(page: ChatListRoute.page, path: 'chat'), + AutoRoute( + page: ChatShellRoute.page, + path: 'chat', + children: [ + AutoRoute(page: ChatListRoute.page, path: ''), + AutoRoute(page: ChatRoomRoute.page, path: ':id'), + AutoRoute(page: NewChatRoute.page, path: 'new'), + AutoRoute(page: EditChatRoute.page, path: ':id/edit'), + AutoRoute(page: ChatDetailRoute.page, path: ':id/detail'), + ], + ), ], ), AutoRoute(page: WalletRoute.page, path: '/wallet'), @@ -47,10 +57,6 @@ class AppRouter extends RootStackRouter { AutoRoute(page: NewRealmRoute.page, path: '/realms/new'), AutoRoute(page: RealmDetailRoute.page, path: '/realms/:slug'), AutoRoute(page: EditRealmRoute.page, path: '/realms/:slug/edit'), - AutoRoute(page: NewChatRoute.page, path: '/chat/new'), - AutoRoute(page: EditChatRoute.page, path: '/chat/:id/edit'), - AutoRoute(page: ChatRoomRoute.page, path: '/chat/:id'), - AutoRoute(page: ChatDetailRoute.page, path: '/chat/:id/detail'), AutoRoute(page: CreatorHubRoute.page, path: '/creators'), AutoRoute(page: StickersRoute.page, path: '/creators/:name/stickers'), AutoRoute( diff --git a/lib/route.gr.dart b/lib/route.gr.dart index f5207f7..12e3db2 100644 --- a/lib/route.gr.dart +++ b/lib/route.gr.dart @@ -209,6 +209,22 @@ class ChatRoomRouteArgs { } } +/// generated route for +/// [_i5.ChatShellScreen] +class ChatShellRoute extends _i25.PageRouteInfo { + const ChatShellRoute({List<_i25.PageRouteInfo>? children}) + : super(ChatShellRoute.name, initialChildren: children); + + static const String name = 'ChatShellRoute'; + + static _i25.PageInfo page = _i25.PageInfo( + name, + builder: (data) { + return const _i5.ChatShellScreen(); + }, + ); +} + /// generated route for /// [_i7.CreateAccountScreen] class CreateAccountRoute extends _i25.PageRouteInfo { diff --git a/lib/screens/auth/tabs.dart b/lib/screens/auth/tabs.dart index c44c97d..31c5ca0 100644 --- a/lib/screens/auth/tabs.dart +++ b/lib/screens/auth/tabs.dart @@ -1,6 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; import 'package:island/route.gr.dart'; import 'package:island/services/responsive.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -11,46 +12,107 @@ class TabsScreen extends StatelessWidget { @override Widget build(BuildContext context) { - final useHorizontalLayout = - MediaQuery.of(context).size.width > kWideScreenWidth; + final useHorizontalLayout = isWideScreen(context); + final useExpandableLayout = isWidestScreen(context); - return AutoTabsRouter.pageView( - routes: const [ - ExploreRoute(), - ChatListRoute(), - RealmListRoute(), - AccountRoute(), - ], + final destinations = [ + NavigationDestination( + label: 'explore'.tr(), + icon: const Icon(Symbols.explore), + ), + NavigationDestination(label: 'chat'.tr(), icon: const Icon(Symbols.chat)), + NavigationDestination( + label: 'realms'.tr(), + icon: const Icon(Symbols.workspaces), + ), + NavigationDestination( + label: 'account'.tr(), + icon: const Icon(Symbols.account_circle), + ), + ]; + + final routes = [ + ExploreRoute(), + ChatListRoute(), + RealmListRoute(), + AccountRoute(), + ]; + + return AutoTabsRouter.tabBar( + routes: routes, scrollDirection: useHorizontalLayout ? Axis.vertical : Axis.horizontal, physics: const NeverScrollableScrollPhysics(), builder: (context, child, _) { final tabsRouter = AutoTabsRouter.of(context); + + // Check if current route is a tab route + final currentRoute = context.router.topRoute; + final isTabRoute = routes.any( + (route) => route.routeName == currentRoute.name, + ); + return Scaffold( extendBodyBehindAppBar: true, backgroundColor: Colors.transparent, - body: child, - bottomNavigationBar: NavigationBar( - selectedIndex: tabsRouter.activeIndex, - onDestinationSelected: tabsRouter.setActiveIndex, - destinations: [ - NavigationDestination( - label: 'explore'.tr(), - icon: const Icon(Symbols.explore), - ), - NavigationDestination( - label: 'chat'.tr(), - icon: const Icon(Symbols.chat), - ), - NavigationDestination( - label: 'realms'.tr(), - icon: const Icon(Symbols.workspaces), - ), - NavigationDestination( - label: 'account'.tr(), - icon: const Icon(Symbols.account_circle), - ), - ], - ), + body: + useHorizontalLayout + ? Row( + children: [ + if (isTabRoute) + Column( + children: [ + Gap(MediaQuery.of(context).padding.top + 8), + if (useExpandableLayout) + Expanded( + child: NavigationDrawer( + backgroundColor: Colors.transparent, + children: [ + for (final destination in destinations) + NavigationDrawerDestination( + label: Text(destination.label), + icon: destination.icon, + ), + ], + ), + ) + else + Expanded( + child: NavigationRail( + selectedIndex: tabsRouter.activeIndex, + onDestinationSelected: + tabsRouter.setActiveIndex, + labelType: NavigationRailLabelType.all, + destinations: + destinations + .map( + (d) => NavigationRailDestination( + icon: d.icon, + label: Text(d.label), + ), + ) + .toList(), + ), + ), + Gap(MediaQuery.of(context).padding.bottom + 8), + ], + ), + if (isTabRoute) + VerticalDivider( + color: Theme.of(context).dividerColor, + width: 1 / MediaQuery.of(context).devicePixelRatio, + ), + Expanded(child: child), + ], + ) + : child, + bottomNavigationBar: + !useHorizontalLayout && isTabRoute + ? NavigationBar( + selectedIndex: tabsRouter.activeIndex, + onDestinationSelected: tabsRouter.setActiveIndex, + destinations: destinations, + ) + : null, ); }, ); diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index 3377d48..db73045 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -16,11 +16,13 @@ import 'package:island/pods/network.dart'; import 'package:island/route.gr.dart'; import 'package:island/screens/realm/realms.dart'; import 'package:island/services/file.dart'; +import 'package:island/services/responsive.dart'; import 'package:island/widgets/account/account_picker.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/realms/selection_dropdown.dart'; +import 'package:island/widgets/response.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -69,11 +71,7 @@ class ChatRoomListTile extends StatelessWidget { ? Text(room.members!.map((e) => '@${e.account.name}').join(', ')) : Text(room.description ?? 'descriptionNone'.tr()), trailing: trailing, - onTap: - onTap ?? - () { - context.pushRoute(ChatRoomRoute(id: room.id)); - }, + onTap: onTap, ); } } @@ -88,9 +86,32 @@ Future> chatroomsJoined(Ref ref) async { .toList(); } +@RoutePage() +class ChatShellScreen extends HookConsumerWidget { + const ChatShellScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isWide = isWideScreen(context); + + if (isWide) { + return Row( + children: [ + SizedBox(width: 320, child: ChatListScreen(isAside: true)), + VerticalDivider(width: 1), + Expanded(child: AutoRouter()), + ], + ); + } + + return AutoRouter(); + } +} + @RoutePage() class ChatListScreen extends HookConsumerWidget { - const ChatListScreen({super.key}); + final bool isAside; + const ChatListScreen({super.key, this.isAside = false}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -112,6 +133,12 @@ class ChatListScreen extends HookConsumerWidget { } } + final isWide = isWideScreen(context); + + if (isWide && !isAside) { + return SizedBox.shrink(); + } + return AppScaffold( appBar: AppBar( title: Text('chat').tr(), @@ -191,15 +218,25 @@ class ChatListScreen extends HookConsumerWidget { itemCount: items.length, itemBuilder: (context, index) { final item = items[index]; - return ChatRoomListTile(room: item, isDirect: item.type == 1); + return ChatRoomListTile( + room: item, + isDirect: item.type == 1, + onTap: () { + if (isWide) { + context.router.replace(ChatRoomRoute(id: item.id)); + } else { + context.router.push(ChatRoomRoute(id: item.id)); + } + }, + ); }, ), ), loading: () => const Center(child: CircularProgressIndicator()), error: - (error, stack) => GestureDetector( - child: Center(child: Text('Error: $error')), - onTap: () { + (error, stack) => ResponseErrorWidget( + error: error, + onRetry: () { ref.invalidate(chatroomsJoinedProvider); }, ), diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index ae6d343..edeea09 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -17,7 +17,9 @@ import 'package:island/pods/network.dart'; import 'package:island/pods/websocket.dart'; import 'package:island/route.gr.dart'; import 'package:island/screens/posts/compose.dart'; +import 'package:island/services/responsive.dart'; import 'package:island/widgets/alert.dart'; +import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/chat/message_item.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/response.dart'; @@ -415,46 +417,96 @@ class ChatRoomScreen extends HookConsumerWidget { } } + final compactHeader = isWideScreen(context); + return Scaffold( appBar: AppBar( - toolbarHeight: 64, + leading: !compactHeader ? const Center(child: PageBackButton()) : null, + automaticallyImplyLeading: false, + toolbarHeight: compactHeader ? null : 64, title: chatRoom.when( data: - (room) => Column( - spacing: 4, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - height: 26, - width: 26, - child: - (room!.type == 1 && room.pictureId == null) - ? SplitAvatarWidget( - filesId: - room.members! - .map((e) => e.account.profile.pictureId) - .toList(), - ) - : room.pictureId != null - ? ProfilePictureWidget( - fileId: room.pictureId, - fallbackIcon: Symbols.chat, - ) - : CircleAvatar( - child: Text( - room.name![0].toUpperCase(), - style: const TextStyle(fontSize: 12), - ), - ), - ), - Text( - (room.type == 1 && room.name == null) - ? room.members!.map((e) => e.account.nick).join(', ') - : room.name!, - ).fontSize(15), - ], - ), + (room) => + compactHeader + ? Row( + spacing: 8, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: 26, + width: 26, + child: + (room!.type == 1 && room.pictureId == null) + ? SplitAvatarWidget( + filesId: + room.members! + .map( + (e) => + e.account.profile.pictureId, + ) + .toList(), + ) + : room.pictureId != null + ? ProfilePictureWidget( + fileId: room.pictureId, + fallbackIcon: Symbols.chat, + ) + : CircleAvatar( + child: Text( + room.name![0].toUpperCase(), + style: const TextStyle(fontSize: 12), + ), + ), + ), + Text( + (room.type == 1 && room.name == null) + ? room.members! + .map((e) => e.account.nick) + .join(', ') + : room.name!, + ).fontSize(19), + ], + ) + : Column( + spacing: 4, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: 26, + width: 26, + child: + (room!.type == 1 && room.pictureId == null) + ? SplitAvatarWidget( + filesId: + room.members! + .map( + (e) => + e.account.profile.pictureId, + ) + .toList(), + ) + : room.pictureId != null + ? ProfilePictureWidget( + fileId: room.pictureId, + fallbackIcon: Symbols.chat, + ) + : CircleAvatar( + child: Text( + room.name![0].toUpperCase(), + style: const TextStyle(fontSize: 12), + ), + ), + ), + Text( + (room.type == 1 && room.name == null) + ? room.members! + .map((e) => e.account.nick) + .join(', ') + : room.name!, + ).fontSize(15), + ], + ), loading: () => const Text('Loading...'), error: (err, __) => ResponseErrorWidget( diff --git a/lib/services/responsive.dart b/lib/services/responsive.dart index 37db4a4..ab3728b 100644 --- a/lib/services/responsive.dart +++ b/lib/services/responsive.dart @@ -1 +1,17 @@ -const kWideScreenWidth = 640; +import 'package:flutter/widgets.dart'; + +const kWideScreenWidth = 768; +const kWiderScreenWidth = 1024; +const kWidescreenWidth = 1280; + +bool isWideScreen(BuildContext context) { + return MediaQuery.of(context).size.width > kWideScreenWidth; +} + +bool isWiderScreen(BuildContext context) { + return MediaQuery.of(context).size.width > kWiderScreenWidth; +} + +bool isWidestScreen(BuildContext context) { + return MediaQuery.of(context).size.width > kWidescreenWidth; +} diff --git a/lib/widgets/chat/message_item.dart b/lib/widgets/chat/message_item.dart index 9970e23..f32690a 100644 --- a/lib/widgets/chat/message_item.dart +++ b/lib/widgets/chat/message_item.dart @@ -151,92 +151,77 @@ class MessageItem extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.end, children: [ Flexible( - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - decoration: BoxDecoration( - color: containerColor, - borderRadius: BorderRadius.circular(16), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (remoteMessage.repliedMessageId != null) - MessageQuoteWidget( - message: message, - textColor: textColor, - isReply: true, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (remoteMessage.repliedMessageId != null) + MessageQuoteWidget( + message: message, + textColor: textColor, + isReply: true, + ), + if (remoteMessage.forwardedMessageId != null) + MessageQuoteWidget( + message: message, + textColor: textColor, + isReply: false, + ), + if (remoteMessage.content?.isNotEmpty ?? false) + MarkdownTextContent( + content: remoteMessage.content!, + isSelectable: true, + linkStyle: TextStyle(color: linkColor), + textStyle: TextStyle( + color: textColor, + fontSize: 14, ), - if (remoteMessage.forwardedMessageId != null) - MessageQuoteWidget( - message: message, - textColor: textColor, - isReply: false, - ), - if (remoteMessage.content?.isNotEmpty ?? false) - MarkdownTextContent( - content: remoteMessage.content!, - isSelectable: true, - linkStyle: TextStyle(color: linkColor), - textStyle: TextStyle( - color: textColor, - fontSize: 14, - ), - ), - if (remoteMessage.attachments.isNotEmpty) - CloudFileList( - files: remoteMessage.attachments, - maxWidth: MediaQuery.of(context).size.width * 0.8, - ).padding(top: 4), - if (progress != null && progress!.isNotEmpty) - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - spacing: 8, - children: [ - if ((remoteMessage.content?.isNotEmpty ?? - false)) - const Gap(0), - for (var entry in progress!.entries) - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - 'fileUploadingProgress'.tr( - args: [ - (entry.key + 1).toString(), - entry.value.toStringAsFixed(1), - ], - ), - style: TextStyle( - fontSize: 12, - color: textColor.withOpacity(0.8), - ), - ), - const Gap(4), - LinearProgressIndicator( - value: entry.value / 100, - backgroundColor: - Theme.of( - context, - ).colorScheme.surfaceVariant, - valueColor: - AlwaysStoppedAnimation( - Theme.of( - context, - ).colorScheme.primary, - ), - ), - ], - ), + ), + if (remoteMessage.attachments.isNotEmpty) + CloudFileList( + files: remoteMessage.attachments, + maxWidth: MediaQuery.of(context).size.width * 0.8, + ).padding(top: 4), + if (progress != null && progress!.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 8, + children: [ + if ((remoteMessage.content?.isNotEmpty ?? false)) const Gap(0), - ], - ), - ], - ), - ), + for (var entry in progress!.entries) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'fileUploadingProgress'.tr( + args: [ + (entry.key + 1).toString(), + entry.value.toStringAsFixed(1), + ], + ), + style: TextStyle( + fontSize: 12, + color: textColor.withOpacity(0.8), + ), + ), + const Gap(4), + LinearProgressIndicator( + value: entry.value / 100, + backgroundColor: + Theme.of( + context, + ).colorScheme.surfaceVariant, + valueColor: AlwaysStoppedAnimation( + Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + const Gap(0), + ], + ), + ], + ).padding(left: 40), ), _buildMessageIndicators( context,