Files
maidong-ktv/app/src/main/java/com/local/ktv/KtvStore.kt
T
zijunchiang ce57068c68 V1.3.5: 调音弹窗重构(音量+均衡器) + 公放切歌修复 + 远程均衡器可折叠
1) 公放歌曲按「切歌」不再停止
   原先 advancePlayback 在已点队列为空时把「用户主动切歌」一律当作停止(进入空闲态),
   导致公放(随机)歌曲播放中按切歌就停了。现在: 若当前曲目属于公放清单
   (RandomPlayQueue.isCurrentTrack), 按切歌直接接着播清单里的下一首;
   不调用 markFinished(跳过不等于播完, 不该 +1), 但它已在 markStarted 计入本轮已播,
   所以本轮不会重复。playRandomLocalSong 新增 skipped 参数用于提示文案。

2) 「调音」弹窗重构为 App 内唯一音控入口(播放界面 + 主界面右上角同一弹窗)
   自上而下: 音量(横向控制杆, 即时生效) → 均衡器开关(默认关闭) → 均衡器预设
   (自定义/流行/摇滚/舞曲, 手改频段自动转「自定义」并保存) → EQ 频段(竖向控制杆)。
   全部即时生效, 因此没有「完成/确认」按钮, 只有关闭。
   新增自绘控件 VerticalEqBar(SeekBar 只支持横向): 触摸拖动 + 遥控器上下键步进,
   量程端点放行按键以便焦点离开, 焦点态自绘橙色边框并 suppressFill。
   均衡器预设改为 标准(仅供远程"恢复标准", 不在 App 行内)/流行/摇滚/舞曲,
   eqPreset 默认 "自定义"。
   清除旧 UI 与代码: 删除 dialog_tuning.xml / dialog_audio_control.xml /
   CircularVolumeView.kt, 移除 showAudioControlDialog / showAlignedTuningDialog /
   showEqualizerDialog / showFullScreenVolumeDialog / bindAtmospherePreset /
   updateToggleState 及 fullScreenVolume* / fullScreenToneControl /
   hideFullScreenVolumeIndicatorRunnable 等字段与 dispatchKeyEvent 中的面板分支。
   设置页「音频控制」改为指向调音弹窗(麦克风音量仍提示由功放硬件调节;
   升降调保留在下一页「音调控制」)。

3) 远程点歌页面均衡器界面不变, 均衡器文字栏改为折叠按钮(默认收起)。

版本号 1.3.4 -> 1.3.5(versionCode 10 -> 11)。按约定本次不发布 Release, 仅本地打包测试。
2026-09-20 15:38:48 +08:00

