自动退出登录卡死:_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
两个构建环境坑的说明。
79 lines
2.5 KiB
Dart
79 lines
2.5 KiB
Dart
import 'package:flutter/material.dart';
|
||
|
||
/// Breakpoints: phone < 600, tablet 600–900, desktop > 900
|
||
const double _breakpointTablet = 600;
|
||
const double _breakpointDesktop = 900;
|
||
|
||
/// Returns a responsive max width for content containers.
|
||
/// - Phone: 500
|
||
/// - Tablet: 700
|
||
/// - Desktop: 900
|
||
double responsiveMaxWidth(BuildContext context) {
|
||
final width = MediaQuery.of(context).size.width;
|
||
if (width >= _breakpointDesktop) return 900;
|
||
if (width >= _breakpointTablet) return 700;
|
||
return 500;
|
||
}
|
||
|
||
/// Whether the screen is tablet-sized or wider.
|
||
bool isTablet(BuildContext context) =>
|
||
MediaQuery.of(context).size.width >= _breakpointTablet;
|
||
|
||
/// Whether the screen is desktop-sized or wider.
|
||
bool isDesktop(BuildContext context) =>
|
||
MediaQuery.of(context).size.width >= _breakpointDesktop;
|
||
|
||
/// 把页面内容限制在响应式宽度内,并在宽窗口(桌面/横屏)下水平居中。
|
||
/// 之前各页只有 ConstrainedBox 限宽,没有居中容器,1440 宽的窗口里内容
|
||
/// 会贴左、右侧留出大片空白。
|
||
Widget responsiveBody(BuildContext context, {required Widget child}) {
|
||
return Center(
|
||
child: ConstrainedBox(
|
||
constraints: BoxConstraints(maxWidth: responsiveMaxWidth(context)),
|
||
child: child,
|
||
),
|
||
);
|
||
}
|
||
|
||
/// Build columns for tablet/desktop — on phone, each item is full-width;
|
||
/// on tablet+ they are laid out in [columns] columns.
|
||
List<Widget> responsiveGrid({
|
||
required BuildContext context,
|
||
required List<Widget> children,
|
||
int columns = 2,
|
||
double spacing = 12,
|
||
}) {
|
||
final tablet = isTablet(context);
|
||
if (!tablet) return children;
|
||
|
||
final rows = <Widget>[];
|
||
final padded = <Widget>[];
|
||
for (int i = 0; i < children.length; i++) {
|
||
padded.add(children[i]);
|
||
if ((i + 1) % columns == 0 || i == children.length - 1) {
|
||
final cols = <Widget>[];
|
||
for (int j = 0; j < padded.length; j++) {
|
||
cols.add(Expanded(child: padded[j]));
|
||
}
|
||
// pad incomplete last row with empty expanded widgets
|
||
while (cols.length < columns) {
|
||
cols.add(const Expanded(child: SizedBox.shrink()));
|
||
}
|
||
rows.add(Padding(
|
||
padding: EdgeInsets.only(bottom: rows.isEmpty ? 0 : spacing),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
for (int j = 0; j < cols.length; j++) ...[
|
||
if (j > 0) SizedBox(width: spacing),
|
||
cols[j],
|
||
],
|
||
],
|
||
),
|
||
));
|
||
padded.clear();
|
||
}
|
||
}
|
||
return rows;
|
||
}
|