自动退出登录卡死:_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
两个构建环境坑的说明。
114 lines
3.5 KiB
Dart
114 lines
3.5 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
|
|
import '../config/responsive.dart';
|
|
import '../config/strings.dart';
|
|
import '../config/title_server_config.dart';
|
|
|
|
class ConfigExportImportPage extends StatelessWidget {
|
|
const ConfigExportImportPage({super.key});
|
|
|
|
Future<void> _exportConfig(BuildContext context) async {
|
|
final config = TitleServerConfigHolder().config;
|
|
if (config == null) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text(AppStrings.noConfigToExport)),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
final json = jsonEncode(config.toJson());
|
|
final base64 = base64Encode(utf8.encode(json));
|
|
await Clipboard.setData(ClipboardData(text: base64));
|
|
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text(AppStrings.exportSuccess)),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _importConfig(BuildContext context) async {
|
|
final data = await Clipboard.getData(Clipboard.kTextPlain);
|
|
final text = data?.text;
|
|
if (text == null || text.isEmpty) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text(AppStrings.importFailed)),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
try {
|
|
final json = utf8.decode(base64Decode(text));
|
|
final map = jsonDecode(json) as Map<String, dynamic>;
|
|
final config = TitleServerConfig.fromJson(map);
|
|
await TitleServerConfigHolder().update(config);
|
|
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text(AppStrings.importSuccess)),
|
|
);
|
|
Navigator.of(context).pop();
|
|
}
|
|
} catch (_) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text(AppStrings.importFailed)),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text(AppStrings.configExportImport),
|
|
),
|
|
body: Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24),
|
|
child: responsiveBody(
|
|
context,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
FilledButton.icon(
|
|
onPressed: () => _exportConfig(context),
|
|
icon: const Icon(Icons.upload, size: 20),
|
|
label: const Text(AppStrings.exportConfig),
|
|
style: FilledButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
OutlinedButton.icon(
|
|
onPressed: () => _importConfig(context),
|
|
icon: const Icon(Icons.download, size: 20),
|
|
label: const Text(AppStrings.importConfig),
|
|
style: OutlinedButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|