feat: 万花筒专区可设置门的通关状态
通关态与前两个动作一样落在 userKaleidxScopeList 同一行,是 isClear。 用三态下拉(不改动 / 已通关 / 未通关)而不是勾选框,因为通关可以往回改。 - 标记通关时补 clearDate,格式按客户端 TimeManager.GetDateString 的 yyyy-MM-dd HH:mm:ss.f;已有首次通关日期不覆盖,撤销通关时一起清空 - 上行是整行替换,只叠加要改的位,playCount / best 成绩 / 日期保持原值 - 通关态只在 found && key 的前提下才显示得出来,缺钥匙时给一条短提示 - 校验放宽:三个动作任选其一即可执行
This commit is contained in:
1 parent
44c4a1daae
commit
6438fb2171
7 files changed
+241
-27
No files matched your search
@@ -361,7 +361,14 @@ class AppStrings {
|
||||
static const kaleidxGateIdLabel = '门 ID (gateId)';
|
||||
static const kaleidxGateIdHint = '例如 3';
|
||||
static const kaleidxNeedGateId = '请填写门 ID。';
|
||||
static const kaleidxNeedAction = '请至少勾选一项:发现门或获取钥匙。';
|
||||
static const kaleidxNeedAction = '请至少选一项:发现门、获取钥匙或通关状态。';
|
||||
static const kaleidxClearLabel = '通关状态';
|
||||
static const kaleidxClearKeep = '不改动';
|
||||
static const kaleidxClearCleared = '标记为已通关';
|
||||
static const kaleidxClearUncleared = '标记为未通关';
|
||||
static const kaleidxClearNeedsKey =
|
||||
'通关态只在「已发现 + 有钥匙」的门上才显示得出来;缺钥匙时它仍算未解锁。'
|
||||
'要一并补钥匙就勾上「获取钥匙」。标记未通关会连首次通关日期一起清空。';
|
||||
static const kaleidxPendingEmpty = '列表为空,可在上方填写门 ID 后添加。';
|
||||
static const kaleidxActionDiscover = '发现门 (isGateFound)';
|
||||
static const kaleidxActionKey = '获取钥匙 (isKeyFound)';
|
||||
|
||||
@@ -72,6 +72,7 @@ class UserKaleidxScopeBean {
|
||||
bool? isGateFound,
|
||||
bool? isKeyFound,
|
||||
bool? isClear,
|
||||
String? clearDate,
|
||||
}) => UserKaleidxScopeBean(
|
||||
gateId: gateId,
|
||||
isGateFound: isGateFound ?? this.isGateFound,
|
||||
@@ -85,29 +86,48 @@ class UserKaleidxScopeBean {
|
||||
bestAchievementDate: bestAchievementDate,
|
||||
bestDeluxscoreDate: bestDeluxscoreDate,
|
||||
playCount: playCount,
|
||||
clearDate: clearDate,
|
||||
clearDate: clearDate ?? this.clearDate,
|
||||
lastPlayDate: lastPlayDate,
|
||||
isInfoWatched: isInfoWatched,
|
||||
);
|
||||
|
||||
/// 服务器上还没有这扇门时的新行。`isGateFound` 恒为 true——不存在「只给钥匙
|
||||
/// 不给门」这种可用状态(见类注释的状态机),新行其余字段取 struct 默认值。
|
||||
static UserKaleidxScopeBean discovered(int gateId, {required bool giveKey}) =>
|
||||
UserKaleidxScopeBean(
|
||||
gateId: gateId,
|
||||
isGateFound: true,
|
||||
isKeyFound: giveKey,
|
||||
);
|
||||
static UserKaleidxScopeBean discovered(
|
||||
int gateId, {
|
||||
required bool giveKey,
|
||||
bool? clear,
|
||||
String timestamp = '',
|
||||
}) => UserKaleidxScopeBean(
|
||||
gateId: gateId,
|
||||
isGateFound: true,
|
||||
isKeyFound: giveKey,
|
||||
isClear: clear ?? false,
|
||||
clearDate: (clear ?? false) ? timestamp : '',
|
||||
);
|
||||
|
||||
/// 在服务器原行上叠加本次动作。**必须**走这里而不是新建一行:整行替换会把
|
||||
/// best 成绩、playCount、clearDate 这些字段清零。
|
||||
///
|
||||
/// [clear] 传 null 表示不改动通关状态。给钥匙必须连带发现门,否则这行在
|
||||
/// 客户端是隐形的。
|
||||
UserKaleidxScopeBean withActions({
|
||||
required bool discover,
|
||||
required bool giveKey,
|
||||
bool? clear,
|
||||
String timestamp = '',
|
||||
}) => copyWith(
|
||||
// 给钥匙连带发现门,否则这行在客户端是隐形的。
|
||||
isGateFound: isGateFound || discover || giveKey,
|
||||
isKeyFound: isKeyFound || giveKey,
|
||||
isClear: clear,
|
||||
// 首次通关日期只在刚转成通关时落一个时间戳(客户端就是这么写的:
|
||||
// `clearDate = TimeManager.GetNowDateString()`);撤销通关时一起清空,
|
||||
// 留着一个日期比缺日期更容易让人误读。
|
||||
clearDate: clear == null
|
||||
? null
|
||||
: (clear
|
||||
? (clearDate.isNotEmpty ? clearDate : timestamp)
|
||||
: ''),
|
||||
);
|
||||
|
||||
static int _toInt(dynamic v) {
|
||||
|
||||
@@ -12,10 +12,10 @@ import '../widgets/app_notice.dart';
|
||||
import '../widgets/cooldown_mixin.dart';
|
||||
import '../widgets/step_progress.dart';
|
||||
|
||||
/// 万花筒专区:发现新的宿命之门 + 获取门的钥匙。
|
||||
/// 万花筒专区:发现新的宿命之门、获取门的钥匙、设置门的通关状态。
|
||||
///
|
||||
/// 两个动作落在 `upsertUserAll.userKaleidxScopeList` 同一行的两个布尔位上
|
||||
/// (`isGateFound` / `isKeyFound`)。钥匙不走 `userItemList`:
|
||||
/// 三个动作都落在 `upsertUserAll.userKaleidxScopeList` 同一行的三个布尔位上
|
||||
/// (`isGateFound` / `isKeyFound` / `isClear`)。钥匙不走 `userItemList`:
|
||||
/// `ExportUserItems` 从不输出 itemKind 15,下行也没有读它的路径。
|
||||
class KaleidxScopePage extends StatefulWidget {
|
||||
final int userId;
|
||||
@@ -37,6 +37,9 @@ class KaleidxScopePage extends StatefulWidget {
|
||||
State<KaleidxScopePage> createState() => _KaleidxScopePageState();
|
||||
}
|
||||
|
||||
/// 通关态可以往两个方向改,所以用三态而不是又一个勾选框。
|
||||
enum _ClearChoice { keep, cleared, uncleared }
|
||||
|
||||
class _KaleidxScopePageState extends State<KaleidxScopePage>
|
||||
with CooldownMixin<KaleidxScopePage> {
|
||||
final _gateIdController = TextEditingController();
|
||||
@@ -44,6 +47,7 @@ class _KaleidxScopePageState extends State<KaleidxScopePage>
|
||||
|
||||
bool _discover = true;
|
||||
bool _giveKey = false;
|
||||
_ClearChoice _clearChoice = _ClearChoice.keep;
|
||||
|
||||
Map<String, Map<String, dynamic>> _userAllData = const {};
|
||||
List<UserKaleidxScopeBean> _serverScopes = const [];
|
||||
@@ -88,12 +92,30 @@ class _KaleidxScopePageState extends State<KaleidxScopePage>
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 整行替换,所以已存在的门必须拿服务器原行做底,只改那两个布尔位。
|
||||
bool? get _targetClear => switch (_clearChoice) {
|
||||
_ClearChoice.keep => null,
|
||||
_ClearChoice.cleared => true,
|
||||
_ClearChoice.uncleared => false,
|
||||
};
|
||||
|
||||
/// 整行替换,所以已存在的门必须拿服务器原行做底,只改要改的位。
|
||||
List<UserKaleidxScopeBean> _buildRows() {
|
||||
final clear = _targetClear;
|
||||
final timestamp = UserAllPayloadBuilder.nowTimestamp();
|
||||
return [
|
||||
for (final id in _pending)
|
||||
_serverRow(id)?.withActions(discover: _discover, giveKey: _giveKey) ??
|
||||
UserKaleidxScopeBean.discovered(id, giveKey: _giveKey),
|
||||
_serverRow(id)?.withActions(
|
||||
discover: _discover,
|
||||
giveKey: _giveKey,
|
||||
clear: clear,
|
||||
timestamp: timestamp,
|
||||
) ??
|
||||
UserKaleidxScopeBean.discovered(
|
||||
id,
|
||||
giveKey: _giveKey,
|
||||
clear: clear,
|
||||
timestamp: timestamp,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -139,7 +161,7 @@ class _KaleidxScopePageState extends State<KaleidxScopePage>
|
||||
_snack(AppStrings.kaleidxNeedGateId);
|
||||
return;
|
||||
}
|
||||
if (!_discover && !_giveKey) {
|
||||
if (!_discover && !_giveKey && _clearChoice == _ClearChoice.keep) {
|
||||
_snack(AppStrings.kaleidxNeedAction);
|
||||
return;
|
||||
}
|
||||
@@ -366,10 +388,42 @@ class _KaleidxScopePageState extends State<KaleidxScopePage>
|
||||
enabled: enabled,
|
||||
onChanged: (v) => setState(() => _giveKey = v),
|
||||
),
|
||||
if (_giveKey) ...[
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: 10),
|
||||
DropdownButtonFormField<_ClearChoice>(
|
||||
initialValue: _clearChoice,
|
||||
decoration: InputDecoration(
|
||||
labelText: AppStrings.kaleidxClearLabel,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(14),
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: _ClearChoice.keep,
|
||||
child: Text(AppStrings.kaleidxClearKeep),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: _ClearChoice.cleared,
|
||||
child: Text(AppStrings.kaleidxClearCleared),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: _ClearChoice.uncleared,
|
||||
child: Text(AppStrings.kaleidxClearUncleared),
|
||||
),
|
||||
],
|
||||
onChanged: enabled
|
||||
? (v) => setState(
|
||||
() => _clearChoice = v ?? _ClearChoice.keep,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
if (_giveKey || _clearChoice == _ClearChoice.cleared) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
AppStrings.kaleidxKeyNeedsGate,
|
||||
_giveKey
|
||||
? AppStrings.kaleidxKeyNeedsGate
|
||||
: AppStrings.kaleidxClearNeedsKey,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
height: 1.5,
|
||||
|
||||
@@ -55,6 +55,11 @@ class UserAllPayloadBuilder {
|
||||
'${dt.year}-${_two(dt.month)}-${_two(dt.day)} '
|
||||
'${_two(dt.hour)}:${_two(dt.minute)}:${_two(dt.second)}.0';
|
||||
|
||||
/// 客户端 `TimeManager.GetDateString` 的格式是
|
||||
/// `ToString("yyyy-MM-dd HH:mm:ss.f")`,也就是带一位小数的秒。
|
||||
/// 万花筒的 `clearDate` 之类字段要按这个形状给。
|
||||
static String nowTimestamp() => _formatPlayDateTime(_shanghaiNow());
|
||||
|
||||
static int _toInt(dynamic v) {
|
||||
if (v is int) return v;
|
||||
if (v is num) return v.toInt();
|
||||
|
||||
Reference in new issue
Block a user