完善景区排队设置页与全局按钮配置迁移。
重构排队设置与变更日志交互,补充 Overlay 与资源,并将多页面主操作按钮统一到 UIButton Configuration,同步更新相关单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -47,6 +47,7 @@ final class ScenicQueueSettingsViewModel {
|
||||
private(set) var presetVoices: [String]
|
||||
private(set) var settingsTab: ScenicQueueSettingsTab = .basic
|
||||
private(set) var highlightedInvalidFieldKeys: Set<String> = []
|
||||
private(set) var blockingInvalidNumericFieldKeys: Set<String> = []
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
@ -56,7 +57,7 @@ final class ScenicQueueSettingsViewModel {
|
||||
private let appStore: AppStore
|
||||
private let ttsManager: ScenicQueueTTSManaging
|
||||
private var punchSpotSettingRequestGeneration = 0
|
||||
private var ttsLoopAnchorText: String?
|
||||
private(set) var ttsLoopAnchorText: String?
|
||||
|
||||
init(appStore: AppStore = .shared, ttsManager: ScenicQueueTTSManaging = AliyunNLSTTSManager.shared) {
|
||||
self.appStore = appStore
|
||||
@ -108,6 +109,30 @@ final class ScenicQueueSettingsViewModel {
|
||||
appStore.scenicQueue.useOfflineTTS
|
||||
}
|
||||
|
||||
/// 自定义文案或预设文案是否正在循环播报。
|
||||
var isCustomTTSLooping: Bool {
|
||||
ttsManager.customTextLooping
|
||||
}
|
||||
|
||||
/// 将米格式化为 Android 设置页使用的千米文本,最多保留两位小数。
|
||||
nonisolated static func kilometersDisplay(fromMeters meters: Int) -> String {
|
||||
let value = Double(max(0, meters)) / 1_000
|
||||
if value.rounded() == value { return String(format: "%.0f", value) }
|
||||
let text = String(format: "%.2f", value)
|
||||
return text.replacingOccurrences(of: #"0+$"#, with: "", options: .regularExpression)
|
||||
.replacingOccurrences(of: #"\.$"#, with: "", options: .regularExpression)
|
||||
}
|
||||
|
||||
/// 将最多两位小数的千米文本转换为米;非法格式或越界时返回 nil。
|
||||
nonisolated static func meters(fromKilometersText raw: String) -> Int? {
|
||||
let text = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard text.range(of: #"^\d{1,4}(\.\d{0,2})?$"#, options: .regularExpression) != nil,
|
||||
let value = Decimal(string: text, locale: Locale(identifier: "en_US_POSIX")) else { return nil }
|
||||
let meters = NSDecimalNumber(decimal: value * 1_000).intValue
|
||||
guard Limits.queueDistance.contains(meters) else { return nil }
|
||||
return meters
|
||||
}
|
||||
|
||||
func openSettingChangeLog() {
|
||||
onNavigateLog?()
|
||||
}
|
||||
@ -146,6 +171,18 @@ final class ScenicQueueSettingsViewModel {
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
/// 记录数字输入框是否存在尚未修正的越界原始值,并同步红框状态。
|
||||
func reportOutOfRangeNumericPending(fieldKey: String, pending: Bool) {
|
||||
if pending {
|
||||
blockingInvalidNumericFieldKeys.insert(fieldKey)
|
||||
highlightedInvalidFieldKeys.insert(fieldKey)
|
||||
} else {
|
||||
blockingInvalidNumericFieldKeys.remove(fieldKey)
|
||||
highlightedInvalidFieldKeys.remove(fieldKey)
|
||||
}
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func setShootMinute(_ value: Int) { shootMinute = value.clamped(to: Limits.shootMinute); notifyStateChange() }
|
||||
func setShootSecond(_ value: Int) { shootSecond = value.clamped(to: Limits.shootSecond); notifyStateChange() }
|
||||
func setFirstAheadCount(_ value: Int) { firstAheadCount = value.clamped(to: Limits.ahead); notifyStateChange() }
|
||||
@ -222,6 +259,21 @@ final class ScenicQueueSettingsViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// 循环播放指定预设文案;若已有文案播放则先停止再切换。
|
||||
func startPresetVoiceLoop(_ phrase: String) {
|
||||
let text = String(phrase.trimmingCharacters(in: .whitespacesAndNewlines).prefix(Limits.remarkMaxLen))
|
||||
guard !text.isEmpty else { return }
|
||||
Task {
|
||||
if ttsManager.customTextLooping {
|
||||
ttsManager.stopCustomTextLoop()
|
||||
}
|
||||
let ok = await ttsManager.startCustomTextLoop(text)
|
||||
ttsLoopAnchorText = ok ? text : nil
|
||||
if !ok { onShowMessage?("播报失败,请检查网络与语音服务") }
|
||||
notifyStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
/// 播放蓝牙测试音。
|
||||
func playBluetoothTestSound() {
|
||||
Task {
|
||||
@ -289,7 +341,7 @@ final class ScenicQueueSettingsViewModel {
|
||||
|
||||
/// 校验并保存设置。
|
||||
func save(api: ScenicQueueAPIProtocol) async {
|
||||
highlightedInvalidFieldKeys = []
|
||||
highlightedInvalidFieldKeys = blockingInvalidNumericFieldKeys
|
||||
guard let ids = currentScenicAndSpotIdsForSelected() else {
|
||||
highlightedInvalidFieldKeys = ["punchSpot"]
|
||||
settingsTab = .basic
|
||||
@ -297,6 +349,12 @@ final class ScenicQueueSettingsViewModel {
|
||||
notifyStateChange()
|
||||
return
|
||||
}
|
||||
guard blockingInvalidNumericFieldKeys.isEmpty else {
|
||||
settingsTab = tabForValidationKeys(blockingInvalidNumericFieldKeys)
|
||||
onShowMessage?("请修正标红的配置项")
|
||||
notifyStateChange()
|
||||
return
|
||||
}
|
||||
let validation = buildSaveValidation()
|
||||
guard validation.messages.isEmpty else {
|
||||
highlightedInvalidFieldKeys = validation.keys
|
||||
|
||||
Reference in New Issue
Block a user