Files
Ichino/lib/config/strings.dart
T
Cookiues 8dda399f76 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
两个构建环境坑的说明。
2026-09-26 00:37:54 +08:00

305 lines
15 KiB
Dart

class AppStrings {
AppStrings._();
// Tab
static const tabHome = '主页';
static const tabTickets = '票据';
static const tabBest50 = 'B50';
static const tabRisk = '风险';
static const tabSettings = '设置';
static const tabAbout = '关于';
// Login
static const appTitle = 'Ichino';
static const loginSubtitle = '扫描或输入二维码进行登录';
static const qrCodeToken = 'QR Code 令牌';
static const qrHint = '粘贴二维码解析内容...';
static const qrParsedResult = '解析结果(截取后 64 位)';
static const uploadQR = '上传二维码';
static const login = '登录';
static const loggingIn = '登录中...';
static const qrScanSuccess = '二维码解析成功';
static const qrScanFailed = '未能识别二维码,请手动粘贴内容';
static const qrScanError = '解析失败';
static const forcePreviewApi = '强制使用 Preview API';
static const loginFailed = '登录失败';
static const requestFailed = '请求失败';
static const settingsTooltip = '设置';
// Home
static const logoutTooltip = '退出登录';
static const titleServerNotConfigured = '未配置 Title Server';
static const titleServerNotConfiguredDesc = '请配置 Title Server 以获取用户数据。';
static const openSettings = '打开设置';
static const loadFailed = '数据加载失败';
static const retry = '重试';
static const unknownUser = '未知用户';
static const online = '在线';
static const offline = '离线';
static const gameInfo = '游戏信息';
static const lastGame = '最后游戏';
static const lastPlay = '最后游玩';
static const lastLogin = '最后登录';
static const lastRegion = '最后地区';
static const romVersion = 'Rom 版本';
static const dataVersion = '数据版本';
static const status = '状态';
static const netMember = '联网会员';
static const inherit = '继承';
static const banState = '封禁状态';
static const clean = '正常';
static const dailyBonus = '每日奖励';
static const notClaimed = '未领取';
static const moreDetails = '更多详情';
static const userId = '用户 ID';
static const totalAwake = '总计 Awake';
static const displayRate = '展示 Rate';
static const iconId = '图标 ID';
static const nameplateId = '名牌 ID';
static const plateId = '姓名框 ID';
static const frameId = '背景框 ID';
static const titleId = '称号 ID';
static const trophyId = '奖杯 ID';
static const partnerId = '伙伴 ID';
static const gradeRank = '段位';
static const courseRank = '阶级';
static const classRank = '段级';
static const point = '当前 P';
static const totalPoint = '累计 P';
static const headphoneVol = '耳机音量';
static const errorId = '错误 ID';
static const rawApiResponse = '原始 API 响应 (调试)';
// Ratings / breakdown
static const ratingBreakdown = 'Rating 详情';
static const musicRating = '乐曲 Rating';
static const newChartRating = '新谱面 Rating';
static const oldChartRating = '旧谱面 Rating';
static const highestRating = '历史最高';
// First play
static const firstPlay = '入坑信息';
static const joinDate = '入坑日期';
static const firstGame = '首次游戏';
static const firstRomVersion = '初始 Rom';
static const firstDataVersion = '初始数据';
// Play stats
static const playStats = '游玩统计';
static const playCount = '总游玩次数';
static const currentPlayCount = '当期游玩';
static const totalAchievement = '总达成率';
static const totalDxScore = '总 DX 分';
static const totalSync = '总同步数';
// Notices
static const inheritedAccountNotice = '此账号当前正在游玩, 仅显示 Preview 概要, 不执行登录。';
static String rating(int r) => 'Rating: $r';
// Tickets
static const runTicket = '使用功能票';
static const runningTicket = '执行中...';
static const ticketNotConfigured = '请先配置 Title Server 设置。';
static const ticketNotLoggedIn = '尚未登录游戏服务器,无法使用功能票。';
static const ticketAutoLogout = '完成后自动退出登录';
static const stepChargeTicket = '使用功能票';
static const stepLogout = '退出游戏';
static const myTickets = '功能票';
static const refreshTickets = '刷新数据';
static const loading = '加载中...';
static const ticketNotSelected = '请先在功能票列表中选中一张票。';
static const ticketStockNotEmpty = '该功能票已有库存,出于安全考虑不会继续发票。';
static const selectedTicket = '已选票';
static const ticketUsed = '功能票使用完成';
static const ticketCooldownSeconds = 60;
static String ticketCooldownCountdown(int remaining) =>
'冷却中,剩余 $remaining 秒';
static String ticketCooldownNotice(int remaining) =>
'登录后需冷却 $ticketCooldownSeconds 秒,剩余 $remaining 秒后可使用功能票。';
// Settings
static const titleServerSettings = '参数设置';
static const titleServerSection = 'Title Server';
static const authServerSettings = 'Auth Server';
static const machineSettings = '机器信息';
static const save = '保存';
static const labelTitleServerUrl = 'Title Server URL';
static const hintTitleServerUrl = '';
static const labelAesKey = 'AES Key';
static const hintAesKey = 'your aes key string';
static const labelAesIv = 'AES IV';
static const hintAesIv = 'your aes iv string';
static const labelClientId = 'Client ID';
static const hintClientId = 'A63E01C2805';
static const labelRegionId = 'Region ID';
static const hintRegionId = '1';
static const labelPlaceId = 'Place ID';
static const hintPlaceId = '1403';
static const labelObfuscateParam = 'Obfuscate Param';
static const hintObfuscateParam = 'LatuAa81';
static const labelApiVersion = 'API Version (Mai-Encoding)';
static const hintApiVersion = '1.53';
static const labelRegionName = 'Region 名称';
static const hintRegionName = '北京';
static const labelPlaceName = 'Place 名称';
static const hintPlaceName = 'Place name';
static const labelKeychipId = 'Keychip ID';
static const hintKeychipId = 'ID';
static const labelAimeUrl = 'Aime URL';
static const hintAimeUrl = '';
static const labelAimeSalt = 'Aime Salt';
static const hintAimeSalt = 'API Key 派生用 Salt';
static String fieldRequired(String label) => '$label 不能为空';
// Config Export/Import
static const configExportImport = '配置导入/导出';
static const exportConfig = '导出当前配置';
static const importConfig = '从剪贴板导入配置';
static const exportSuccess = '配置已导出到剪贴板 (Base64)';
static const importSuccess = '配置已导入并保存';
static const importFailed = '导入失败:剪贴板内容无效';
static const noConfigToExport = '没有可导出的配置';
// UnlockMusic (解锁歌曲 / UpsertUserAll 上传解锁道具)
static const unlockFeatureTitle = '歌曲解锁';
static const unlockFeatureDesc = '通过上传 itemKind 5/6/7 道具解锁歌曲与谱面。';
static const unlockMusicIdLabel = '解锁歌曲 ID';
static const unlockMusicIdHint = '例如 834';
static const unlockMusicOption = '解锁歌曲 (itemKind 5)';
static const unlockMasterOption = '解锁 Master (itemKind 6)';
static const unlockRemasterOption = '解锁 Re:Master (itemKind 7)';
static const unlockNeedMusicId = '请填写要解锁的歌曲 ID。';
static const unlockNeedOption = '请至少勾选一项解锁内容。';
static const unlockNotLoggedIn = '尚未登录游戏服务器,无法执行解锁操作。';
// 待提交列表 (userItemList 可累加多行)
static const listAddButton = '添加到列表';
static const listRemoveTooltip = '从列表移除';
static const listDuplicate = '该内容已在列表中。';
static const unlockPendingTitle = '待解锁列表';
static const collectiblesPendingTitle = '待获取列表';
static const unlockPendingEmpty = '列表为空,可在上方填写歌曲 ID 并勾选难度后添加。';
static const collectiblesPendingEmpty = '列表为空,可在上方选择类型与 ID 后添加。';
static const unlockNeedList = '请至少添加一首要解锁的歌曲。';
static const collectiblesNeedList = '请至少添加一项收藏品。';
static const autoLogoutAndExit = '完成后自动退出登录并返回标题页';
static const exitingToTitle = '正在退出登录并返回标题页...';
static String unlockCooldownNotice(int remaining) =>
'登录后需冷却 $ticketCooldownSeconds 秒,剩余 $remaining 秒后可执行解锁操作。';
static const unlockFetchData = '拉取数据';
static const unlockFetching = '拉取中...';
static const unlockRefetch = '重新拉取';
static const unlockNoData = '⚠️ 未拉取到用户数据,请先点「拉取数据」按钮获取。';
static const unlockFetched = '数据已拉取';
static const unlockMusicRun = '执行解锁';
static const unlockRunning = '执行中...';
static const unlockStepFetch = '拉取用户数据';
static const unlockStepUpload = '上传解锁数据';
static const unlockMusicSuccess = '解锁数据上传完成';
// Collectibles (收藏品获取 / UpsertUserAll 上传收藏品道具)
// itemKind: 1=姓名框 2=称号 3=头像 10=搭档 11=背景板 12=功能票
static const collectiblesFeatureTitle = '收藏品获取';
static const collectiblesFeatureDesc =
'通过上传 itemKind 道具获取收藏品(姓名框/称号/头像/搭档/背景板/功能票)。';
static const collectiblesItemKindLabel = '收藏品类型 (itemKind)';
static const collectiblesItemIdLabel = '收藏品 ID';
static const collectiblesItemIdHint = '例如 250103';
static const collectiblesNeedItemId = '请填写要获取的收藏品 ID。';
static const collectiblesNotLoggedIn = '尚未登录游戏服务器,无法执行获取操作。';
static String collectiblesCooldownNotice(int remaining) =>
'登录后需冷却 $ticketCooldownSeconds 秒,剩余 $remaining 秒后可执行获取操作。';
static const collectiblesRun = '执行获取';
static const collectiblesStepUpload = '上传获取数据';
static const collectiblesSuccess = '收藏品获取数据上传完成';
static const List<int> collectiblesItemKinds = [1, 2, 3, 10, 11, 12];
static String collectiblesItemKindName(int kind) {
switch (kind) {
case 1:
return '姓名框';
case 2:
return '称号';
case 3:
return '头像';
case 10:
return '搭档';
case 11:
return '背景板';
case 12:
return '功能票';
default:
return '$kind';
}
}
// TravelPartner 旅行伙伴 (ItemKind.Character = 9,走 userCharacterList)
static const travelPartnerFeatureTitle = '旅行伙伴';
static const travelPartnerFeatureDesc =
'发放旅行伙伴并编组出战槽位。角色走 userCharacterList(不是搭档 Partner=10,也不走 userItemList)。';
static const travelPartnerNotLoggedIn = '尚未登录游戏服务器,无法操作旅行伙伴。';
static String travelPartnerCooldownNotice(int remaining) =>
'登录后需冷却 $ticketCooldownSeconds 秒,剩余 $remaining 秒后可操作旅行伙伴。';
static const travelPartnerFetchData = '拉取角色数据';
static const travelPartnerFetching = '拉取中...';
static const travelPartnerRefetch = '重新拉取';
static const travelPartnerFetched = '角色数据已拉取';
static const travelPartnerNoData = '⚠️ 尚未拉取,请先获取角色与用户数据。';
static const travelPartnerOwnedTitle = '已拥有旅行伙伴';
static const travelPartnerOwnedEmpty = '还没有任何旅行伙伴。';
static const travelPartnerGrantTitle = '发放新的旅行伙伴';
static const travelPartnerGrantEmpty = '未添加待发放角色。';
static const travelPartnerCharacterIdLabel = '旅行伙伴 ID';
static const travelPartnerCharacterIdHint = '例如 1001';
static const travelPartnerNeedCharacterId = '请填写旅行伙伴 ID。';
static const travelPartnerAlreadyOwned = '该旅行伙伴已拥有,无需重复发放。';
static const travelPartnerSlotTitle = '编组出战槽位';
static const travelPartnerSlotNone = '空';
static String travelPartnerSlotLabel(int index) =>
index == 0 ? '槽 0(队长)' : '槽 $index';
static const travelPartnerCopySlot0 = '把槽 0 复制到其他槽位';
static const travelPartnerCopyNeedSlot0 = '槽 0 还没有选择旅行伙伴。';
static const travelPartnerNotOwnedHint = '编组失败:你并不拥有该旅行伙伴。';
static const travelPartnerRun = '执行旅行伙伴操作';
static const travelPartnerRunning = '执行中...';
static const travelPartnerSuccess = '旅行伙伴数据上传完成';
static const travelPartnerStepFetch = '拉取角色与用户数据';
static const travelPartnerStepUpload = '上传旅行伙伴数据';
// HighRiskFeature hub (中转页)
static const musicRiskHubTitle = '高危功能';
static const musicRiskHubDesc = '以下功能会向服务器发送伪造数据,属于高风险操作,请谨慎使用。';
static const musicRiskHubNotLoggedIn = '尚未登录游戏服务器,无法执行高危操作。';
static String musicRiskHubCooldownNotice(int remaining) =>
'登录后需冷却 $ticketCooldownSeconds 秒,剩余 $remaining 秒后可执行高危操作。';
// Best50 (Best 50 成绩图)
static const best50FeatureTitle = 'Best 50';
static const best35Label = 'BEST 35';
static const best15Label = 'BEST 15';
static const best50Empty = '没有 Rating 数据。';
static const best50LoadingMusic = '正在加载乐曲数据...';
static const best50Export = '导出图片';
static const best50Exporting = '导出中...';
static const best50NoMusicData = '乐曲数据未就绪,定数与曲名可能缺失。';
static String best50RatingSum(int b35, int b15) =>
'B35: $b35 + B15: $b15 = ${b35 + b15}';
static String best50Exported(String? path) =>
path == null ? '图片已导出' : '图片已导出到 $path';
static String best50ExportFailed(String error) => '导出失败: $error';
static const best50Footer = 'Generated by Ichino. UI designed by chuxuehaocai.';
// About
static const aboutTitle = 'Ichino';
static const aboutDesc = '基于 QR Code 的 maimai DX 街机网络登录客户端。';
static const credits = '致谢';
static const creditBuiltWith = '构建框架';
static const creditQRDecode = 'QR 解码';
static const creditIconAssets = '图标资源';
static const creditLicense = '许可证';
}