⚗️ Testing desktop layout

This commit is contained in:
LittleSheep 2025-05-18 23:50:50 +08:00
parent ad41de3674
commit 911439f3af
7 changed files with 342 additions and 168 deletions

View File

@ -16,7 +16,17 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: ExploreRoute.page, path: 'explore'), AutoRoute(page: ExploreRoute.page, path: 'explore'),
AutoRoute(page: AccountRoute.page, path: 'account'), AutoRoute(page: AccountRoute.page, path: 'account'),
AutoRoute(page: RealmListRoute.page, path: 'realms'), 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'), AutoRoute(page: WalletRoute.page, path: '/wallet'),
@ -47,10 +57,6 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: NewRealmRoute.page, path: '/realms/new'), AutoRoute(page: NewRealmRoute.page, path: '/realms/new'),
AutoRoute(page: RealmDetailRoute.page, path: '/realms/:slug'), AutoRoute(page: RealmDetailRoute.page, path: '/realms/:slug'),
AutoRoute(page: EditRealmRoute.page, path: '/realms/:slug/edit'), 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: CreatorHubRoute.page, path: '/creators'),
AutoRoute(page: StickersRoute.page, path: '/creators/:name/stickers'), AutoRoute(page: StickersRoute.page, path: '/creators/:name/stickers'),
AutoRoute( AutoRoute(

View File

@ -209,6 +209,22 @@ class ChatRoomRouteArgs {
} }
} }
/// generated route for
/// [_i5.ChatShellScreen]
class ChatShellRoute extends _i25.PageRouteInfo<void> {
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 /// generated route for
/// [_i7.CreateAccountScreen] /// [_i7.CreateAccountScreen]
class CreateAccountRoute extends _i25.PageRouteInfo<void> { class CreateAccountRoute extends _i25.PageRouteInfo<void> {

View File

@ -1,6 +1,7 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
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:gap/gap.dart';
import 'package:island/route.gr.dart'; import 'package:island/route.gr.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
@ -11,46 +12,107 @@ class TabsScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final useHorizontalLayout = final useHorizontalLayout = isWideScreen(context);
MediaQuery.of(context).size.width > kWideScreenWidth; final useExpandableLayout = isWidestScreen(context);
return AutoTabsRouter.pageView( final destinations = [
routes: const [ NavigationDestination(
ExploreRoute(), label: 'explore'.tr(),
ChatListRoute(), icon: const Icon(Symbols.explore),
RealmListRoute(), ),
AccountRoute(), 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, scrollDirection: useHorizontalLayout ? Axis.vertical : Axis.horizontal,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
builder: (context, child, _) { builder: (context, child, _) {
final tabsRouter = AutoTabsRouter.of(context); 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( return Scaffold(
extendBodyBehindAppBar: true, extendBodyBehindAppBar: true,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
body: child, body:
bottomNavigationBar: NavigationBar( useHorizontalLayout
selectedIndex: tabsRouter.activeIndex, ? Row(
onDestinationSelected: tabsRouter.setActiveIndex, children: [
destinations: [ if (isTabRoute)
NavigationDestination( Column(
label: 'explore'.tr(), children: [
icon: const Icon(Symbols.explore), Gap(MediaQuery.of(context).padding.top + 8),
), if (useExpandableLayout)
NavigationDestination( Expanded(
label: 'chat'.tr(), child: NavigationDrawer(
icon: const Icon(Symbols.chat), backgroundColor: Colors.transparent,
), children: [
NavigationDestination( for (final destination in destinations)
label: 'realms'.tr(), NavigationDrawerDestination(
icon: const Icon(Symbols.workspaces), label: Text(destination.label),
), icon: destination.icon,
NavigationDestination( ),
label: 'account'.tr(), ],
icon: const Icon(Symbols.account_circle), ),
), )
], 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,
); );
}, },
); );

View File

@ -16,11 +16,13 @@ import 'package:island/pods/network.dart';
import 'package:island/route.gr.dart'; import 'package:island/route.gr.dart';
import 'package:island/screens/realm/realms.dart'; import 'package:island/screens/realm/realms.dart';
import 'package:island/services/file.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/account/account_picker.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/realms/selection_dropdown.dart'; import 'package:island/widgets/realms/selection_dropdown.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.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.members!.map((e) => '@${e.account.name}').join(', '))
: Text(room.description ?? 'descriptionNone'.tr()), : Text(room.description ?? 'descriptionNone'.tr()),
trailing: trailing, trailing: trailing,
onTap: onTap: onTap,
onTap ??
() {
context.pushRoute(ChatRoomRoute(id: room.id));
},
); );
} }
} }
@ -88,9 +86,32 @@ Future<List<SnChatRoom>> chatroomsJoined(Ref ref) async {
.toList(); .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() @RoutePage()
class ChatListScreen extends HookConsumerWidget { class ChatListScreen extends HookConsumerWidget {
const ChatListScreen({super.key}); final bool isAside;
const ChatListScreen({super.key, this.isAside = false});
@override @override
Widget build(BuildContext context, WidgetRef ref) { 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( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: Text('chat').tr(), title: Text('chat').tr(),
@ -191,15 +218,25 @@ class ChatListScreen extends HookConsumerWidget {
itemCount: items.length, itemCount: items.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = items[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()), loading: () => const Center(child: CircularProgressIndicator()),
error: error:
(error, stack) => GestureDetector( (error, stack) => ResponseErrorWidget(
child: Center(child: Text('Error: $error')), error: error,
onTap: () { onRetry: () {
ref.invalidate(chatroomsJoinedProvider); ref.invalidate(chatroomsJoinedProvider);
}, },
), ),

View File

@ -17,7 +17,9 @@ import 'package:island/pods/network.dart';
import 'package:island/pods/websocket.dart'; import 'package:island/pods/websocket.dart';
import 'package:island/route.gr.dart'; import 'package:island/route.gr.dart';
import 'package:island/screens/posts/compose.dart'; import 'package:island/screens/posts/compose.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.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/chat/message_item.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart'; import 'package:island/widgets/response.dart';
@ -415,46 +417,96 @@ class ChatRoomScreen extends HookConsumerWidget {
} }
} }
final compactHeader = isWideScreen(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
toolbarHeight: 64, leading: !compactHeader ? const Center(child: PageBackButton()) : null,
automaticallyImplyLeading: false,
toolbarHeight: compactHeader ? null : 64,
title: chatRoom.when( title: chatRoom.when(
data: data:
(room) => Column( (room) =>
spacing: 4, compactHeader
mainAxisAlignment: MainAxisAlignment.center, ? Row(
crossAxisAlignment: CrossAxisAlignment.center, spacing: 8,
children: [ crossAxisAlignment: CrossAxisAlignment.center,
SizedBox( children: [
height: 26, SizedBox(
width: 26, height: 26,
child: width: 26,
(room!.type == 1 && room.pictureId == null) child:
? SplitAvatarWidget( (room!.type == 1 && room.pictureId == null)
filesId: ? SplitAvatarWidget(
room.members! filesId:
.map((e) => e.account.profile.pictureId) room.members!
.toList(), .map(
) (e) =>
: room.pictureId != null e.account.profile.pictureId,
? ProfilePictureWidget( )
fileId: room.pictureId, .toList(),
fallbackIcon: Symbols.chat, )
) : room.pictureId != null
: CircleAvatar( ? ProfilePictureWidget(
child: Text( fileId: room.pictureId,
room.name![0].toUpperCase(), fallbackIcon: Symbols.chat,
style: const TextStyle(fontSize: 12), )
), : CircleAvatar(
), child: Text(
), room.name![0].toUpperCase(),
Text( style: const TextStyle(fontSize: 12),
(room.type == 1 && room.name == null) ),
? room.members!.map((e) => e.account.nick).join(', ') ),
: room.name!, ),
).fontSize(15), 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...'), loading: () => const Text('Loading...'),
error: error:
(err, __) => ResponseErrorWidget( (err, __) => ResponseErrorWidget(

View File

@ -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;
}

View File

@ -151,92 +151,77 @@ class MessageItem extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Flexible( Flexible(
child: Container( child: Column(
padding: const EdgeInsets.symmetric( crossAxisAlignment: CrossAxisAlignment.start,
horizontal: 12, children: [
vertical: 8, if (remoteMessage.repliedMessageId != null)
), MessageQuoteWidget(
decoration: BoxDecoration( message: message,
color: containerColor, textColor: textColor,
borderRadius: BorderRadius.circular(16), isReply: true,
), ),
child: Column( if (remoteMessage.forwardedMessageId != null)
crossAxisAlignment: CrossAxisAlignment.start, MessageQuoteWidget(
children: [ message: message,
if (remoteMessage.repliedMessageId != null) textColor: textColor,
MessageQuoteWidget( isReply: false,
message: message, ),
textColor: textColor, if (remoteMessage.content?.isNotEmpty ?? false)
isReply: true, MarkdownTextContent(
content: remoteMessage.content!,
isSelectable: true,
linkStyle: TextStyle(color: linkColor),
textStyle: TextStyle(
color: textColor,
fontSize: 14,
), ),
if (remoteMessage.forwardedMessageId != null) ),
MessageQuoteWidget( if (remoteMessage.attachments.isNotEmpty)
message: message, CloudFileList(
textColor: textColor, files: remoteMessage.attachments,
isReply: false, maxWidth: MediaQuery.of(context).size.width * 0.8,
), ).padding(top: 4),
if (remoteMessage.content?.isNotEmpty ?? false) if (progress != null && progress!.isNotEmpty)
MarkdownTextContent( Column(
content: remoteMessage.content!, crossAxisAlignment: CrossAxisAlignment.stretch,
isSelectable: true, spacing: 8,
linkStyle: TextStyle(color: linkColor), children: [
textStyle: TextStyle( if ((remoteMessage.content?.isNotEmpty ?? false))
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<Color>(
Theme.of(
context,
).colorScheme.primary,
),
),
],
),
const Gap(0), 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<Color>(
Theme.of(context).colorScheme.primary,
),
),
],
),
const Gap(0),
],
),
],
).padding(left: 40),
), ),
_buildMessageIndicators( _buildMessageIndicators(
context, context,