From 6fda5628ffefc69a65acc1a70a162d01073651c3 Mon Sep 17 00:00:00 2001 From: xkeyC <3334969096@qq.com> Date: Wed, 19 Nov 2025 21:14:36 +0800 Subject: [PATCH] feat: UI update --- lib/generated/l10n.dart | 7 +- lib/provider/party_room.dart | 4 + .../index_ui_widgets/user_avatar_widget.dart | 11 +-- lib/ui/party_room/party_room_ui.dart | 25 +++--- lib/ui/party_room/utils/party_room_utils.dart | 22 +++++ .../widgets/create_room_dialog.dart | 22 +++-- .../detail/party_room_member_list.dart | 2 - .../widgets/party_room_list_page.dart | 80 ++++++++++++------- pubspec.lock | 16 ++-- pubspec.yaml | 2 +- 10 files changed, 126 insertions(+), 65 deletions(-) create mode 100644 lib/ui/party_room/utils/party_room_utils.dart diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 249b16c..0f62308 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -28,9 +28,10 @@ class S { static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); static Future load(Locale locale) { - final name = (locale.countryCode?.isEmpty ?? false) - ? locale.languageCode - : locale.toString(); + final name = + (locale.countryCode?.isEmpty ?? false) + ? locale.languageCode + : locale.toString(); final localeName = Intl.canonicalizedLocale(name); return initializeMessages(localeName).then((_) { Intl.defaultLocale = localeName; diff --git a/lib/provider/party_room.dart b/lib/provider/party_room.dart index 8850ca3..ddf6a3a 100644 --- a/lib/provider/party_room.dart +++ b/lib/provider/party_room.dart @@ -889,4 +889,8 @@ class PartyRoom extends _$PartyRoom { _stopEventStream(); _confBox?.close(); } + + common.Tag? getMainTagById(String mainTagId) { + return state.room.tags[mainTagId]; + } } diff --git a/lib/ui/index_ui_widgets/user_avatar_widget.dart b/lib/ui/index_ui_widgets/user_avatar_widget.dart index 221b6c1..9478ad2 100644 --- a/lib/ui/index_ui_widgets/user_avatar_widget.dart +++ b/lib/ui/index_ui_widgets/user_avatar_widget.dart @@ -1,9 +1,10 @@ +import 'package:fixnum/fixnum.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:starcitizen_doctor/app.dart'; import 'package:starcitizen_doctor/common/conf/url_conf.dart'; import 'package:starcitizen_doctor/provider/party_room.dart'; import 'package:starcitizen_doctor/ui/party_room/party_room_ui_model.dart'; +import 'package:starcitizen_doctor/ui/party_room/utils/party_room_utils.dart'; import 'package:starcitizen_doctor/widgets/widgets.dart'; class UserAvatarWidget extends HookConsumerWidget { @@ -18,12 +19,12 @@ class UserAvatarWidget extends HookConsumerWidget { final isLoggedIn = partyRoomState.auth.isLoggedIn; final userName = partyRoomState.auth.userInfo?.gameUserId ?? S.current.user_not_logged_in; final avatarUrl = partyRoomState.auth.userInfo?.avatarUrl; + final enlistedDate = partyRoomState.auth.userInfo?.enlistedDate; final fullAvatarUrl = (avatarUrl != null && avatarUrl.isNotEmpty) ? '${URLConf.rsiAvatarBaseUrl}$avatarUrl' : null; - final uuid = ref.read(appGlobalModelProvider).deviceUUID; return HoverButton( onPressed: () { if (isLoggedIn) { - _showAccountCard(context, ref, userName, fullAvatarUrl, uuid); + _showAccountCard(context, ref, userName, fullAvatarUrl, enlistedDate); } else { onTapNavigateToPartyRoom(); } @@ -75,7 +76,7 @@ class UserAvatarWidget extends HookConsumerWidget { ); } - void _showAccountCard(BuildContext context, WidgetRef ref, String userName, String? avatarUrl, String? uuid) { + void _showAccountCard(BuildContext context, WidgetRef ref, String userName, String? avatarUrl, Int64? enlistedDate) { final targetContext = context; final box = targetContext.findRenderObject() as RenderBox?; final offset = box?.localToGlobal(Offset.zero) ?? Offset.zero; @@ -139,7 +140,7 @@ class UserAvatarWidget extends HookConsumerWidget { ), const SizedBox(height: 4), Text( - '$uuid', + '注册时间:${PartyRoomUtils.formatDateTime(enlistedDate)}', style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .6)), ), ], diff --git a/lib/ui/party_room/party_room_ui.dart b/lib/ui/party_room/party_room_ui.dart index ab644e0..f7cb81a 100644 --- a/lib/ui/party_room/party_room_ui.dart +++ b/lib/ui/party_room/party_room_ui.dart @@ -1,6 +1,5 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:local_hero/local_hero.dart'; import 'package:starcitizen_doctor/provider/party_room.dart'; import 'package:starcitizen_doctor/ui/party_room/party_room_ui_model.dart'; import 'package:starcitizen_doctor/ui/party_room/widgets/party_room_connect_page.dart'; @@ -27,14 +26,22 @@ class PartyRoomUI extends HookConsumerWidget { widget = PartyRoomDetailPage(); } - return LocalHeroScope( - duration: Duration(milliseconds: 180), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 230), - switchInCurve: Curves.easeInOut, - switchOutCurve: Curves.easeInOut, - child: widget, - ), + return AnimatedSwitcher( + duration: const Duration(milliseconds: 230), + switchInCurve: Curves.easeInOut, + switchOutCurve: Curves.easeInOut, + transitionBuilder: (Widget child, Animation animation) { + final offsetAnimation = Tween( + begin: const Offset(0, 0.08), + end: Offset.zero, + ).chain(CurveTween(curve: Curves.easeInOut)).animate(animation); + + return SlideTransition( + position: offsetAnimation, + child: FadeTransition(opacity: animation, child: child), + ); + }, + child: widget, ); } } diff --git a/lib/ui/party_room/utils/party_room_utils.dart b/lib/ui/party_room/utils/party_room_utils.dart new file mode 100644 index 0000000..5fa218a --- /dev/null +++ b/lib/ui/party_room/utils/party_room_utils.dart @@ -0,0 +1,22 @@ +import 'package:fixnum/fixnum.dart'; +import 'package:intl/intl.dart'; + +class PartyRoomUtils { + static final DateFormat dateTimeFormatter = DateFormat('yyyy-MM-dd HH:mm:ss'); + + /// rpc Int64 时间戳 转 DateTime + static DateTime? getDateTime(Int64? timestamp) { + if (timestamp == null || timestamp == Int64.ZERO) { + return null; + } + return DateTime.fromMillisecondsSinceEpoch(timestamp.toInt() * 1000); + } + + static String? formatDateTime(Int64? timestamp) { + final dateTime = getDateTime(timestamp); + if (dateTime == null) { + return null; + } + return dateTimeFormatter.format(dateTime); + } +} diff --git a/lib/ui/party_room/widgets/create_room_dialog.dart b/lib/ui/party_room/widgets/create_room_dialog.dart index db82cd9..bc2bc68 100644 --- a/lib/ui/party_room/widgets/create_room_dialog.dart +++ b/lib/ui/party_room/widgets/create_room_dialog.dart @@ -24,7 +24,7 @@ class CreateRoomDialog extends HookConsumerWidget { final selectedMainTagData = selectedMainTag.value != null ? partyRoomState.room.tags[selectedMainTag.value] : null; return ContentDialog( - constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.4), + constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.5), title: const Text('创建房间'), content: SizedBox( child: SingleChildScrollView( @@ -55,11 +55,14 @@ class CreateRoomDialog extends HookConsumerWidget { borderRadius: BorderRadius.circular(2), ), ), - Text(tag.name), + Text(tag.name, style: TextStyle(fontSize: 16)), if (tag.info.isNotEmpty) Padding( padding: const EdgeInsets.only(left: 8), - child: Text(tag.info, style: TextStyle(fontSize: 11, color: Colors.grey[100])), + child: Text( + tag.info, + style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .7)), + ), ), ], ), @@ -99,11 +102,14 @@ class CreateRoomDialog extends HookConsumerWidget { borderRadius: BorderRadius.circular(2), ), ), - Text(subTag.name), + Text(subTag.name, style: TextStyle(fontSize: 16)), if (subTag.info.isNotEmpty) Padding( padding: const EdgeInsets.only(left: 8), - child: Text(subTag.info, style: TextStyle(fontSize: 11, color: Colors.grey[100])), + child: Text( + subTag.info, + style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .7)), + ), ), ], ), @@ -159,7 +165,11 @@ class CreateRoomDialog extends HookConsumerWidget { InfoLabel( label: '社交链接 (可选)', - child: TextBox(controller: socialLinksController, placeholder: 'https://discord.gg/xxxxx', maxLines: 1), + child: TextBox( + controller: socialLinksController, + placeholder: '以 https:// 开头,目前仅支持 qq、discord、kook、oopz 链接', + maxLines: 1, + ), ), ], ), diff --git a/lib/ui/party_room/widgets/detail/party_room_member_list.dart b/lib/ui/party_room/widgets/detail/party_room_member_list.dart index f1d12c4..7c539b0 100644 --- a/lib/ui/party_room/widgets/detail/party_room_member_list.dart +++ b/lib/ui/party_room/widgets/detail/party_room_member_list.dart @@ -1,7 +1,6 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:local_hero/local_hero.dart'; import 'package:starcitizen_doctor/common/conf/url_conf.dart'; import 'package:starcitizen_doctor/generated/proto/partroom/partroom.pb.dart'; import 'package:starcitizen_doctor/provider/party_room.dart'; @@ -246,7 +245,6 @@ class PartyRoomMemberItem extends ConsumerWidget { child: CacheNetImage(url: avatarUrl), ), ); - if (isOwner) return LocalHero(tag: 'party_room_detail_hero', child: avatarWidget); return avatarWidget; } } diff --git a/lib/ui/party_room/widgets/party_room_list_page.dart b/lib/ui/party_room/widgets/party_room_list_page.dart index d21a6e8..b687eed 100644 --- a/lib/ui/party_room/widgets/party_room_list_page.dart +++ b/lib/ui/party_room/widgets/party_room_list_page.dart @@ -1,12 +1,13 @@ import 'dart:ui'; +import 'package:animate_do/animate_do.dart'; import 'package:extended_image/extended_image.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:flutter_tilt/flutter_tilt.dart'; +import 'package:hexcolor/hexcolor.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:local_hero/local_hero.dart'; import 'package:starcitizen_doctor/common/conf/url_conf.dart'; import 'package:starcitizen_doctor/generated/proto/partroom/partroom.pb.dart'; import 'package:starcitizen_doctor/provider/party_room.dart'; @@ -105,17 +106,17 @@ class PartyRoomListPage extends HookConsumerWidget { ); final avatarUrl = owner.avatarUrl.isNotEmpty ? '${URLConf.rsiAvatarBaseUrl}${owner.avatarUrl}' : ''; - return Container( - padding: const EdgeInsets.all(16), - alignment: Alignment.bottomRight, - child: Tooltip( - message: '返回当前房间', - child: GestureDetector( - onTap: () { - ref.read(partyRoomUIModelProvider.notifier).setMinimized(false); - }, - child: LocalHero( - tag: 'party_room_detail_hero', + return Bounce( + duration: Duration(milliseconds: 230), + child: Container( + padding: const EdgeInsets.all(16), + alignment: Alignment.bottomRight, + child: Tooltip( + message: '返回当前房间', + child: GestureDetector( + onTap: () { + ref.read(partyRoomUIModelProvider.notifier).setMinimized(false); + }, child: Container( width: 56, height: 56, @@ -196,13 +197,22 @@ class PartyRoomListPage extends HookConsumerWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(FluentIcons.room, size: 48, color: Colors.grey.withValues(alpha: 0.6)), + Icon(FluentIcons.room, size: 48, color: Colors.white.withValues(alpha: 0.6)), const SizedBox(height: 16), - Text('暂无房间', style: TextStyle(color: Colors.white.withValues(alpha: 0.7))), + Text( + uiState.searchOwnerName.isNotEmpty + ? '没有找到符合条件的房间' + : uiState.selectedMainTagId != null + ? '当前分类下没有房间' + : '暂无可用房间', + style: TextStyle(color: Colors.white.withValues(alpha: 0.7)), + ), const SizedBox(height: 8), - Text('成为第一个创建房间的人吧!', style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: 0.5))), - const SizedBox(height: 16), - FilledButton(onPressed: () => _showCreateRoomDialog(context, ref), child: const Text('创建房间')), + if (uiState.searchOwnerName.isEmpty) ...[ + Text('成为第一个创建房间的人吧!', style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: 0.5))), + const SizedBox(height: 16), + FilledButton(onPressed: () => _showCreateRoomDialog(context, ref), child: const Text('创建房间')), + ], ], ), ); @@ -237,11 +247,10 @@ class PartyRoomListPage extends HookConsumerWidget { ); } - Widget _buildRoomCard(BuildContext context, WidgetRef ref, PartyRoom partyRoom, dynamic room, int index) { + Widget _buildRoomCard(BuildContext context, WidgetRef ref, PartyRoom partyRoom, RoomListItem room, int index) { final avatarUrl = room.ownerAvatar.isNotEmpty ? '${URLConf.rsiAvatarBaseUrl}${room.ownerAvatar}' : ''; final partyRoomState = ref.watch(partyRoomProvider); final isCurrentRoom = partyRoomState.room.isInRoom && partyRoomState.room.roomUuid == room.roomUuid; - return GridItemAnimator( index: index, child: GestureDetector( @@ -338,18 +347,7 @@ class PartyRoomListPage extends HookConsumerWidget { spacing: 6, runSpacing: 6, children: [ - if (room.mainTagId.isNotEmpty) - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: const Color(0xFF4A9EFF).withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - room.mainTagId, - style: const TextStyle(fontSize: 11, color: Color(0xFF4A9EFF)), - ), - ), + makeTagContainer(partyRoom, room), if (room.socialLinks.isNotEmpty) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), @@ -382,6 +380,26 @@ class PartyRoomListPage extends HookConsumerWidget { ); } + Widget makeTagContainer(PartyRoom partyRoom, RoomListItem room) { + final tag = partyRoom.getMainTagById(room.mainTagId); + final subTag = tag?.subTags.where((e) => e.id == room.subTagId).firstOrNull; + + Widget buildContainer(String name, Color color) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(4)), + child: Text(name, style: const TextStyle(fontSize: 11, color: Colors.white)), + ); + } + + return Row( + children: [ + buildContainer(tag?.name ?? "<${tag?.id}>", HexColor(tag?.color ?? "#0096FF")), + if (subTag != null) ...[const SizedBox(width: 4), buildContainer(subTag.name, HexColor(subTag.color))], + ], + ); + } + Future _showCreateRoomDialog(BuildContext context, WidgetRef ref) async { await showDialog(context: context, builder: (context) => const CreateRoomDialog()); } diff --git a/pubspec.lock b/pubspec.lock index 08c7c1e..f6b7688 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.7" + animate_do: + dependency: "direct main" + description: + name: animate_do + sha256: e5c8b92e8495cba5adfff17c0b017d50f46b2766226e9faaf68bc08c91aef034 + url: "https://pub.dev" + source: hosted + version: "4.2.0" archive: dependency: "direct main" description: @@ -830,14 +838,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" - local_hero: - dependency: "direct main" - description: - name: local_hero - sha256: "5c85451dd51ecd0e8d3656775fac9a6db82f296f200d9931217186d34fed6089" - url: "https://pub.dev" - source: hosted - version: "0.3.0" logging: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 65477e3..40e606e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,7 +36,7 @@ dependencies: markdown: ^7.3.0 markdown_widget: ^2.3.2+8 extended_image: ^10.0.1 - local_hero: ^0.3.0 + animate_do: ^4.2.0 device_info_plus: ^12.2.0 file_picker: ^10.3.6 file_sizes: ^1.0.6