feat: UI update

This commit is contained in:
xkeyC 2025-11-19 21:14:36 +08:00
parent 2ffb02e62d
commit 6fda5628ff
10 changed files with 126 additions and 65 deletions

View File

@ -28,9 +28,10 @@ class S {
static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); static const AppLocalizationDelegate delegate = AppLocalizationDelegate();
static Future<S> load(Locale locale) { static Future<S> load(Locale locale) {
final name = (locale.countryCode?.isEmpty ?? false) final name =
? locale.languageCode (locale.countryCode?.isEmpty ?? false)
: locale.toString(); ? locale.languageCode
: locale.toString();
final localeName = Intl.canonicalizedLocale(name); final localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((_) { return initializeMessages(localeName).then((_) {
Intl.defaultLocale = localeName; Intl.defaultLocale = localeName;

View File

@ -889,4 +889,8 @@ class PartyRoom extends _$PartyRoom {
_stopEventStream(); _stopEventStream();
_confBox?.close(); _confBox?.close();
} }
common.Tag? getMainTagById(String mainTagId) {
return state.room.tags[mainTagId];
}
} }

View File

@ -1,9 +1,10 @@
import 'package:fixnum/fixnum.dart';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/provider/party_room.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/party_room_ui_model.dart';
import 'package:starcitizen_doctor/ui/party_room/utils/party_room_utils.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart'; import 'package:starcitizen_doctor/widgets/widgets.dart';
class UserAvatarWidget extends HookConsumerWidget { class UserAvatarWidget extends HookConsumerWidget {
@ -18,12 +19,12 @@ class UserAvatarWidget extends HookConsumerWidget {
final isLoggedIn = partyRoomState.auth.isLoggedIn; final isLoggedIn = partyRoomState.auth.isLoggedIn;
final userName = partyRoomState.auth.userInfo?.gameUserId ?? S.current.user_not_logged_in; final userName = partyRoomState.auth.userInfo?.gameUserId ?? S.current.user_not_logged_in;
final avatarUrl = partyRoomState.auth.userInfo?.avatarUrl; final avatarUrl = partyRoomState.auth.userInfo?.avatarUrl;
final enlistedDate = partyRoomState.auth.userInfo?.enlistedDate;
final fullAvatarUrl = (avatarUrl != null && avatarUrl.isNotEmpty) ? '${URLConf.rsiAvatarBaseUrl}$avatarUrl' : null; final fullAvatarUrl = (avatarUrl != null && avatarUrl.isNotEmpty) ? '${URLConf.rsiAvatarBaseUrl}$avatarUrl' : null;
final uuid = ref.read(appGlobalModelProvider).deviceUUID;
return HoverButton( return HoverButton(
onPressed: () { onPressed: () {
if (isLoggedIn) { if (isLoggedIn) {
_showAccountCard(context, ref, userName, fullAvatarUrl, uuid); _showAccountCard(context, ref, userName, fullAvatarUrl, enlistedDate);
} else { } else {
onTapNavigateToPartyRoom(); 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 targetContext = context;
final box = targetContext.findRenderObject() as RenderBox?; final box = targetContext.findRenderObject() as RenderBox?;
final offset = box?.localToGlobal(Offset.zero) ?? Offset.zero; final offset = box?.localToGlobal(Offset.zero) ?? Offset.zero;
@ -139,7 +140,7 @@ class UserAvatarWidget extends HookConsumerWidget {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'$uuid', '注册时间:${PartyRoomUtils.formatDateTime(enlistedDate)}',
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .6)), style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .6)),
), ),
], ],

View File

@ -1,6 +1,5 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/provider/party_room.dart';
import 'package:starcitizen_doctor/ui/party_room/party_room_ui_model.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'; import 'package:starcitizen_doctor/ui/party_room/widgets/party_room_connect_page.dart';
@ -27,14 +26,22 @@ class PartyRoomUI extends HookConsumerWidget {
widget = PartyRoomDetailPage(); widget = PartyRoomDetailPage();
} }
return LocalHeroScope( return AnimatedSwitcher(
duration: Duration(milliseconds: 180), duration: const Duration(milliseconds: 230),
child: AnimatedSwitcher( switchInCurve: Curves.easeInOut,
duration: const Duration(milliseconds: 230), switchOutCurve: Curves.easeInOut,
switchInCurve: Curves.easeInOut, transitionBuilder: (Widget child, Animation<double> animation) {
switchOutCurve: Curves.easeInOut, final offsetAnimation = Tween<Offset>(
child: widget, 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,
); );
} }
} }

View File

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

View File

@ -24,7 +24,7 @@ class CreateRoomDialog extends HookConsumerWidget {
final selectedMainTagData = selectedMainTag.value != null ? partyRoomState.room.tags[selectedMainTag.value] : null; final selectedMainTagData = selectedMainTag.value != null ? partyRoomState.room.tags[selectedMainTag.value] : null;
return ContentDialog( 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('创建房间'), title: const Text('创建房间'),
content: SizedBox( content: SizedBox(
child: SingleChildScrollView( child: SingleChildScrollView(
@ -55,11 +55,14 @@ class CreateRoomDialog extends HookConsumerWidget {
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(2),
), ),
), ),
Text(tag.name), Text(tag.name, style: TextStyle(fontSize: 16)),
if (tag.info.isNotEmpty) if (tag.info.isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.only(left: 8), 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), borderRadius: BorderRadius.circular(2),
), ),
), ),
Text(subTag.name), Text(subTag.name, style: TextStyle(fontSize: 16)),
if (subTag.info.isNotEmpty) if (subTag.info.isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.only(left: 8), 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( InfoLabel(
label: '社交链接 (可选)', label: '社交链接 (可选)',
child: TextBox(controller: socialLinksController, placeholder: 'https://discord.gg/xxxxx', maxLines: 1), child: TextBox(
controller: socialLinksController,
placeholder: '以 https:// 开头,目前仅支持 qq、discord、kook、oopz 链接',
maxLines: 1,
),
), ),
], ],
), ),