249 lines
12 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.local.ktv
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.util.LinkedHashMap
import java.util.LinkedHashSet
class KtvStore {
private val file = AppPaths.stateFile
fun stateFile(): File = file
@JvmField var loopOne = false
@JvmField var autoNext = true
@JvmField var originalVocal = true
@JvmField var vocalChannelMode = "自动"
@JvmField var musicVolume = DEFAULT_MUSIC_VOLUME
@JvmField var micVolume = 70
@JvmField var tone = 0
@JvmField var atmosphere = "标准"
@JvmField var singMode = "普通演唱"
@JvmField var recordEnabled = true
@JvmField var scoreEnabled = true
@JvmField var lastScore = 0
@JvmField var lastScoreSong = ""
@JvmField var marqueeEnabled = true
@JvmField var marqueeText = "欢迎使用麦动"
@JvmField var playWhileDownloading = false
@JvmField var clearDownloadsOnBoot = false
@JvmField var autoFullscreenSeconds = 30
@JvmField var showUsbSongs = true
@JvmField var autoDeleteSongs = true
@JvmField var reserveStorageGb = 1.0
@JvmField var floatingButtonEnabled = true
@JvmField var songTitleSubtitleEnabled = true
@JvmField var orderedSongOriginal = true
@JvmField var orderedVolumeMode = 0
@JvmField var orderedVocalMode = 0
@JvmField var voiceEngineEnabled = true
@JvmField var voiceEngineVolume = 70
@JvmField var lightMode = "自动"
@JvmField var audioDelayMs = 0
@JvmField var screenBrightness = 80
@JvmField var screenMode = "适应屏幕"
@JvmField var doubleScreenEnabled = false
@JvmField var tableBroadcastEnabled = true
@JvmField var tableBroadcastSeconds = 8
@JvmField var tableBroadcastText = "欢迎光临"
// ===== 均衡器(KtvEqualizer)=====
@JvmField var eqEnabled = false
@JvmField var eqPreset = "自定义"
/** 各频段增益(dB),逗号分隔;不同设备频段数可能不同,加载后会按实际频段数对齐。 */
@JvmField var eqLevels = ""
@JvmField var eqGlobalMix = false
@JvmField val orderQueue = mutableListOf<Song>()
@JvmField val sangHistory = mutableListOf<Song>()
@JvmField val scoreHistory = mutableListOf<String>()
@JvmField val broadcastHistory = mutableListOf<String>()
@JvmField val favoriteIds = LinkedHashSet<String>()
@JvmField val hiddenSongIds = LinkedHashSet<String>()
@JvmField val playlists = mutableListOf<String>()
@JvmField val playlistSongs = LinkedHashMap<String, MutableSet<String>>()
fun load() {
runCatching {
if (!file.exists()) return@runCatching
val root = JSONObject(file.readText())
loopOne = root.optBoolean("loopOne", false)
autoNext = root.optBoolean("autoNext", true)
originalVocal = root.optBoolean("originalVocal", true)
vocalChannelMode = root.optString("vocalChannelMode", "自动")
musicVolume = root.optInt("musicVolume", DEFAULT_MUSIC_VOLUME)
micVolume = root.optInt("micVolume", 70)
tone = root.optInt("tone", 0)
atmosphere = root.optString("atmosphere", "标准")
singMode = root.optString("singMode", "普通演唱")
recordEnabled = root.optBoolean("recordEnabled", true)
scoreEnabled = root.optBoolean("scoreEnabled", true)
lastScore = root.optInt("lastScore", 0)
lastScoreSong = root.optString("lastScoreSong", "")
marqueeEnabled = root.optBoolean("marqueeEnabled", true)
marqueeText = root.optString("marqueeText", "欢迎使用麦动")
playWhileDownloading = root.optBoolean("playWhileDownloading", false)
clearDownloadsOnBoot = root.optBoolean("clearDownloadsOnBoot", false)
autoFullscreenSeconds = root.optInt("autoFullscreenSeconds", 30)
showUsbSongs = root.optBoolean("showUsbSongs", true)
autoDeleteSongs = root.optBoolean("autoDeleteSongs", true)
reserveStorageGb = root.optDouble("reserveStorageGb", 1.0).coerceIn(0.5, 64.0)
floatingButtonEnabled = root.optBoolean("floatingButtonEnabled", true)
songTitleSubtitleEnabled = root.optBoolean("songTitleSubtitleEnabled", true)
orderedSongOriginal = root.optBoolean("orderedSongOriginal", true)
orderedVolumeMode = root.optInt("orderedVolumeMode", 0)
orderedVocalMode = root.optInt("orderedVocalMode", 0)
voiceEngineEnabled = root.optBoolean("voiceEngineEnabled", true)
voiceEngineVolume = root.optInt("voiceEngineVolume", 70)
lightMode = root.optString("lightMode", "自动")
audioDelayMs = root.optInt("audioDelayMs", 0)
screenBrightness = root.optInt("screenBrightness", 80)
screenMode = root.optString("screenMode", "适应屏幕")
doubleScreenEnabled = root.optBoolean("doubleScreenEnabled", false)
tableBroadcastEnabled = root.optBoolean("tableBroadcastEnabled", true)
tableBroadcastSeconds = root.optInt("tableBroadcastSeconds", 8)
tableBroadcastText = root.optString("tableBroadcastText", "欢迎光临")
eqEnabled = root.optBoolean("eqEnabled", false)
eqPreset = root.optString("eqPreset", "自定义")
eqLevels = root.optString("eqLevels", "")
eqGlobalMix = root.optBoolean("eqGlobalMix", false)
readSongs(root.optJSONArray("orderQueue"), orderQueue)
readSongs(root.optJSONArray("sangHistory"), sangHistory)
root.optJSONArray("scoreHistory").copyStringsTo(scoreHistory)
root.optJSONArray("broadcastHistory").copyStringsTo(broadcastHistory)
root.optJSONArray("favoriteIds").copyStringsTo(favoriteIds)
root.optJSONArray("hiddenSongIds").copyStringsTo(hiddenSongIds)
root.optJSONArray("playlists").copyStringsTo(playlists)
root.optJSONObject("playlistSongs")?.let { map ->
map.names()?.let { names ->
repeat(names.length()) {
val name = names.optString(it)
val values = LinkedHashSet<String>()
map.optJSONArray(name).copyStringsTo(values)
playlistSongs[name] = values
}
}
}
}
// 默认值迁移(v1.2.5+):原唱默认 伴唱→原唱、自动全屏 0→30 秒、点播歌曲原伴唱 伴唱→原唱。
// 仅当旧数据仍是旧默认值时升级为新默认;已在设置里手动改过的值不受影响。
if (!originalVocal) originalVocal = true
if (autoFullscreenSeconds == 0) autoFullscreenSeconds = 30
if (!orderedSongOriginal) orderedSongOriginal = true
ensureDefaultPlaylists()
}
fun save() {
writeSnapshot(snapshotJson())
}
fun snapshotJson(): String = runCatching {
val root = JSONObject().apply {
put("loopOne", loopOne)
put("autoNext", autoNext)
put("originalVocal", originalVocal)
put("vocalChannelMode", vocalChannelMode)
put("musicVolume", musicVolume)
put("micVolume", micVolume)
put("tone", tone)
put("atmosphere", atmosphere)
put("singMode", singMode)
put("recordEnabled", recordEnabled)
put("scoreEnabled", scoreEnabled)
put("lastScore", lastScore)
put("lastScoreSong", lastScoreSong)
put("marqueeEnabled", marqueeEnabled)
put("marqueeText", marqueeText)
put("playWhileDownloading", playWhileDownloading)
put("clearDownloadsOnBoot", clearDownloadsOnBoot)
put("autoFullscreenSeconds", autoFullscreenSeconds)
put("showUsbSongs", showUsbSongs)
put("autoDeleteSongs", autoDeleteSongs)
put("reserveStorageGb", reserveStorageGb)
put("floatingButtonEnabled", floatingButtonEnabled)
put("songTitleSubtitleEnabled", songTitleSubtitleEnabled)
put("orderedSongOriginal", orderedSongOriginal)
put("orderedVolumeMode", orderedVolumeMode)
put("orderedVocalMode", orderedVocalMode)
put("voiceEngineEnabled", voiceEngineEnabled)
put("voiceEngineVolume", voiceEngineVolume)
put("lightMode", lightMode)
put("audioDelayMs", audioDelayMs)
put("screenBrightness", screenBrightness)
put("screenMode", screenMode)
put("doubleScreenEnabled", doubleScreenEnabled)
put("tableBroadcastEnabled", tableBroadcastEnabled)
put("tableBroadcastSeconds", tableBroadcastSeconds)
put("tableBroadcastText", tableBroadcastText)
put("eqEnabled", eqEnabled)
put("eqPreset", eqPreset)
put("eqLevels", eqLevels)
put("eqGlobalMix", eqGlobalMix)
put("orderQueue", writeSongs(orderQueue))
put("sangHistory", writeSongs(sangHistory))
put("scoreHistory", JSONArray(scoreHistory.take(100)))
put("broadcastHistory", JSONArray(broadcastHistory.take(100)))
put("favoriteIds", JSONArray(favoriteIds.toList()))
put("hiddenSongIds", JSONArray(hiddenSongIds.toList()))
put("playlists", JSONArray(playlists))
put("playlistSongs", JSONObject().apply {
playlistSongs.forEach { (name, ids) -> put(name, JSONArray(ids.toList())) }
})
}
root.toString(2)
}.getOrDefault("{}")
fun writeSnapshot(snapshot: String) {
runCatching {
file.parentFile?.mkdirs()
file.writeText(snapshot)
}
}
fun toggleFavorite(song: Song): Boolean = toggleId(favoriteIds, stableId(song))
fun isFavorite(song: Song): Boolean = stableId(song) in favoriteIds
fun togglePlaylistSong(playlist: String, song: Song): Boolean = toggleId(songsInPlaylist(playlist), stableId(song))
fun isInPlaylist(playlist: String, song: Song): Boolean = stableId(song) in songsInPlaylist(playlist)
fun songsInPlaylist(playlist: String): MutableSet<String> = playlistSongs.getOrPut(playlist) { LinkedHashSet() }
private fun ensureDefaultPlaylists() {
listOf("我的收藏", "常唱歌曲").forEach { if (it !in playlists) playlists += it }
playlists.forEach(::songsInPlaylist)
}
private fun toggleId(ids: MutableSet<String>, id: String): Boolean = if (ids.remove(id)) false else ids.add(id)
companion object {
/**
* App 内部输出音量默认值(%)。
*
* 三处音量入口(播放界面-调音、声音设置-歌曲最大音量、远程控制-音乐音量)共用它;
* 远程页面 `assets/mobile/{index.html,app.js}` 里的默认值需与之保持一致。
*/
const val DEFAULT_MUSIC_VOLUME = 85
@JvmStatic
fun stableId(song: Song): String = song.id?.takeIf(String::isNotEmpty)
?: song.path?.takeIf(String::isNotEmpty)
?: "${song.title}|${song.singer}"
private fun readSongs(array: JSONArray?, out: MutableList<Song>) {
out.clear()
if (array == null) return
repeat(array.length()) { out += Song.fromJson(array.getJSONObject(it)) }
}
private fun writeSongs(songs: List<Song>): JSONArray = JSONArray().apply {
songs.take(200).forEach { put(it.toJson()) }
}
private fun JSONArray?.copyStringsTo(out: MutableCollection<String>) {
if (this == null) return
repeat(length()) { out += optString(it) }
}
}
}