feat: 旅行伙伴发放与编组、修复退登卡死、优化宽屏布局、加 CI

自动退出登录卡死:_performLogoutAndExit 只 pop() 一层,而解锁/收藏品页是 push
在 HomePage 之上的,退登后被弹回「风险」hub,同时 _loggingOut 仍为 true,
PopScope(canPop:false) + 返回键转圈形成死局。改为 popUntil(isFirst)。

旅行伙伴发不了:它是 ItemKind.Character = 9,与搭档 Partner = 10 是两种东西,
原先收藏品的类型列表 [1,2,3,10,11,12] 里根本没有 9。按客户端实现,角色不走
userItemList(ExportUserItems 不导出 itemKind 9),而是
upsertUserAll.userCharacterList 的 {characterId, level, awakening, useCount},
配 isNewCharacterList 一行一字符。新增发放与 5 槽 charaSlot 编组页,
含「把槽 0 复制到其他槽位」,并校验所选角色确为已拥有或本次待发放。

宽屏布局:各页只有 ConstrainedBox 限宽、没有居中容器,1440 窗口下内容贴左、
右侧大片空白。抽出 responsiveBody 统一居中;B50 预览改用按宽度适配
(InteractiveViewer constrained:false + FittedBox fitWidth),横屏下不再被
contain 缩成中间一小块,导出仍是原始 1762x1850。

CI:.github/workflows/build.yml 先跑 analyze+test 作为门槛,再产出
Windows exe、Android apk、iOS 未签名 ipa(macOS runner,无证书故不可安装)。
README 从 Flutter 模板改写为实际文档,含 path_provider/jni 与 JBR jlink
两个构建环境坑的说明。
This commit is contained in:
Cookiues committed 2026-09-26 00:37:54 +08:00
1 parent ebd1fdddaa
commit 8dda399f76
24 files changed
+1832 -134

No files matched your search

+11
View File
@@ -9,6 +9,7 @@ import 'package:pointycastle/export.dart';
import '../config/title_server_config.dart';
import '../models/user_data.dart';
import '../models/user_character.dart';
import '../models/user_preview.dart';
import '../models/user_rating.dart';
@@ -557,6 +558,16 @@ class TitleApiService {
final list = json['userCharacterList'] as List<dynamic>? ?? [];
return list.map((e) => e as Map<String, dynamic>).toList();
}
/// 旅行伙伴(角色)列表,对应 UserCharacterResponseVO。
Future<List<UserCharacterBean>> getUserCharacters(int userId) async {
final json = await _callApi(
'GetUserCharacterApi',
{'userId': userId},
userId,
);
return UserCharacterBean.listFromResponse(json);
}
}
class _RandomHelper {
+36 -16
View File
@@ -1,4 +1,5 @@
import '../config/title_server_config.dart';
import '../models/user_character.dart';
import 'title_api_service.dart';
/// Dart port of `eaquira/src/sdgb/payload.py::UserAll_payload` and
@@ -356,27 +357,46 @@ class UserAllPayloadBuilder {
upsert['isNewItemList'] = List.filled(items.length, '1').join();
}
/// Port of `music_user_all_patcher`: append unlock items
/// (itemKind 5=music, 6=master, 7=remaster) to the packet.
void applyMusicUnlockPatch(
/// 解锁歌曲/谱面时补一条 musicDetail 记录并标记为新增。
/// 难度本身由 userItemList 的 itemKind 5/6/7 行表达(见客户端
/// VOExtensions.ExportUserItems),不写在 musicDetail 上。
void applyMusicDetailPatch(
Map<String, dynamic> packet, {
required Map<String, dynamic> musicData,
required int musicId,
bool unlockMusic = false,
bool unlockMaster = false,
bool unlockRemaster = false,
}) {
applyItemListPatch(packet, items: [
if (unlockMusic)
{'itemKind': 5, 'itemId': musicId, 'stock': 1, 'isValid': true},
if (unlockMaster)
{'itemKind': 6, 'itemId': musicId, 'stock': 1, 'isValid': true},
if (unlockRemaster)
{'itemKind': 7, 'itemId': musicId, 'stock': 1, 'isValid': true},
]);
final upsert = packet['upsertUserAll'] as Map<String, dynamic>;
upsert['userMusicDetailList'] = [musicData];
upsert['isNewMusicDetailList'] = '1';
}
/// 发放/更新旅行伙伴:写 `userCharacterList` 并按行数生成 `isNewCharacterList`。
/// 客户端 BuildListData 只发增量、每位一个字符,所以这里只放要新增的行。
void applyCharacterListPatch(
Map<String, dynamic> packet, {
required List<Map<String, dynamic>> characters,
}) {
final upsert = packet['upsertUserAll'] as Map<String, dynamic>;
upsert['userCharacterList'] = characters;
upsert['isNewCharacterList'] = List.filled(characters.length, '1').join();
}
/// 编组旅行伙伴:改 `userData[0].charaSlot`(int[5],槽 0 为队长)。
/// 客户端 ExportUserPlaylog 用同一份 CharaSlot 填 playlog 的
/// characterId1..5,这里一起同步,避免 playlog 与 userData 互相矛盾。
void applyCharaSlotPatch(
Map<String, dynamic> packet, {
required List<int> charaSlot,
}) {
final slots = normalizeCharaSlot(charaSlot);
final upsert = packet['upsertUserAll'] as Map<String, dynamic>;
final userDataList = upsert['userData'] as List;
(userDataList.first as Map<String, dynamic>)['charaSlot'] = slots;
for (final raw in (packet['userPlaylogList'] as List)) {
final playlog = raw as Map<String, dynamic>;
for (var i = 0; i < 5; i++) {
playlog['characterId${i + 1}'] = slots[i];
}
}
}
}