View File

@ -1,7 +1,6 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/generated/proto/partroom/partroom.pb.dart'; import 'package:starcitizen_doctor/generated/proto/partroom/partroom.pb.dart';
import 'package:starcitizen_doctor/provider/party_room.dart'; import 'package:starcitizen_doctor/provider/party_room.dart';
@ -246,7 +245,6 @@ class PartyRoomMemberItem extends ConsumerWidget {
child: CacheNetImage(url: avatarUrl), child: CacheNetImage(url: avatarUrl),
), ),
); );
if (isOwner) return LocalHero(tag: 'party_room_detail_hero', child: avatarWidget);
return avatarWidget; return avatarWidget;
} }
} }

View File

@ -1,12 +1,13 @@
import 'dart:ui'; import 'dart:ui';
import 'package:animate_do/animate_do.dart';
import 'package:extended_image/extended_image.dart'; import 'package:extended_image/extended_image.dart';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:flutter_tilt/flutter_tilt.dart'; import 'package:flutter_tilt/flutter_tilt.dart';
import 'package:hexcolor/hexcolor.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/generated/proto/partroom/partroom.pb.dart'; import 'package:starcitizen_doctor/generated/proto/partroom/partroom.pb.dart';
import 'package:starcitizen_doctor/provider/party_room.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}' : ''; final avatarUrl = owner.avatarUrl.isNotEmpty ? '${URLConf.rsiAvatarBaseUrl}${owner.avatarUrl}' : '';
return Container( return Bounce(
padding: const EdgeInsets.all(16), duration: Duration(milliseconds: 230),
alignment: Alignment.bottomRight, child: Container(
child: Tooltip( padding: const EdgeInsets.all(16),
message: '返回当前房间', alignment: Alignment.bottomRight,
child: GestureDetector( child: Tooltip(
onTap: () { message: '返回当前房间',
ref.read(partyRoomUIModelProvider.notifier).setMinimized(false); child: GestureDetector(
}, onTap: () {
child: LocalHero( ref.read(partyRoomUIModelProvider.notifier).setMinimized(false);
tag: 'party_room_detail_hero', },
child: Container( child: Container(
width: 56, width: 56,
height: 56, height: 56,
@ -196,13 +197,22 @@ class PartyRoomListPage extends HookConsumerWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ 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), 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), const SizedBox(height: 8),
Text('成为第一个创建房间的人吧!', style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: 0.5))), if (uiState.searchOwnerName.isEmpty) ...[
const SizedBox(height: 16), Text('成为第一个创建房间的人吧!', style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: 0.5))),
FilledButton(onPressed: () => _showCreateRoomDialog(context, ref), child: const Text('创建房间')), 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 avatarUrl = room.ownerAvatar.isNotEmpty ? '${URLConf.rsiAvatarBaseUrl}${room.ownerAvatar}' : '';
final partyRoomState = ref.watch(partyRoomProvider); final partyRoomState = ref.watch(partyRoomProvider);
final isCurrentRoom = partyRoomState.room.isInRoom && partyRoomState.room.roomUuid == room.roomUuid; final isCurrentRoom = partyRoomState.room.isInRoom && partyRoomState.room.roomUuid == room.roomUuid;
return GridItemAnimator( return GridItemAnimator(
index: index, index: index,
child: GestureDetector( child: GestureDetector(
@ -338,18 +347,7 @@ class PartyRoomListPage extends HookConsumerWidget {
spacing: 6, spacing: 6,
runSpacing: 6, runSpacing: 6,
children: [ children: [
if (room.mainTagId.isNotEmpty) makeTagContainer(partyRoom, room),
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)),
),
),
if (room.socialLinks.isNotEmpty) if (room.socialLinks.isNotEmpty)
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), 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<void> _showCreateRoomDialog(BuildContext context, WidgetRef ref) async { Future<void> _showCreateRoomDialog(BuildContext context, WidgetRef ref) async {
await showDialog(context: context, builder: (context) => const CreateRoomDialog()); await showDialog(context: context, builder: (context) => const CreateRoomDialog());
} }

View File

@ -33,6 +33,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.13.7" 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: archive:
dependency: "direct main" dependency: "direct main"
description: description:
@ -830,14 +838,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" 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: logging:
dependency: transitive dependency: transitive
description: description:

View File

@ -36,7 +36,7 @@ dependencies:
markdown: ^7.3.0 markdown: ^7.3.0
markdown_widget: ^2.3.2+8 markdown_widget: ^2.3.2+8
extended_image: ^10.0.1 extended_image: ^10.0.1
local_hero: ^0.3.0 animate_do: ^4.2.0
device_info_plus: ^12.2.0 device_info_plus: ^12.2.0
file_picker: ^10.3.6 file_picker: ^10.3.6
file_sizes: ^1.0.6 file_sizes: ^1.0.6