feat: 主页账号备份,并修好 B50 预览缩不小

账号备份(AccountBackupCard + AccountBackupService):
- 并发读 GetUserData/Extend/Option/Rating/Charge/Activity/MissionData +
  Character + Map(分页)+ KaleidxScope + 各 itemKind 的 GetUserItemApi,
  汇成一个 JSON 字符串可复制到剪贴板。舞里程与总游玩数据本就在
  userData 的 point/totalPoint/playCount 里。
- itemKind 15(KaleidxScopeKey)不列进回读清单:客户端 ExportUserItems
  从不输出它,下行也没有读它的路径,钥匙实际是
  userKaleidxScopeList[].isKeyFound。
- 全程只发 GetUser*Api,不碰 UpsertUserAllApi。
- 逐段容错:私服没实现某个接口时其余照样备出,失败段单独列出——
  「读到但是空的」和「没读到」在 userItemList 里按 kind 分开存,可区分。
- 取数与拼装分开,buildBackupDocument/encode 部分是纯函数,单测覆盖。

B50 预览:原因是 InteractiveViewer(constrained:false) 给孩子无界约束,
FittedBox(fit: fitWidth) 在无界下根本不缩放,海报按 1762x1850 原尺寸铺开;
而 minScale 又算成 1.0,于是既巨大又缩不动。改为给一个 contain 尺寸的
有限盒子让 FittedBox 真正缩放,minScale 固定 1.0(这个版本没有
initialScale 可用)。RepaintBoundary 留在 FittedBox 内侧,绘制尺寸仍是
1762x1850,导出分辨率不受影响,并加了断言守住这点。
This commit is contained in:
Cookiues committed 2026-10-02 12:26:03 +08:00
1 parent a5fb2c97cd
commit 0cb5320290
8 files changed
+524 -36

No files matched your search

+32 -21
View File
@@ -232,30 +232,41 @@ class _Best50PageState extends State<Best50Page> {
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
// 按视口宽度适配:横屏/桌面下海报占满宽度、竖向拖动看全图,
// 而不是被 contain 缩成中间一小块、两侧大片空白。
// minScale 再允许缩到整张可见,导出仍是原始 1762x1850。
// 海报要按视口整体缩放一档,进来才能看见完整一张(之前的现象是
// 「太大且缩不小」)。两处坑:
// 1. InteractiveViewer(constrained:false) 给子节点无界约束,
// 此时 FittedBox 根本不会缩放,必须先给一个有限大小的盒子。
// 2. InteractiveViewer 没有 initialScale,所以「初始就缩到位」
// 只能靠把子节点做成 contain 尺寸、minScale 固定 1.0。
// RepaintBoundary 放在 FittedBox 内侧:它自身的绘制尺寸仍是
// 1762x1850,导出分辨率不受这里缩放影响。
final fitWidth =
constraints.maxWidth / Best50Poster.canvasWidth;
final scaledHeight = Best50Poster.canvasHeight * fitWidth;
final containScale = constraints.maxHeight / scaledHeight;
final minScale = containScale < 1 ? containScale : 1.0;
final fitHeight =
constraints.maxHeight / Best50Poster.canvasHeight;
final contain = fitWidth < fitHeight ? fitWidth : fitHeight;
return InteractiveViewer(
constrained: false,
minScale: minScale,
maxScale: minScale * 6,
boundaryMargin: const EdgeInsets.all(24),
child: FittedBox(
fit: BoxFit.fitWidth,
child: RepaintBoundary(
key: _posterKey,
child: Best50Poster(
userName: widget.userName,
iconId: widget.iconId,
playerRating: widget.playerRating,
best35: best35,
best15: best15,
return Center(
child: InteractiveViewer(
constrained: false,
minScale: 1.0,
maxScale: 10.0,
boundaryMargin: const EdgeInsets.all(24),
child: SizedBox(
width: Best50Poster.canvasWidth * contain,
height: Best50Poster.canvasHeight * contain,
child: FittedBox(
fit: BoxFit.fill,
child: RepaintBoundary(
key: _posterKey,
child: Best50Poster(
userName: widget.userName,
iconId: widget.iconId,
playerRating: widget.playerRating,
best35: best35,
best15: best15,
),
),
),
),
),