diff --git a/lib/base/ui_model.dart b/lib/base/ui_model.dart index 962a3d9..8ebc002 100644 --- a/lib/base/ui_model.dart +++ b/lib/base/ui_model.dart @@ -1,4 +1,5 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:grpc/grpc.dart'; import 'ui.dart'; @@ -62,13 +63,11 @@ class BaseUIModel extends ChangeNotifier { } catch (e) { dPrint("$runtimeType.handleError Error:$e"); String errorMsg = "Unknown Error"; - // if (e is AppHttpResultData && stringIsNotEmpty(e.msg)) { - // errorMsg = e.msg!; - // return null; - // } else { - // errorMsg = e.toString(); - // } - errorMsg = e.toString(); + if (e is GrpcError) { + errorMsg = "服务器错误: ${e.message} ?? Unknown Error"; + } else { + errorMsg = e.toString(); + } if (showFullScreenError) { uiErrorMsg = errorMsg; notifyListeners(); @@ -78,7 +77,8 @@ class BaseUIModel extends ChangeNotifier { showToast(context!, errorOverride ?? errorMsg, constraints: BoxConstraints( maxWidth: MediaQuery.of(context!).size.width * .6, - ),title: "出现错误!"); + ), + title: "出现错误!"); } } return null; diff --git a/lib/common/conf.dart b/lib/common/conf.dart index 6fd6240..14ce87a 100644 --- a/lib/common/conf.dart +++ b/lib/common/conf.dart @@ -47,6 +47,8 @@ class AppConf { static const gameChannels = ["LIVE", "PTU", "EPTU"]; + static String deviceUUID = ""; + static late final String applicationSupportDir; static AppVersionData? networkVersionData; @@ -83,6 +85,7 @@ class AppConf { await box.put("install_id", const Uuid().v4()); AnalyticsApi.touch("firstLaunch"); } + deviceUUID = box.get("install_id", defaultValue: ""); } catch (e) { exit(1); } diff --git a/lib/common/utils/input_utils.dart b/lib/common/utils/input_utils.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/generated/grpc/party_room_server/index.pb.dart b/lib/generated/grpc/party_room_server/index.pb.dart index 54a02d4..fb5ea57 100644 --- a/lib/generated/grpc/party_room_server/index.pb.dart +++ b/lib/generated/grpc/party_room_server/index.pb.dart @@ -517,6 +517,7 @@ class RoomData extends $pb.GeneratedMessage { $core.int? curPlayer, RoomStatus? status, $core.String? deviceUUID, + $core.String? announcement, }) { final $result = create(); if (id != null) { @@ -546,6 +547,9 @@ class RoomData extends $pb.GeneratedMessage { if (deviceUUID != null) { $result.deviceUUID = deviceUUID; } + if (announcement != null) { + $result.announcement = announcement; + } return $result; } RoomData._() : super(); @@ -562,6 +566,7 @@ class RoomData extends $pb.GeneratedMessage { ..a<$core.int>(7, _omitFieldNames ? '' : 'curPlayer', $pb.PbFieldType.O3, protoName: 'curPlayer') ..e(8, _omitFieldNames ? '' : 'status', $pb.PbFieldType.OE, defaultOrMaker: RoomStatus.All, valueOf: RoomStatus.valueOf, enumValues: RoomStatus.values) ..aOS(9, _omitFieldNames ? '' : 'deviceUUID', protoName: 'deviceUUID') + ..aOS(10, _omitFieldNames ? '' : 'announcement') ..hasRequiredFields = false ; @@ -660,6 +665,15 @@ class RoomData extends $pb.GeneratedMessage { $core.bool hasDeviceUUID() => $_has(8); @$pb.TagNumber(9) void clearDeviceUUID() => clearField(9); + + @$pb.TagNumber(10) + $core.String get announcement => $_getSZ(9); + @$pb.TagNumber(10) + set announcement($core.String v) { $_setString(9, v); } + @$pb.TagNumber(10) + $core.bool hasAnnouncement() => $_has(9); + @$pb.TagNumber(10) + void clearAnnouncement() => clearField(10); } class RoomListPageReqData extends $pb.GeneratedMessage { diff --git a/lib/generated/grpc/party_room_server/index.pbjson.dart b/lib/generated/grpc/party_room_server/index.pbjson.dart index 9d3e024..00e7b2d 100644 --- a/lib/generated/grpc/party_room_server/index.pbjson.dart +++ b/lib/generated/grpc/party_room_server/index.pbjson.dart @@ -163,6 +163,7 @@ const RoomData$json = { {'1': 'curPlayer', '3': 7, '4': 1, '5': 5, '10': 'curPlayer'}, {'1': 'status', '3': 8, '4': 1, '5': 14, '6': '.RoomStatus', '10': 'status'}, {'1': 'deviceUUID', '3': 9, '4': 1, '5': 9, '10': 'deviceUUID'}, + {'1': 'announcement', '3': 10, '4': 1, '5': 9, '10': 'announcement'}, ], }; @@ -173,7 +174,7 @@ final $typed_data.Uint8List roomDataDescriptor = $convert.base64Decode( 'KAlSBW93bmVyEhwKCW1heFBsYXllchgFIAEoBVIJbWF4UGxheWVyEh4KCmNyZWF0ZVRpbWUYBi' 'ABKANSCmNyZWF0ZVRpbWUSHAoJY3VyUGxheWVyGAcgASgFUgljdXJQbGF5ZXISIwoGc3RhdHVz' 'GAggASgOMgsuUm9vbVN0YXR1c1IGc3RhdHVzEh4KCmRldmljZVVVSUQYCSABKAlSCmRldmljZV' - 'VVSUQ='); + 'VVSUQSIgoMYW5ub3VuY2VtZW50GAogASgJUgxhbm5vdW5jZW1lbnQ='); @$core.Deprecated('Use roomListPageReqDataDescriptor instead') const RoomListPageReqData$json = { diff --git a/lib/global_ui_model.dart b/lib/global_ui_model.dart index c654c87..5d2065f 100644 --- a/lib/global_ui_model.dart +++ b/lib/global_ui_model.dart @@ -16,6 +16,13 @@ final globalUIModelProvider = ChangeNotifierProvider((ref) => globalUIModel); class AppGlobalUIModel extends BaseUIModel { Timer? activityThemeColorTimer; + Future getRunningGameUser() async { + await Future.delayed(const Duration(milliseconds: 300)); + + ///TODO 实现获取运行中用户名 + return "xkeyC"; + } + Future doCheckUpdate(BuildContext context, {bool init = true}) async { dynamic checkUpdateError; if (!init) { diff --git a/lib/grpc/party_room_server.dart b/lib/grpc/party_room_server.dart index b80ea75..8aca767 100644 --- a/lib/grpc/party_room_server.dart +++ b/lib/grpc/party_room_server.dart @@ -30,4 +30,8 @@ class PartyRoomGrpcServer { static Future getRoomList(RoomListPageReqData req) async { return await _indexService.getRoomList(req); } + + static Future createRoom(RoomData roomData) async { + await _indexService.createRoom(roomData); + } } diff --git a/lib/ui/party_room/dialogs/party_room_create_dialog_ui.dart b/lib/ui/party_room/dialogs/party_room_create_dialog_ui.dart new file mode 100644 index 0000000..3cd56df --- /dev/null +++ b/lib/ui/party_room/dialogs/party_room_create_dialog_ui.dart @@ -0,0 +1,185 @@ +import 'package:flutter/services.dart'; +import 'package:starcitizen_doctor/base/ui.dart'; +import 'package:starcitizen_doctor/generated/grpc/party_room_server/index.pb.dart'; + +import 'party_room_create_dialog_ui_model.dart'; + +class PartyRoomCreateDialogUI extends BaseUI { + @override + Widget? buildBody(BuildContext context, PartyRoomCreateDialogUIModel model) { + return ContentDialog( + title: makeTitle(context, model), + constraints: + BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .6), + content: Padding( + padding: const EdgeInsets.only(left: 12, right: 12, top: 12), + child: AnimatedSize( + duration: const Duration(milliseconds: 130), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (model.userName == null) ...[ + SizedBox( + height: 200, + child: makeLoading(context), + ) + ] else ...[ + Row( + children: [ + const Text("请选择一种玩法"), + const SizedBox(width: 12), + Expanded( + child: SizedBox( + height: 36, + child: ComboBox( + value: model.selectedRoomType, + items: [ + for (final t in model.roomTypes.entries) + ComboBoxItem( + value: t.value, + child: Text(t.value.name), + ) + ], + onChanged: model.onChangeRoomType)), + ) + ], + ), + if (model.selectedRoomType != null && + (model.selectedRoomType?.subTypes.isNotEmpty ?? false)) + ...makeSubTypeSelectWidgets(context, model), + const SizedBox(height: 24), + Row( + children: [ + const Text("游戏用户名(自动获取)"), + const SizedBox(width: 12), + Expanded( + child: TextFormBox( + initialValue: model.userName, + enabled: false, + ), + ), + ], + ), + const SizedBox(height: 24), + Row( + children: [ + const Text("最大玩家数(2 ~ 32)"), + const SizedBox(width: 12), + Expanded( + child: TextFormBox( + controller: model.playerMaxCtrl, + onChanged: (_) => model.notifyListeners(), + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly + ], + keyboardType: TextInputType.number, + ), + ), + ], + ), + const SizedBox(height: 24), + const Text("公告(可选)"), + const SizedBox(height: 12), + TextFormBox( + controller: model.announcementCtrl, + maxLines: 5, + placeholder: "可编写 任务简报,集合地点,船只要求,活动规则等,公告将会自动发送给进入房间的玩家。", + placeholderStyle: + TextStyle(color: Colors.white.withOpacity(.4)), + ), + const SizedBox(height: 32), + for (var v in [ + "创建房间后,其他玩家可以在大厅首页看到您的房间和您选择的玩法,当一个玩家选择加入房间时,你们将可以互相看到对方的用户名。当房间人数达到最大玩家数时,将不再接受新的玩家加入。", + "这是《SC汉化盒子》提供的公益服务,请勿滥用,我们保留拒绝服务的权力。" + ]) ...[ + Text( + v, + style: TextStyle( + fontSize: 14, color: Colors.white.withOpacity(.6)), + ), + const SizedBox(height: 6), + ], + ] + ], + ), + ), + ), + actions: [ + if (model.isWorking) + const ProgressRing() + else + FilledButton( + onPressed: model.onSubmit(), + child: const Padding( + padding: EdgeInsets.all(3), + child: Text("创建房间"), + )) + ], + ); + } + + List makeSubTypeSelectWidgets( + BuildContext context, PartyRoomCreateDialogUIModel model) { + bool isItemSelected(RoomSubtype subtype) { + return model.selectedSubType.contains(subtype); + } + + return [ + const SizedBox(height: 24), + const Text("标签(可选)"), + const SizedBox(height: 12), + Row( + children: [ + for (var item in model.selectedRoomType!.subTypes) + Container( + decoration: BoxDecoration( + color: isItemSelected(item) + ? Colors.green + : FluentTheme.of(context).cardColor, + borderRadius: BorderRadius.circular(1000)), + padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 12), + margin: const EdgeInsets.only(right: 12), + child: IconButton( + icon: Row( + children: [ + Icon(isItemSelected(item) + ? FluentIcons.check_mark + : FluentIcons.add), + const SizedBox(width: 12), + Text( + item.name, + style: TextStyle( + fontSize: 13, + color: isItemSelected(item) + ? null + : Colors.white.withOpacity(.4)), + ), + ], + ), + onPressed: () => model.onTapSubType(item)), + ) + ], + ) + ]; + } + + Widget makeTitle(BuildContext context, PartyRoomCreateDialogUIModel model) { + return Row( + children: [ + IconButton( + icon: const Icon( + FluentIcons.back, + size: 22, + ), + onPressed: model.onBack()), + const SizedBox(width: 12), + Text(getUITitle(context, model)), + ], + ); + } + + @override + String getUITitle(BuildContext context, PartyRoomCreateDialogUIModel model) => + "创建房间"; +} diff --git a/lib/ui/party_room/dialogs/party_room_create_dialog_ui_model.dart b/lib/ui/party_room/dialogs/party_room_create_dialog_ui_model.dart new file mode 100644 index 0000000..f1c71cd --- /dev/null +++ b/lib/ui/party_room/dialogs/party_room_create_dialog_ui_model.dart @@ -0,0 +1,78 @@ +import 'package:starcitizen_doctor/base/ui_model.dart'; +import 'package:starcitizen_doctor/common/conf.dart'; +import 'package:starcitizen_doctor/generated/grpc/party_room_server/index.pb.dart'; +import 'package:starcitizen_doctor/global_ui_model.dart'; +import 'package:starcitizen_doctor/grpc/party_room_server.dart'; + +class PartyRoomCreateDialogUIModel extends BaseUIModel { + Map roomTypes; + + RoomType? selectedRoomType; + + List selectedSubType = []; + + PartyRoomCreateDialogUIModel(this.roomTypes); + + String? userName; + + bool isWorking = false; + + final playerMaxCtrl = TextEditingController(text: "8"); + final announcementCtrl = TextEditingController(); + + @override + initModel() { + super.initModel(); + roomTypes.removeWhere((key, value) => key == ""); + } + + @override + loadData() async { + userName = await globalUIModel.getRunningGameUser(); + notifyListeners(); + } + + onBack() { + if (isWorking) return null; + return () { + Navigator.pop(context!); + }; + } + + void onChangeRoomType(RoomType? value) { + selectedSubType = []; + selectedRoomType = value; + notifyListeners(); + } + + onTapSubType(RoomSubtype item) { + if (!selectedSubType.contains(item)) { + selectedSubType.add(item); + } else { + selectedSubType.remove(item); + } + notifyListeners(); + } + + onSubmit() { + final maxPlayer = int.tryParse(playerMaxCtrl.text) ?? 0; + if (selectedRoomType == null) return null; + if (maxPlayer < 2 || maxPlayer > 32) return null; + return () async { + isWorking = true; + notifyListeners(); + final room = await handleError(() => PartyRoomGrpcServer.createRoom( + RoomData( + roomTypeID: selectedRoomType?.id, + roomSubTypeIds: [for (var value in selectedSubType) value.id], + owner: userName, + deviceUUID: AppConf.deviceUUID, + announcement: announcementCtrl.text.trim()))); + isWorking = false; + notifyListeners(); + if (room != null) { + Navigator.pop(context!, room); + } + }; + } +} diff --git a/lib/ui/party_room/party_room_home_ui.dart b/lib/ui/party_room/party_room_home_ui.dart index df984de..6b7a5b4 100644 --- a/lib/ui/party_room/party_room_home_ui.dart +++ b/lib/ui/party_room/party_room_home_ui.dart @@ -103,7 +103,7 @@ class PartyRoomHomeUI extends BaseUI { ), const SizedBox(width: 12), Button( - onPressed: () {}, + onPressed: () => model.onCreateRoom(), child: const Padding( padding: EdgeInsets.all(3), child: Row( diff --git a/lib/ui/party_room/party_room_home_ui_model.dart b/lib/ui/party_room/party_room_home_ui_model.dart index f2f8d32..cc20e49 100644 --- a/lib/ui/party_room/party_room_home_ui_model.dart +++ b/lib/ui/party_room/party_room_home_ui_model.dart @@ -2,6 +2,9 @@ import 'package:fixnum/fixnum.dart'; import 'package:starcitizen_doctor/base/ui_model.dart'; import 'package:starcitizen_doctor/generated/grpc/party_room_server/index.pb.dart'; import 'package:starcitizen_doctor/grpc/party_room_server.dart'; +import 'package:starcitizen_doctor/ui/party_room/dialogs/party_room_create_dialog_ui_model.dart'; + +import 'dialogs/party_room_create_dialog_ui.dart'; class PartyRoomHomeUIModel extends BaseUIModel { String? pingServerMessage; @@ -33,7 +36,7 @@ class PartyRoomHomeUIModel extends BaseUIModel { RoomSortType selectedSortType = RoomSortType.Default; - int pageNum = 1; + int pageNum = 0; List? rooms; @@ -48,17 +51,27 @@ class PartyRoomHomeUIModel extends BaseUIModel { if (pingServerMessage != "") { pingServerMessage = null; notifyListeners(); + await _pingServer(); } - await _pingServer(); - _loadPage(); + await _loadPage(); + } + + @override + reloadData() async { + pageNum = 0; + rooms = null; + notifyListeners(); + return super.reloadData(); } _loadPage() async { - final r = await PartyRoomGrpcServer.getRoomList(RoomListPageReqData( - pageNum: Int64.tryParseInt("$pageNum"), - typeID: selectedRoomType?.id, - subTypeID: selectedRoomSubType?.id, - status: selectedStatus)); + final r = await handleError(() => PartyRoomGrpcServer.getRoomList( + RoomListPageReqData( + pageNum: Int64.tryParseInt("$pageNum"), + typeID: selectedRoomType?.id, + subTypeID: selectedRoomSubType?.id, + status: selectedStatus))); + if (r == null) return; if (r.pageData.hasNext) { pageNum++; } else { @@ -88,7 +101,7 @@ class PartyRoomHomeUIModel extends BaseUIModel { selectedRoomType = RoomType(id: "", name: "全部", desc: "查看所有类型的房间,寻找一起玩的伙伴。"); selectedRoomSubType = RoomSubtype(id: "", name: "全部"); - roomTypes = {null: selectedRoomType!}; + roomTypes = {"": selectedRoomType!}; for (var element in r.roomTypes) { roomTypes![element.id] = element; } @@ -110,24 +123,42 @@ class PartyRoomHomeUIModel extends BaseUIModel { void onChangeRoomType(RoomType? value) { selectedRoomType = value; selectedRoomSubType = null; + reloadData(); notifyListeners(); } void onChangeRoomStatus(RoomStatus? value) { if (value == null) return; selectedStatus = value; + reloadData(); notifyListeners(); } void onChangeRoomSort(RoomSortType? value) { if (value == null) return; selectedSortType = value; + reloadData(); notifyListeners(); } void onChangeRoomSubType(RoomSubtype? value) { if (value == null) return; selectedRoomSubType = value; + reloadData(); notifyListeners(); } + + onCreateRoom() async { + final room = await showDialog( + context: context!, + dismissWithEsc: false, + builder: (BuildContext context) { + return BaseUIContainer( + uiCreate: () => PartyRoomCreateDialogUI(), + modelCreate: () => + PartyRoomCreateDialogUIModel(Map.from(roomTypes!))); + }, + ); + dPrint(room); + } } diff --git a/pubspec.yaml b/pubspec.yaml index 0efb980..60a94de 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,7 +32,7 @@ dependencies: sdk: flutter flutter_riverpod: ^2.3.6 window_manager: ^0.3.2 - fluent_ui: ^4.8.1 + fluent_ui: ^4.8.5 flutter_staggered_grid_view: ^0.7.0 flutter_acrylic: ^1.1.0 url_launcher: ^6.1.10