diff --git a/MAINPAGE_REFACTORING_ANALYSIS.md b/MAINPAGE_REFACTORING_ANALYSIS.md new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/AppPreferenceKeys.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/AppPreferenceKeys.kt index 5272f06..198bd21 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/AppPreferenceKeys.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/AppPreferenceKeys.kt @@ -2,8 +2,6 @@ package io.github.miuzarte.scrcpyforandroid.constants object AppPreferenceKeys { const val PREFS_NAME = "scrcpy_app_prefs" - const val NATIVE_ADB_KEY_PREFS_NAME = "nativecore_adb_rsa" - const val NATIVE_ADB_PRIVATE_KEY = "priv" // Devices const val QUICK_DEVICES = "quick_devices" diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/Defaults.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/Defaults.kt new file mode 100644 index 0000000..eecfdb6 --- /dev/null +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/Defaults.kt @@ -0,0 +1,6 @@ +package io.github.miuzarte.scrcpyforandroid.constants + +object Defaults { + const val EVENT_LOG_LINES = 512 + const val ADB_PORT = 5555; +} \ No newline at end of file diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/models/DeviceModels.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/models/DeviceModels.kt index 38253ef..5b390bd 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/models/DeviceModels.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/models/DeviceModels.kt @@ -1,17 +1,129 @@ package io.github.miuzarte.scrcpyforandroid.models -import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults +import io.github.miuzarte.scrcpyforandroid.constants.Defaults + +// Composable 用, 不可变 List +class DeviceShortcuts(val devices: List) : List by devices { + fun marshalToString( + separator: String = DEFAULT_SEPARATOR, + ): String = joinToString(separator) { it.marshalToString() } + + companion object { + const val DEFAULT_SEPARATOR = "\n" + + fun unmarshalFrom( + s: String, + separator: String = DEFAULT_SEPARATOR, + ): DeviceShortcuts { + if (s.isBlank()) return DeviceShortcuts(emptyList()) + val list = s.splitToSequence(separator) + .mapNotNull { DeviceShortcut.unmarshalFrom(it) } + .toList() + return DeviceShortcuts(list) + } + } + + private fun getIndex(id: String) = devices.indexOfFirst { it.id == id } + private fun getIndex(host: String, port: Int) = devices.indexOfFirst { + it.host == host && it.port == port + } + + fun get(id: String) = devices.firstOrNull { it.id == id } + fun get(host: String, port: Int) = devices.firstOrNull { + it.host == host && it.port == port + } + + fun update( + id: String? = null, + host: String? = null, + port: Int? = null, + name: String? = null, + online: Boolean? = null, + newPort: Int? = null, + updateNameOnlyWhenEmpty: Boolean = false, + ): DeviceShortcuts { + val idx = if (id != null) getIndex(id) + else if (host != null && port != null) getIndex(host, port) + else -1 + + if (idx < 0) return this + val old = devices[idx] + + // 确定最终的属性值 + val finalName = when { + name == null -> old.name + updateNameOnlyWhenEmpty && old.name.isNotBlank() -> old.name + else -> name + } + val finalPort = newPort ?: old.port + val finalOnline = online ?: old.online + + // 若无任何变化,返回原实例 + if (finalName == old.name && finalPort == old.port && finalOnline == old.online) + return this + + val newList = devices.toMutableList().apply { + this[idx] = DeviceShortcut( + name = finalName, + host = old.host, + port = finalPort, + online = finalOnline + ) + } + return DeviceShortcuts( + if (newPort != null && newPort != old.port) + newList.distinctBy { it.id } + else newList + ) + } + + fun upsert( + shortcut: DeviceShortcut, + index: Int? = null, + ): DeviceShortcuts { + val existingIdx = getIndex(shortcut.id) + val newList = devices.toMutableList() + if (existingIdx >= 0) { + newList[existingIdx] = shortcut + } else { + if (index != null) newList.add(index, shortcut) + else newList.add(shortcut) + } + return DeviceShortcuts(newList) + } + + fun move(fromIndex: Int, toIndex: Int): DeviceShortcuts { + if (fromIndex !in devices.indices || toIndex !in devices.indices) return this + if (fromIndex == toIndex) return this + val mutable = devices.toMutableList() + val item = mutable.removeAt(fromIndex) + // 如果目标位置在原位置之后,移除后列表长度减1,因此目标索引需减1 + val target = if (toIndex > fromIndex) toIndex - 1 else toIndex + mutable.add(target, item) + return DeviceShortcuts(mutable) + } + + // 删除指定设备 + fun remove(id: String) = DeviceShortcuts(devices.filterNot { it.id == id }) + + // 清空所有设备 + fun clear() = DeviceShortcuts(emptyList()) + + // 复制当前实例 + fun copy(devices: List = this.devices): DeviceShortcuts = + DeviceShortcuts(devices) +} data class DeviceShortcut( val name: String = "", val host: String, - val port: Int = AppDefaults.ADB_PORT, + val port: Int = Defaults.ADB_PORT, val online: Boolean = false, ) { val id: String get() = "$host:$port" fun marshalToString( - separator: String = "|", + separator: String = DEFAULT_SEPARATOR, ): String = listOf( name.trim(), host.trim(), port.toString() ).joinToString( @@ -19,16 +131,17 @@ data class DeviceShortcut( ) companion object { + const val DEFAULT_SEPARATOR = "|" fun unmarshalFrom( s: String, - delimiter: String = "|", + separator: String = DEFAULT_SEPARATOR, ): DeviceShortcut? { - val parts = s.split(delimiter, limit = 3) + val parts = s.split(separator, limit = 3) return when (parts.size) { 3 -> { val name = parts[0].trim() val host = parts[1].trim() - val port = parts[2].trim().toIntOrNull() ?: AppDefaults.ADB_PORT + val port = parts[2].trim().toIntOrNull() ?: Defaults.ADB_PORT if (host.isNotBlank()) DeviceShortcut( name = name, host = host, @@ -45,7 +158,7 @@ data class DeviceShortcut( data class ConnectionTarget( val host: String, - val port: Int = AppDefaults.ADB_PORT, + val port: Int = Defaults.ADB_PORT, ) { fun marshalToString(): String = "$host:$port" @@ -55,17 +168,17 @@ data class ConnectionTarget( return when (parts.size) { 2 -> ConnectionTarget( host = parts[0].trim(), - port = parts[1].trim().toIntOrNull() ?: AppDefaults.ADB_PORT, + port = parts[1].trim().toIntOrNull() ?: Defaults.ADB_PORT, ) 1 -> ConnectionTarget( host = parts[0].trim(), - port = AppDefaults.ADB_PORT, + port = Defaults.ADB_PORT, ) 0 -> ConnectionTarget( host = s.trim(), - port = AppDefaults.ADB_PORT, + port = Defaults.ADB_PORT, ) else -> null diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/DirectAdbClient.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/DirectAdbClient.kt index e19972c..16f50c3 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/DirectAdbClient.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/DirectAdbClient.kt @@ -5,8 +5,7 @@ import android.os.Build import android.util.Base64 import android.util.Log import androidx.core.content.edit -import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults -import io.github.miuzarte.scrcpyforandroid.constants.AppPreferenceKeys +import io.github.miuzarte.scrcpyforandroid.storage.AppSettings import java.io.BufferedInputStream import java.io.Closeable import java.io.EOFException @@ -51,7 +50,7 @@ internal class DirectAdbTransport(private val context: Context) { val publicKeyX509: ByteArray get() = keys.second @Volatile - var keyName: String = AppDefaults.ADB_KEY_NAME + var keyName: String = AppSettings.ADB_KEY_NAME.defaultValue fun connect(host: String, port: Int): DirectAdbConnection { Log.i(TAG, "connect(): opening direct adbd transport to $host:$port") @@ -60,7 +59,7 @@ internal class DirectAdbTransport(private val context: Context) { port, privateKey, publicKeyX509, - keyName.ifBlank { AppDefaults.ADB_KEY_NAME }) + keyName.ifBlank { AppSettings.ADB_KEY_NAME.defaultValue }) conn.handshake() Log.i(TAG, "connect(): handshake success for $host:$port") return conn @@ -78,7 +77,7 @@ internal class DirectAdbTransport(private val context: Context) { val pairingKey = AdbPairingKey( privateKey = privateKey, - alias = keyName.ifBlank { AppDefaults.ADB_KEY_NAME }, + alias = keyName.ifBlank { AppSettings.ADB_KEY_NAME.defaultValue }, ) return DirectAdbPairingClient(targetHost, port, targetCode, pairingKey).use { it.start() @@ -106,11 +105,12 @@ internal class DirectAdbTransport(private val context: Context) { * Returns (privateKey, publicX509Bytes). */ private fun loadOrCreate(): Pair { + // TODO: migrate to data store val prefs = context.getSharedPreferences( - AppPreferenceKeys.NATIVE_ADB_KEY_PREFS_NAME, + "nativecore_adb_rsa", Context.MODE_PRIVATE ) - val privB64 = prefs.getString(AppPreferenceKeys.NATIVE_ADB_PRIVATE_KEY, null) + val privB64 = prefs.getString("priv", null) if (privB64 != null) { try { val kf = KeyFactory.getInstance("RSA") @@ -128,7 +128,7 @@ internal class DirectAdbTransport(private val context: Context) { val kp = kpg.generateKeyPair() prefs.edit { putString( - AppPreferenceKeys.NATIVE_ADB_PRIVATE_KEY, + "priv", Base64.encodeToString(kp.private.encoded, Base64.NO_WRAP) ) } @@ -203,7 +203,7 @@ internal class DirectAdbConnection( val port: Int, private val privateKey: PrivateKey, private val publicKeyX509: ByteArray, - private val keyName: String = AppDefaults.ADB_KEY_NAME, + private val keyName: String = AppSettings.ADB_KEY_NAME.defaultValue, ) : AutoCloseable { private val sha1DigestInfoPrefix = byteArrayOf( diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AdvancedConfigPage.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AdvancedConfigPage.kt index d2a3e1a..9bfcd92 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AdvancedConfigPage.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AdvancedConfigPage.kt @@ -12,9 +12,11 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.platform.LocalContext @@ -56,6 +58,7 @@ private val AUDIO_SOURCE_OPTIONS = listOf( "custom" to "自定义", ) +// TODO: Scrcpy.VideoSource private val VIDEO_SOURCE_OPTIONS = listOf( "display" to "display", "camera" to "camera", @@ -75,73 +78,16 @@ internal fun AdvancedConfigPage( contentPadding: PaddingValues, scrollBehavior: ScrollBehavior, snackbarHostState: SnackbarHostState, -// sessionStarted: Boolean, - - // audioEnabled: Boolean, - -// noControl: Boolean, -// onNoControlChange: (Boolean) -> Unit, - audioDup: Boolean, - onAudioDupChange: (Boolean) -> Unit, - audioSourcePreset: String, - onAudioSourcePresetChange: (String) -> Unit, - audioSourceCustom: String, - onAudioSourceCustomChange: (String) -> Unit, - videoSourcePreset: String, - onVideoSourcePresetChange: (String) -> Unit, - cameraIdInput: String, - onCameraIdInputChange: (String) -> Unit, - cameraFacingPreset: String, - onCameraFacingPresetChange: (String) -> Unit, - cameraSizePreset: String, - onCameraSizePresetChange: (String) -> Unit, - cameraSizeCustom: String, - onCameraSizeCustomChange: (String) -> Unit, + cameraSizeOptions: SnapshotStateList, cameraSizeDropdownItems: List, - cameraSizeIndex: Int, - cameraArInput: String, - onCameraArInputChange: (String) -> Unit, - cameraFpsInput: String, - onCameraFpsInputChange: (String) -> Unit, - cameraHighSpeed: Boolean, - onCameraHighSpeedChange: (Boolean) -> Unit, - noAudioPlayback: Boolean, - onNoAudioPlaybackChange: (Boolean) -> Unit, - noVideo: Boolean, - onNoVideoChange: (Boolean) -> Unit, - requireAudio: Boolean, - onRequireAudioChange: (Boolean) -> Unit, - turnScreenOff: Boolean, - onTurnScreenOffChange: (Boolean) -> Unit, - maxSizeInput: String, - onMaxSizeInputChange: (String) -> Unit, - maxFpsInput: String, - onMaxFpsInputChange: (String) -> Unit, - videoEncoderDropdownItems: List, videoEncoderTypeMap: Map, videoEncoderIndex: Int, - onVideoEncoderChange: (String) -> Unit, - videoCodecOptions: String, - onVideoCodecOptionsChange: (String) -> Unit, audioEncoderDropdownItems: List, audioEncoderTypeMap: Map, audioEncoderIndex: Int, - onAudioEncoderChange: (String) -> Unit, - audioCodecOptions: String, - onAudioCodecOptionsChange: (String) -> Unit, - onRefreshEncoders: () -> Unit, onRefreshCameraSizes: () -> Unit, - - newDisplayWidth: String, - newDisplayHeight: String, - newDisplayDpi: String, - displayIdInput: String, - cropWidth: String, - cropHeight: String, - cropX: String, - cropY: String, ) { val context = LocalContext.current @@ -152,20 +98,6 @@ internal fun AdvancedConfigPage( val scope = rememberCoroutineScope() - val maxSizePresetIndex = - presetIndexFromInputForAdvancedPage(maxSizeInput, ScrcpyPresets.MaxSize) - val maxFpsPresetIndex = presetIndexFromInputForAdvancedPage(maxFpsInput, ScrcpyPresets.MaxFPS) - val audioSourceItems = AUDIO_SOURCE_OPTIONS.map { it.second } - val audioSourceIndex = AUDIO_SOURCE_OPTIONS.indexOfFirst { it.first == audioSourcePreset } - .let { if (it >= 0) it else 0 } - val videoSourceItems = VIDEO_SOURCE_OPTIONS.map { it.second } - val videoSourceIndex = VIDEO_SOURCE_OPTIONS.indexOfFirst { it.first == videoSourcePreset } - .let { if (it >= 0) it else 0 } - val cameraFacingItems = CAMERA_FACING_OPTIONS.map { it.second } - val cameraFacingIndex = CAMERA_FACING_OPTIONS.indexOfFirst { it.first == cameraFacingPreset } - .let { if (it >= 0) it else 0 } - val cameraFpsPresetIndex = - presetIndexFromInputForAdvancedPage(cameraFpsInput, CAMERA_FPS_PRESETS) val videoEncoderEntries = videoEncoderDropdownItems.map { encoderName -> if (encoderName == "默认") { SpinnerEntry(title = encoderName) @@ -189,10 +121,108 @@ internal fun AdvancedConfigPage( } } + // TODO: handle custom value + // TODO: handle empty input var turnScreenOff by scrcpyOptions.turnScreenOff.asMutableState() var control by scrcpyOptions.control.asMutableState() var video by scrcpyOptions.video.asMutableState() + var videoSource by scrcpyOptions.videoSource.asMutableState() + val videoSourceItems = VIDEO_SOURCE_OPTIONS.map { it.second } + val videoSourceIndex = VIDEO_SOURCE_OPTIONS.indexOfFirst { + it.first == videoSource + }.let { if (it >= 0) it else 0 } + var displayId by scrcpyOptions.displayId.asMutableState() + + var cameraId by scrcpyOptions.cameraId.asMutableState() + var cameraFacing by scrcpyOptions.cameraFacing.asMutableState() + val cameraFacingItems = CAMERA_FACING_OPTIONS.map { it.second } + val cameraFacingIndex = CAMERA_FACING_OPTIONS.indexOfFirst { + it.first == cameraFacing + }.let { if (it >= 0) it else 0 } + var cameraSize by scrcpyOptions.cameraSize.asMutableState() + val cameraSizeIndex = when (cameraSize) { + "custom" -> cameraSizeOptions.size + 1 + in cameraSizeOptions -> cameraSizeOptions.indexOf(cameraSize) + 1 + else -> 0 + } + var cameraAr by scrcpyOptions.cameraAr.asMutableState() + var cameraFps by scrcpyOptions.cameraFps.asMutableState() + val cameraFpsPresetIndex = presetIndexFromInputForAdvancedPage( + cameraFps, CAMERA_FPS_PRESETS + ) + var cameraHighSpeed by scrcpyOptions.cameraHighSpeed.asMutableState() + + var audioSource by scrcpyOptions.audioSource.asMutableState() + val audioSourceItems = AUDIO_SOURCE_OPTIONS.map { it.second } + val audioSourceIndex = AUDIO_SOURCE_OPTIONS.indexOfFirst { + it.first == audioSource + }.let { if (it >= 0) it else 0 } + var audioDup by scrcpyOptions.audioDup.asMutableState() + var audioPlayback by scrcpyOptions.audioPlayback.asMutableState() + var requireAudio by scrcpyOptions.requireAudio.asMutableState() + + var maxSize by scrcpyOptions.maxSize.asMutableState() + val maxSizePresetIndex = presetIndexFromInputForAdvancedPage( + maxSize.toString(), ScrcpyPresets.MaxSize + ) + var maxFps by scrcpyOptions.maxFps.asMutableState() + val maxFpsPresetIndex = presetIndexFromInputForAdvancedPage( + maxFps, ScrcpyPresets.MaxFPS + ) + + var videoEncoder by scrcpyOptions.videoEncoder.asMutableState() + var videoCodecOptions by scrcpyOptions.videoCodecOptions.asMutableState() + var audioEncoder by scrcpyOptions.audioEncoder.asMutableState() + var audioCodecOptions by scrcpyOptions.audioCodecOptions.asMutableState() + + var newDisplayWidth by remember { + mutableStateOf("") + } + var newDisplayHeight by remember { + mutableStateOf("") + } + var newDisplayDpi by remember { + mutableStateOf("") + } + // [x][/] + // TODO: 填充当前值到输入框 + var newDisplay by scrcpyOptions.newDisplay.asMutableState() + fun updateNewDisplay() { + var nd = "" + if (newDisplayWidth.isNotBlank() && newDisplayHeight.isNotBlank()) { + nd += "${newDisplayWidth}x${newDisplayHeight}" + } + if (newDisplayDpi.isNotBlank()) { + nd += "/$newDisplayDpi" + } + newDisplay = nd + } + + var cropWidth by remember { + mutableStateOf("") + } + var cropHeight by remember { + mutableStateOf("") + } + var cropX by remember { + mutableStateOf("") + } + var cropY by remember { + mutableStateOf("") + } + // width:height:x:y + // TODO: 填充当前值到输入框 + var crop by scrcpyOptions.crop.asMutableState() + fun updateCrop(): Unit { + if (cropWidth.isNotBlank() + && cropHeight.isNotBlank() + && cropX.isNotBlank() + && cropY.isNotBlank() + ) crop = "$cropWidth:$cropHeight:$cropX:$cropY" + } + + // 高级参数 AppPageLazyColumn( contentPadding = contentPadding, @@ -237,13 +267,13 @@ internal fun AdvancedConfigPage( items = videoSourceItems, selectedIndex = videoSourceIndex, onSelectedIndexChange = { - videoSourcePreset = VIDEO_SOURCE_OPTIONS[it].first + videoSource = VIDEO_SOURCE_OPTIONS[it].first }, ) - if (videoSourcePreset == "display") { + if (videoSource == "display") { TextField( - value = displayIdInput, - onValueChange = { displayIdInput = it }, + value = displayId.toString(), + onValueChange = { displayId = it.toInt() }, label = "--display-id", singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), @@ -253,10 +283,10 @@ internal fun AdvancedConfigPage( .padding(bottom = UiSpacing.CardContent), ) } - if (videoSourcePreset == "camera") { + if (videoSource == "camera") { TextField( - value = cameraIdInput, - onValueChange = { cameraIdInput = it }, + value = cameraId, + onValueChange = { cameraId = it }, label = "--camera-id", singleLine = true, modifier = Modifier @@ -275,7 +305,7 @@ internal fun AdvancedConfigPage( items = cameraFacingItems, selectedIndex = cameraFacingIndex, onSelectedIndexChange = { - cameraFacingPreset = CAMERA_FACING_OPTIONS[it].first + cameraFacing = CAMERA_FACING_OPTIONS[it].first }, ) SuperDropdown( @@ -283,21 +313,20 @@ internal fun AdvancedConfigPage( summary = "--camera-size", items = cameraSizeDropdownItems, selectedIndex = cameraSizeIndex.coerceIn( - 0, - (cameraSizeDropdownItems.size - 1).coerceAtLeast(0) + 0, (cameraSizeDropdownItems.size - 1).coerceAtLeast(0) ), onSelectedIndexChange = { - cameraSizePreset = when (it) { + cameraSize = when (it) { 0 -> "" cameraSizeDropdownItems.lastIndex -> "custom" else -> cameraSizeDropdownItems[it] } }, ) - if (cameraSizePreset == "custom") { + if (cameraSize == "custom") { TextField( - value = cameraSizeCustom, - onValueChange = { cameraSizeCustom = it }, + value = cameraSize, + onValueChange = { cameraSize = it }, label = "--camera-size", singleLine = true, modifier = Modifier @@ -307,8 +336,8 @@ internal fun AdvancedConfigPage( ) } TextField( - value = cameraArInput, - onValueChange = { cameraArInput = it }, + value = cameraAr, + onValueChange = { cameraAr = it }, label = "--camera-ar", singleLine = true, modifier = Modifier @@ -322,8 +351,7 @@ internal fun AdvancedConfigPage( value = cameraFpsPresetIndex.toFloat(), onValueChange = { value -> val idx = value.roundToInt().coerceIn(0, CAMERA_FPS_PRESETS.lastIndex) - val preset = CAMERA_FPS_PRESETS[idx] - cameraFpsInput = if (preset == 0) "" else preset.toString() + cameraFps = CAMERA_FPS_PRESETS[idx] }, valueRange = 0f..CAMERA_FPS_PRESETS.lastIndex.toFloat(), steps = (CAMERA_FPS_PRESETS.size - 2).coerceAtLeast(0), @@ -332,14 +360,12 @@ internal fun AdvancedConfigPage( showUnitWhenZeroState = false, showKeyPoints = true, keyPoints = CAMERA_FPS_PRESETS.indices.map { it.toFloat() }, - displayText = cameraFpsInput, + displayText = cameraFps.toString(), inputHint = "0 或留空表示默认", - inputInitialValue = cameraFpsInput, + inputInitialValue = cameraFps.toString(), inputFilter = { it.filter(Char::isDigit) }, inputValueRange = 0f..Float.MAX_VALUE, - onInputConfirm = { - cameraFpsInput = if (normalized == "0") "" else normalized - }, + onInputConfirm = { cameraFps = it.toInt() }, ) SuperSwitch( title = "高帧率模式", @@ -358,12 +384,12 @@ internal fun AdvancedConfigPage( summary = "--audio-source", items = audioSourceItems, selectedIndex = audioSourceIndex, - onSelectedIndexChange = { audioSourcePreset = AUDIO_SOURCE_OPTIONS[it].first }, + onSelectedIndexChange = { audioSource = AUDIO_SOURCE_OPTIONS[it].first }, ) - if (audioSourcePreset == "custom") { + if (audioSource == "custom") { TextField( - value = audioSourceCustom, - onValueChange = { audioSourceCustom = it }, + value = audioSource, + onValueChange = { audioSource = it }, label = "--audio-source", singleLine = true, modifier = Modifier @@ -381,8 +407,8 @@ internal fun AdvancedConfigPage( SuperSwitch( title = "仅转发不播放", summary = "--no-audio-playback", - checked = noAudioPlayback, - onCheckedChange = { noAudioPlayback = it }, + checked = !audioPlayback, + onCheckedChange = { audioPlayback = !it }, ) SuperSwitch( title = "音频失败时终止 [TODO]", @@ -402,8 +428,7 @@ internal fun AdvancedConfigPage( value = maxSizePresetIndex.toFloat(), onValueChange = { value -> val idx = value.roundToInt().coerceIn(0, ScrcpyPresets.MaxSize.lastIndex) - val preset = ScrcpyPresets.MaxSize[idx] - maxSizeInput = if (preset == 0) "" else preset.toString() + maxSize = ScrcpyPresets.MaxSize[idx] }, valueRange = 0f..ScrcpyPresets.MaxSize.lastIndex.toFloat(), steps = (ScrcpyPresets.MaxSize.size - 2).coerceAtLeast(0), @@ -412,12 +437,12 @@ internal fun AdvancedConfigPage( showUnitWhenZeroState = false, showKeyPoints = true, keyPoints = ScrcpyPresets.MaxSize.indices.map { it.toFloat() }, - displayText = maxSizeInput, + displayText = maxSize.toString(), inputHint = "0 或留空表示关闭", - inputInitialValue = maxSizeInput, + inputInitialValue = maxSize.toString(), inputFilter = { it.filter(Char::isDigit) }, inputValueRange = 0f..Float.MAX_VALUE, - onInputConfirm = { maxSizeInput = it.ifBlank { "" } }, + onInputConfirm = { maxSize = it.toInt() }, ) SuperSlide( title = "最大帧率", @@ -425,8 +450,7 @@ internal fun AdvancedConfigPage( value = maxFpsPresetIndex.toFloat(), onValueChange = { value -> val idx = value.roundToInt().coerceIn(0, ScrcpyPresets.MaxFPS.lastIndex) - val preset = ScrcpyPresets.MaxFPS[idx] - maxFpsInput = if (preset == 0) "" else preset.toString() + maxFps = ScrcpyPresets.MaxFPS[idx].toString() }, valueRange = 0f..ScrcpyPresets.MaxFPS.lastIndex.toFloat(), steps = (ScrcpyPresets.MaxFPS.size - 2).coerceAtLeast(0), @@ -435,12 +459,12 @@ internal fun AdvancedConfigPage( showUnitWhenZeroState = false, showKeyPoints = true, keyPoints = ScrcpyPresets.MaxFPS.indices.map { it.toFloat() }, - displayText = maxFpsInput, + displayText = maxFps, inputHint = "0 或留空表示关闭", - inputInitialValue = maxFpsInput, + inputInitialValue = maxFps, inputFilter = { it.filter(Char::isDigit) }, inputValueRange = 0f..Float.MAX_VALUE, - onInputConfirm = { maxFpsInput = it.ifBlank { "" } }, + onInputConfirm = { maxFps = it }, ) } } @@ -457,7 +481,7 @@ internal fun AdvancedConfigPage( summary = "--video-encoder", items = videoEncoderEntries, selectedIndex = videoEncoderIndex, - onSelectedIndexChange = { videoEncoder = it }, + onSelectedIndexChange = { videoEncoder = videoEncoderEntries[it].title ?: "" }, ) TextField( value = videoCodecOptions, @@ -474,7 +498,7 @@ internal fun AdvancedConfigPage( summary = "--audio-encoder", items = audioEncoderEntries, selectedIndex = audioEncoderIndex, - onSelectedIndexChange = { audioEncoder = it }, + onSelectedIndexChange = { audioEncoder = audioEncoderEntries[it].title ?: "" }, ) TextField( value = audioCodecOptions, @@ -510,7 +534,7 @@ internal fun AdvancedConfigPage( ) { TextField( value = newDisplayWidth, - onValueChange = { newDisplayWidth = it }, + onValueChange = { newDisplayWidth = it; updateNewDisplay() }, label = "width", singleLine = true, keyboardOptions = KeyboardOptions( @@ -524,7 +548,7 @@ internal fun AdvancedConfigPage( ) TextField( value = newDisplayHeight, - onValueChange = { newDisplayHeight = it }, + onValueChange = { newDisplayHeight = it; updateNewDisplay() }, label = "height", singleLine = true, keyboardOptions = KeyboardOptions( @@ -538,7 +562,7 @@ internal fun AdvancedConfigPage( ) TextField( value = newDisplayDpi, - onValueChange = { newDisplayDpi = it }, + onValueChange = { newDisplayDpi = it; updateNewDisplay() }, label = "dpi", singleLine = true, keyboardOptions = KeyboardOptions( @@ -648,6 +672,13 @@ internal fun AdvancedConfigPage( } } +private fun presetIndexFromInputForAdvancedPage(raw: Int, presets: List): Int { + val exact = presets.indexOf(raw) + if (exact >= 0) return exact + val nearest = presets.withIndex().minByOrNull { (_, preset) -> kotlin.math.abs(preset - raw) } + return nearest?.index ?: 0 +} + private fun presetIndexFromInputForAdvancedPage(raw: String, presets: List): Int { if (raw.isBlank()) return 0 val value = raw.toIntOrNull() ?: return 0 diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DevicePage.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DevicePage.kt index 833341d..ac1aec7 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DevicePage.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DevicePage.kt @@ -10,26 +10,23 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateList -import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade.ScrcpySessionInfo -import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults +import io.github.miuzarte.scrcpyforandroid.constants.Defaults import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut +import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcuts import io.github.miuzarte.scrcpyforandroid.nativecore.NativeAdbService import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy @@ -37,14 +34,10 @@ import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.ListOptions import io.github.miuzarte.scrcpyforandroid.services.EventLogger import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent import io.github.miuzarte.scrcpyforandroid.services.fetchConnectedDeviceInfo -import io.github.miuzarte.scrcpyforandroid.services.loadDevicePageSettings -import io.github.miuzarte.scrcpyforandroid.services.loadQuickDevices import io.github.miuzarte.scrcpyforandroid.services.parseQuickTarget -import io.github.miuzarte.scrcpyforandroid.services.replaceQuickDevicePort -import io.github.miuzarte.scrcpyforandroid.services.saveQuickDevices -import io.github.miuzarte.scrcpyforandroid.services.updateQuickDeviceNameIfEmpty import io.github.miuzarte.scrcpyforandroid.services.upsertQuickDevice import io.github.miuzarte.scrcpyforandroid.storage.AppSettings +import io.github.miuzarte.scrcpyforandroid.storage.QuickDevices import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions import io.github.miuzarte.scrcpyforandroid.widgets.ConfigPanel import io.github.miuzarte.scrcpyforandroid.widgets.DeviceEditorScreen @@ -76,26 +69,29 @@ import java.util.concurrent.Executors private const val ADB_CONNECT_TIMEOUT_MS = 3_000L private const val ADB_KEEPALIVE_INTERVAL_MS = 3_000L private const val ADB_KEEPALIVE_TIMEOUT_MS = 1_500L -private const val ADB_AUTO_RECONNECT_DISCOVER_TIMEOUT_MS = 1_200L -private const val ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS = 1_500L -private const val ADB_TCP_PROBE_TIMEOUT_MS = 600 +private const val ADB_AUTO_RECONNECT_DISCOVER_TIMEOUT_MS = 2_000L +private const val ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS = 2_000L +private const val ADB_TCP_PROBE_TIMEOUT_MS = 500 + private const val DEVICE_SHORTCUT_SEPARATOR = "\u001F" -private val DeviceShortcutStateListSaver = - listSaver, String>( - save = { list -> - list.map { item -> +private val DeviceShortcutsSaver = + listSaver( + save = { shortcuts -> + // 将 DeviceShortcuts 中的每个设备转换为一行字符串 + shortcuts.devices.map { device -> listOf( - item.id, - item.name, - item.host, - item.port.toString(), - if (item.online) "1" else "0", + device.id, + device.name, + device.host, + device.port.toString(), + if (device.online) "1" else "0" ).joinToString(DEVICE_SHORTCUT_SEPARATOR) } }, restore = { saved -> - saved.mapNotNull { line -> + // 从保存的字符串列表恢复 DeviceShortcuts + DeviceShortcuts(saved.mapNotNull { line -> val parts = line.split(DEVICE_SHORTCUT_SEPARATOR) if (parts.size != 5) return@mapNotNull null val port = parts[3].toIntOrNull() ?: return@mapNotNull null @@ -103,18 +99,12 @@ private val DeviceShortcutStateListSaver = name = parts[1], host = parts[2], port = port, - online = parts[4] == "1", + online = parts[4] == "1" ) - }.toMutableStateList() + }.toMutableList()) }, ) -private val StringStateListSaver = - listSaver, String>( - save = { it.toList() }, - restore = { it.toMutableStateList() }, - ) - @Composable fun DeviceTabScreen( contentPadding: PaddingValues, @@ -144,6 +134,7 @@ fun DeviceTabScreen( val context = LocalContext.current val appSettings = remember(context) { AppSettings(context) } + val quickDevices = remember(context) { QuickDevices(context) } val scrcpyOptions = remember(context) { ScrcpyOptions(context) } val haptics = rememberAppHaptics() @@ -152,7 +143,7 @@ fun DeviceTabScreen( val virtualButtonLayout = remember(virtualButtonsLayout) { VirtualButtonActions.splitLayout(VirtualButtonActions.parseStoredLayout(virtualButtonsLayout)) } - val initialSettings = remember(context) { loadDevicePageSettings(context) } + // val initialSettings = remember(context) { loadDevicePageSettings(context) } val scope = rememberCoroutineScope() val adbWorkerDispatcher = remember { Executors.newSingleThreadExecutor { runnable -> @@ -173,7 +164,7 @@ fun DeviceTabScreen( var statusLine by rememberSaveable { mutableStateOf("未连接") } var adbConnected by rememberSaveable { mutableStateOf(false) } var currentTargetHost by rememberSaveable { mutableStateOf("") } - var currentTargetPort by rememberSaveable { mutableIntStateOf(AppDefaults.ADB_PORT) } + var currentTargetPort by rememberSaveable { mutableIntStateOf(Defaults.ADB_PORT) } var connectedDeviceLabel by rememberSaveable { mutableStateOf("未连接") } var sessionInfoWidth by rememberSaveable { mutableIntStateOf(0) } var sessionInfoHeight by rememberSaveable { mutableIntStateOf(0) } @@ -203,33 +194,33 @@ fun DeviceTabScreen( } } var previewControlsVisible by rememberSaveable { mutableStateOf(false) } - var editingDeviceId by rememberSaveable { mutableStateOf(null) } + var editingDevice by rememberSaveable { mutableStateOf(null) } var activeDeviceActionId by rememberSaveable { mutableStateOf(null) } var showReorderSheet by rememberSaveable { mutableStateOf(false) } var adbConnecting by rememberSaveable { mutableStateOf(false) } - var connectHost by rememberSaveable { mutableStateOf("") } - var connectPort by rememberSaveable { mutableStateOf(AppDefaults.ADB_PORT.toString()) } - var quickConnectInput by rememberSaveable { mutableStateOf(initialSettings.quickConnectInput) } var audioForwardingSupported by rememberSaveable { mutableStateOf(true) } var cameraMirroringSupported by rememberSaveable { mutableStateOf(true) } - var videoBitRateMbps by rememberSaveable { mutableFloatStateOf(initialSettings.videoBitRateMbps) } - var videoBitRateInput by rememberSaveable { mutableStateOf(initialSettings.videoBitRateInput) } - var audioBitRateKbps by rememberSaveable { mutableIntStateOf(initialSettings.audioBitRateKbps) } - val currentTarget = if (currentTargetHost.isNotBlank()) ConnectionTarget( - currentTargetHost, - currentTargetPort - ) else null + val currentTarget = + if (currentTargetHost.isNotBlank()) + ConnectionTarget( + currentTargetHost, + currentTargetPort + ) else null - val quickDevices = - rememberSaveable(saver = DeviceShortcutStateListSaver) { mutableStateListOf() } val sessionReconnectBlacklistHosts = remember { mutableSetOf() } LaunchedEffect(EventLogger.eventLog.size) { onCanClearLogsChange(EventLogger.hasLogs()) } + var quickDevicesList by quickDevices.quickDevicesList.asMutableState() + var savedShortcuts = rememberSaveable(saver = DeviceShortcutsSaver) { + DeviceShortcuts.unmarshalFrom(quickDevicesList) + } + var quickConnectInput by quickDevices.quickConnectInput.asMutableState() + /** * Disconnect the current ADB connection and stop any running scrcpy session. * @@ -264,16 +255,16 @@ fun DeviceTabScreen( } adbConnected = false currentTargetHost = "" - currentTargetPort = AppDefaults.ADB_PORT + currentTargetPort = Defaults.ADB_PORT audioForwardingSupported = true cameraMirroringSupported = true sessionInfo = null statusLine = "未连接" connectedDeviceLabel = "未连接" clearQuickOnlineForTarget?.let { target -> - if (target.host.isNotBlank()) { - upsertQuickDevice(context, quickDevices, target.host, target.port, false) - } + if (target.host.isNotBlank()) + savedShortcuts = + savedShortcuts.update(host = target.host, port = target.port, online = false) } logMessage?.let { logEvent(it) } if (!showSnackMessage.isNullOrBlank()) { @@ -482,15 +473,15 @@ fun DeviceTabScreen( if (audioEncoder.isNotBlank() && audioEncoder !in audioEncoderOptions) { audioEncoder = "" } - EventLogger.logEvent("编码器列表已刷新: video=${lists.videoEncoders.size} audio=${lists.audioEncoders.size}") + logEvent("编码器列表已刷新: video=${lists.videoEncoders.size} audio=${lists.audioEncoders.size}") if (lists.videoEncoders.isEmpty() && lists.audioEncoders.isEmpty()) { - EventLogger.logEvent( + logEvent( "提示: 编码器为空,请检查 server 路径/版本与设备系统日志", Log.WARN ) val preview = lists.rawOutput.lineSequence().take(20).joinToString(" | ") if (preview.isNotBlank()) { - EventLogger.logEvent("编码器原始输出: $preview", Log.DEBUG) + logEvent("编码器原始输出: $preview", Log.DEBUG) } } }.onFailure { e -> @@ -498,7 +489,7 @@ fun DeviceTabScreen( onAudioEncoderOptionsChange(emptyList()) onVideoEncoderTypeMapChange(emptyMap()) onAudioEncoderTypeMapChange(emptyMap()) - EventLogger.logEvent( + logEvent( "读取编码器列表失败: ${e.message ?: e.javaClass.simpleName}", Log.ERROR, e @@ -518,16 +509,16 @@ fun DeviceTabScreen( if (cameraSize.isNotBlank() && cameraSize != "custom" && cameraSize !in lists.sizes) { cameraSize = "" } - EventLogger.logEvent("camera sizes 已刷新: count=${lists.sizes.size}") + logEvent("camera sizes 已刷新: count=${lists.sizes.size}") if (lists.sizes.isEmpty()) { val preview = lists.rawOutput.lineSequence().take(20).joinToString(" | ") if (preview.isNotBlank()) { - EventLogger.logEvent("camera sizes 原始输出: $preview", Log.DEBUG) + logEvent("camera sizes 原始输出: $preview", Log.DEBUG) } } }.onFailure { e -> onCameraSizeOptionsChange(emptyList()) - EventLogger.logEvent( + logEvent( "读取 camera sizes 失败: ${e.message ?: e.javaClass.simpleName}", Log.ERROR, e @@ -548,9 +539,7 @@ fun DeviceTabScreen( connectedDeviceLabel = info.model applyConnectedDeviceCapabilities(info.sdkInt, info.androidRelease) - updateQuickDeviceNameIfEmpty(context, quickDevices, host, port, fullLabel) - connectHost = host - connectPort = port.toString() + quickDevices.updateQuickDeviceNameIfEmpty(host, port, fullLabel) statusLine = "$host:$port" logEvent("ADB 已连接: model=${info.model}, serial=${info.serial.ifBlank { "unknown" }}, manufacturer=${info.manufacturer.ifBlank { "unknown" }}, brand=${info.brand.ifBlank { "unknown" }}, device=${info.device.ifBlank { "unknown" }}, android=${info.androidRelease.ifBlank { "unknown" }}, sdk=${info.sdkInt}") @@ -561,30 +550,17 @@ fun DeviceTabScreen( refreshCameraSizeLists() } - LaunchedEffect(videoBitRateInput) { - val parsed = videoBitRateInput.toFloatOrNull() ?: return@LaunchedEffect - videoBitRateMbps = parsed.coerceAtLeast(0.1f) - } - - - LaunchedEffect(Unit) { - if (quickDevices.isEmpty()) { - quickDevices.clear() - quickDevices.addAll(loadQuickDevices(context)) - } - } - - LaunchedEffect(adbConnected, currentTargetHost, currentTargetPort, quickDevices.size) { + LaunchedEffect(adbConnected, currentTargetHost, currentTargetPort, savedShortcuts.size) { val activeId = if (adbConnected && currentTargetHost.isNotBlank()) { "$currentTargetHost:$currentTargetPort" } else { null } - for (index in quickDevices.indices) { - val item = quickDevices[index] + for (index in savedShortcuts.indices) { + val item = savedShortcuts[index] val shouldOnline = activeId != null && item.id == activeId if (item.online != shouldOnline) { - quickDevices[index] = item.copy(online = shouldOnline) + savedShortcuts = savedShortcuts.update(id = item.id, online = shouldOnline) } } } @@ -638,7 +614,7 @@ fun DeviceTabScreen( continue } - val quickCandidates = quickDevices.toList() + val quickCandidates = savedShortcuts.toList() if (quickCandidates.isNotEmpty()) { for (target in quickCandidates) { if (adbConnected || adbConnecting) break @@ -653,12 +629,10 @@ fun DeviceTabScreen( try { runAutoAdbConnect(target.host, target.port) adbConnected = true - upsertQuickDevice( - context, - quickDevices, - target.host, - target.port, - true, + savedShortcuts = savedShortcuts.update( + host = target.host, + port = target.port, + online = true ) handleAdbConnected(target.host, target.port) logEvent("ADB 快速探测连接成功: ${target.host}:${target.port}") @@ -686,24 +660,22 @@ fun DeviceTabScreen( delay(ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS) continue } - val knownDevice = quickDevices.firstOrNull { it.host == discoveredHost } + val knownDevice = savedShortcuts.firstOrNull { it.host == discoveredHost } if (knownDevice == null) { delay(ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS) continue } - val portToReplace = quickDevices.firstOrNull { + val portToReplace = savedShortcuts.firstOrNull { it.host == discoveredHost && - it.port != AppDefaults.ADB_PORT && + it.port != Defaults.ADB_PORT && it.port != discoveredPort }?.port if (portToReplace != null) { - replaceQuickDevicePort( - context = context, - quickDevices = quickDevices, + savedShortcuts = savedShortcuts.update( host = discoveredHost, - oldPort = portToReplace, + port = portToReplace, newPort = discoveredPort, - online = false, + online = false ) logEvent( "mDNS 发现新端口,已更新快速设备: $discoveredHost:$portToReplace -> $discoveredHost:$discoveredPort" @@ -718,12 +690,10 @@ fun DeviceTabScreen( try { runAutoAdbConnect(discoveredHost, discoveredPort) adbConnected = true - upsertQuickDevice( - context, - quickDevices, - discoveredHost, - discoveredPort, - true + savedShortcuts = savedShortcuts.update( + host = discoveredHost, + port = discoveredPort, + online = true ) handleAdbConnected(discoveredHost, discoveredPort) logEvent("ADB 自动重连成功: $discoveredHost:$discoveredPort") @@ -790,8 +760,8 @@ fun DeviceTabScreen( ) { val list = remember { ReorderableList( - { - quickDevices.map { device -> + itemsProvider = { + savedShortcuts.map { device -> ReorderableList.Item( id = device.id, title = device.name.ifBlank { device.host }, @@ -800,13 +770,7 @@ fun DeviceTabScreen( } }, onSettle = { fromIndex, toIndex -> - if (fromIndex < 0) return@ReorderableList - val to = toIndex.coerceIn(0, quickDevices.size) - if (fromIndex == to) return@ReorderableList - - val moved = quickDevices.removeAt(fromIndex) - quickDevices.add(to.coerceIn(0, quickDevices.size), moved) - saveQuickDevices(context, quickDevices) + savedShortcuts = savedShortcuts.move(fromIndex, toIndex) }, ) } @@ -822,34 +786,34 @@ fun DeviceTabScreen( } } - if (editingDeviceId != null) { - val device = quickDevices.firstOrNull { it.id == editingDeviceId } - if (device != null) { - DeviceEditorScreen( - contentPadding = contentPadding, - device = device, - onSave = { updated -> - val idx = quickDevices.indexOfFirst { it.id == device.id } - if (idx >= 0) { - quickDevices[idx] = updated.copy(online = quickDevices[idx].online) - saveQuickDevices(context, quickDevices) - } - editingDeviceId = null - }, - onDelete = { - quickDevices.removeAll { it.id == device.id } - saveQuickDevices(context, quickDevices) - editingDeviceId = null - }, - onBack = { editingDeviceId = null }, - ) - return - } - editingDeviceId = null + if (editingDevice != null) { + DeviceEditorScreen( + contentPadding = contentPadding, + device = editingDevice!!, + onSave = { updated -> + savedShortcuts = savedShortcuts.update( + id = editingDevice!!.id, + host = updated.host, + port = updated.port, + online = updated.online, + ) + editingDevice = null + }, + onDelete = { + savedShortcuts = savedShortcuts.remove(id = editingDevice!!.id) + editingDevice = null + }, + onBack = { editingDevice = null }, + ) + return } val devicePreviewCardHeightDp by appSettings.devicePreviewCardHeightDp.asState() val previewVirtualButtonShowText by appSettings.previewVirtualButtonShowText.asState() + + val audioBitRate by scrcpyOptions.audioBitRate.asState() + val videoBitRate by scrcpyOptions.videoBitRate.asState() + // 设备 AppPageLazyColumn( contentPadding = contentPadding, @@ -866,7 +830,7 @@ fun DeviceTabScreen( ) } - itemsIndexed(quickDevices, key = { _, device -> device.id }) { _, device -> + itemsIndexed(savedShortcuts, key = { _, device -> device.id }) { _, device -> val host = device.host val port = device.port val isConnectedTarget = adbConnected @@ -878,7 +842,7 @@ fun DeviceTabScreen( actionText = if (!isConnectedTarget) "连接" else "断开", actionEnabled = !busy && !adbConnecting, actionInProgress = adbConnecting && activeDeviceActionId == device.id, - onLongPress = { editingDeviceId = device.id }, + onLongPress = { editingDevice = device }, onContentClick = { scope.launch { snack.showSnackbar("长按可编辑设备") @@ -893,7 +857,7 @@ fun DeviceTabScreen( try { connectWithTimeout(host, port) adbConnected = true - upsertQuickDevice(context, quickDevices, host, port, true) + savedShortcuts = savedShortcuts.update(host = host, port = port, online = true) handleAdbConnected(host, port) } catch (e: Exception) { statusLine = "ADB 连接失败" @@ -926,13 +890,7 @@ fun DeviceTabScreen( enabled = !adbConnecting, onAddDevice = { val target = parseQuickTarget(quickConnectInput) ?: return@QuickConnectCard - upsertQuickDevice( - context, - quickDevices, - target.host, - target.port, - online = false - ) + savedShortcuts = savedShortcuts.update(host = target.host, port = target.port) scope.launch { snack.showSnackbar("已添加设备: ${target.host}:${target.port}") } @@ -944,7 +902,7 @@ fun DeviceTabScreen( try { connectWithTimeout(target.host, target.port) adbConnected = true - upsertQuickDevice(context, quickDevices, target.host, target.port, true) + savedShortcuts = savedShortcuts.update(host = target.host, port = target.port, online = true) handleAdbConnected(target.host, target.port) } catch (e: Exception) { statusLine = "ADB 连接失败" @@ -1006,13 +964,13 @@ fun DeviceTabScreen( "off" } else { "${session.codec} ${session.width}x${session.height} " + - "@${String.format("%.1f", videoBitRateMbps)}Mbps" + "@${String.format("%.1f", videoBitRate / 1_000_000)}Mbps" } val audioDetail = if (!audio) { "off" } else { val playback = if (!options.audioPlayback) "(no-playback)" else "" - "${options.audioCodec} ${audioBitRateKbps}kbps source=${options.audioSource}$playback" + "${options.audioCodec} ${videoBitRate / 1_000}Kbps source=${options.audioSource}$playback" } logEvent( "scrcpy 已启动: device=${session.deviceName}" + @@ -1085,24 +1043,3 @@ fun DeviceTabScreen( item { Spacer(Modifier.height(UiSpacing.BottomContent)) } } } - -private fun buildNewDisplayArg(width: String, height: String, dpi: String): String { - val w = width.toIntOrNull()?.takeIf { it > 0 } - val h = height.toIntOrNull()?.takeIf { it > 0 } - val d = dpi.toIntOrNull()?.takeIf { it > 0 } - val sizePart = if (w != null && h != null) "${w}x${h}" else "" - return when { - sizePart.isNotEmpty() && d != null -> "$sizePart/$d" - sizePart.isNotEmpty() -> sizePart - d != null -> "/$d" - else -> "" - } -} - -private fun buildCropArg(width: String, height: String, x: String, y: String): String { - val w = width.toIntOrNull()?.takeIf { it > 0 } ?: return "" - val h = height.toIntOrNull()?.takeIf { it > 0 } ?: return "" - val ox = x.toIntOrNull()?.takeIf { it >= 0 } ?: return "" - val oy = y.toIntOrNull()?.takeIf { it >= 0 } ?: return "" - return "$w:$h:$ox:$oy" -} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlPage.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlPage.kt index 2c65564..d9df992 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlPage.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlPage.kt @@ -22,6 +22,8 @@ import androidx.core.view.WindowInsetsControllerCompat import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade.ScrcpySessionInfo import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics +import io.github.miuzarte.scrcpyforandroid.storage.AppSettings +import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions import io.github.miuzarte.scrcpyforandroid.widgets.FullscreenControlScreen import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonBar @@ -38,9 +40,6 @@ data class FullscreenControlLaunch( fun FullscreenControlPage( launch: FullscreenControlLaunch, nativeCore: NativeCoreFacade, - virtualButtonsLayout: String, - showDebugInfo: Boolean, - showVirtualButtons: Boolean, onVideoSizeChanged: (width: Int, height: Int) -> Unit, onDismiss: () -> Unit, ) { @@ -48,15 +47,25 @@ fun FullscreenControlPage( BackHandler(enabled = true, onBack = onDismiss) val context = LocalContext.current + + val appSettings = remember(context) { AppSettings(context) } + val scrcpyOptions = remember(context) { ScrcpyOptions(context) } + val haptics = rememberAppHaptics() + val activity = remember(context) { context as? Activity } - val virtualButtonLayout = remember(virtualButtonsLayout) { + + var virtualButtonsLayout by appSettings.virtualButtonsLayout.asMutableState() + val buttonItems = remember(virtualButtonsLayout) { VirtualButtonActions.splitLayout(VirtualButtonActions.parseStoredLayout(virtualButtonsLayout)) } - val bar = remember(virtualButtonLayout) { + val fullscreenDebugInfo by appSettings.fullscreenDebugInfo.asState() + val showFullscreenVirtualButtons by appSettings.showFullscreenVirtualButtons.asState() + + val bar = remember(buttonItems) { VirtualButtonBar( - outsideActions = virtualButtonLayout.first, - moreActions = virtualButtonLayout.second, + outsideActions = buttonItems.first, + moreActions = buttonItems.second, ) } var session by remember(launch) { @@ -128,7 +137,7 @@ fun FullscreenControlPage( session = session, nativeCore = nativeCore, onDismiss = onDismiss, - showDebugInfo = showDebugInfo, + showDebugInfo = fullscreenDebugInfo, currentFps = currentFps, enableBackHandler = false, onInjectTouch = { action, pointerId, x, y, pressure, buttons -> @@ -146,7 +155,7 @@ fun FullscreenControlPage( }, ) - if (showVirtualButtons) { + if (showFullscreenVirtualButtons) { bar.Fullscreen( modifier = Modifier.align(Alignment.BottomCenter), onAction = { action -> diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainPage.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainPage.kt index 612ce29..203aa10 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainPage.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainPage.kt @@ -33,7 +33,6 @@ import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.runtime.setValue @@ -47,7 +46,6 @@ import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberDecoratedNavEntries import androidx.navigation3.ui.NavDisplay import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade -import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults import io.github.miuzarte.scrcpyforandroid.constants.UiMotion import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.nativecore.NativeAdbService @@ -95,245 +93,43 @@ private sealed interface RootScreen : NavKey { @Composable fun MainPage() { val context = LocalContext.current + val scope = rememberCoroutineScope() val activity = remember(context) { context as? Activity } val initialOrientation = remember(activity) { activity?.requestedOrientation ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } + DisposableEffect(activity) { + onDispose { + activity?.requestedOrientation = initialOrientation + } + } val nativeCore = remember(context) { NativeCoreFacade.get(context.applicationContext) } val adbService = remember(context) { NativeAdbService(context) } val scrcpy = remember(context) { Scrcpy(context) } val snackHostState = remember { SnackbarHostState() } + val saveableStateHolder = rememberSaveableStateHolder() val tabs = remember { MainTabDestination.entries } val pagerState = rememberPagerState( initialPage = MainTabDestination.Device.ordinal, pageCount = { tabs.size }) val currentTab = tabs[pagerState.currentPage] - val saveableStateHolder = rememberSaveableStateHolder() - val scope = rememberCoroutineScope() val rootBackStack = remember { mutableStateListOf(RootScreen.Home) } val currentRootScreen = rootBackStack.lastOrNull() as? RootScreen ?: RootScreen.Home - val deviceScrollBehavior = - MiuixScrollBehavior(canScroll = { currentTab == MainTabDestination.Device }) - val settingsScrollBehavior = - MiuixScrollBehavior(canScroll = { currentTab == MainTabDestination.Settings }) - val advancedScrollBehavior = MiuixScrollBehavior( + val devicesPageScrollBehavior = MiuixScrollBehavior( + canScroll = { currentTab == MainTabDestination.Device }) + val settingsPageScrollBehavior = MiuixScrollBehavior( + canScroll = { currentTab == MainTabDestination.Settings }) + val advancedPageScrollBehavior = MiuixScrollBehavior( canScroll = { - currentRootScreen is RootScreen.Advanced || currentRootScreen is RootScreen.VirtualButtonOrder - }, - ) - val stringListSaver = listSaver, String>( - save = { value -> ArrayList(value) }, - restore = { restored -> restored.toList() }, - ) - - val appSettings = remember(context) { AppSettings(context) } - val scrcpyOptions = remember(context) { ScrcpyOptions(context) } - - /* - val initialSettings = remember(context) { - loadMainSettings(context) - } - var audioEnabled by rememberSaveable { - mutableStateOf(initialSettings.audioEnabled) - } - var audioCodec by rememberSaveable { - mutableStateOf(initialSettings.audioCodec) - } - var videoCodec by rememberSaveable { - mutableStateOf(initialSettings.videoCodec) - } - var fullscreenDebugInfoEnabled by rememberSaveable { - mutableStateOf(initialSettings.fullscreenDebugInfoEnabled) - } - var showFullscreenVirtualButtons by rememberSaveable { - mutableStateOf(initialSettings.showFullscreenVirtualButtons) - } - var showPreviewVirtualButtonText by rememberSaveable { - mutableStateOf(initialSettings.showPreviewVirtualButtonText) - } - var keepScreenOnWhenStreamingEnabled by rememberSaveable { - mutableStateOf(initialSettings.keepScreenOnWhenStreamingEnabled) - } - var devicePreviewCardHeightDp by rememberSaveable { - mutableIntStateOf(initialSettings.devicePreviewCardHeightDp) - } - var virtualButtonsLayout by rememberSaveable { - mutableStateOf(initialSettings.virtualButtonsLayout) - } - var customServerUri by rememberSaveable { - mutableStateOf(initialSettings.customServerUri) - } - var serverRemotePath by rememberSaveable { - mutableStateOf(initialSettings.serverRemotePath) - } - var adbKeyName by rememberSaveable { - mutableStateOf(initialSettings.adbKeyName) - } - var adbPairingAutoDiscoverOnDialogOpen by rememberSaveable { - mutableStateOf(initialSettings.adbPairingAutoDiscoverOnDialogOpen) - } - var adbAutoReconnectPairedDevice by rememberSaveable { - mutableStateOf(initialSettings.adbAutoReconnectPairedDevice) - } - var adbMdnsLanDiscoveryEnabled by rememberSaveable { - mutableStateOf(initialSettings.adbMdnsLanDiscoveryEnabled) - } - - val initialDeviceSettings = remember(context) { - loadDevicePageSettings(context) - } - var noControl by rememberSaveable { - mutableStateOf(initialDeviceSettings.noControl) - } - var videoEncoder by rememberSaveable { - mutableStateOf(initialDeviceSettings.videoEncoder) - } - var videoCodecOptions by rememberSaveable { - mutableStateOf(initialDeviceSettings.videoCodecOptions) - } - var audioEncoder by rememberSaveable { - mutableStateOf(initialDeviceSettings.audioEncoder) - } - var audioCodecOptions by rememberSaveable { - mutableStateOf(initialDeviceSettings.audioCodecOptions) - } - var audioDup by rememberSaveable { - mutableStateOf(initialDeviceSettings.audioDup) - } - var audioSourcePreset by rememberSaveable { - mutableStateOf(initialDeviceSettings.audioSourcePreset) - } - var audioSourceCustom by rememberSaveable { - mutableStateOf(initialDeviceSettings.audioSourceCustom) - } - var videoSourcePreset by rememberSaveable { - mutableStateOf(initialDeviceSettings.videoSourcePreset) - } - var cameraIdInput by rememberSaveable { - mutableStateOf(initialDeviceSettings.cameraIdInput) - } - var cameraFacingPreset by rememberSaveable { - mutableStateOf(initialDeviceSettings.cameraFacingPreset) - } - var cameraSizePreset by rememberSaveable { - mutableStateOf(initialDeviceSettings.cameraSizePreset) - } - var cameraSizeCustom by rememberSaveable { - mutableStateOf(initialDeviceSettings.cameraSizeCustom) - } - var cameraArInput by rememberSaveable { - mutableStateOf(initialDeviceSettings.cameraAr) - } - var cameraFpsInput by rememberSaveable { - mutableStateOf(initialDeviceSettings.cameraFps) - } - var cameraHighSpeed by rememberSaveable { - mutableStateOf(initialDeviceSettings.cameraHighSpeed) - } - var noAudioPlayback by rememberSaveable { - mutableStateOf(initialDeviceSettings.noAudioPlayback) - } - var noVideo by rememberSaveable { - mutableStateOf(initialDeviceSettings.noVideo) - } - var requireAudio by rememberSaveable { - mutableStateOf(initialDeviceSettings.requireAudio) - } - var turnScreenOff by rememberSaveable { - mutableStateOf(initialDeviceSettings.turnScreenOff) - } - var maxSizeInput by rememberSaveable { - mutableStateOf(initialDeviceSettings.maxSizeInput) - } - var maxFpsInput by rememberSaveable { - mutableStateOf(initialDeviceSettings.maxFpsInput) - } - var newDisplayWidth by rememberSaveable { - mutableStateOf(initialDeviceSettings.newDisplayWidth) - } - var newDisplayHeight by rememberSaveable { - mutableStateOf(initialDeviceSettings.newDisplayHeight) - } - var newDisplayDpi by rememberSaveable { - mutableStateOf(initialDeviceSettings.newDisplayDpi) - } - var displayIdInput by rememberSaveable { - mutableStateOf(initialDeviceSettings.displayIdInput) - } - var cropWidth by rememberSaveable { - mutableStateOf(initialDeviceSettings.cropWidth) - } - var cropHeight by rememberSaveable { - mutableStateOf(initialDeviceSettings.cropHeight) - } - var cropX by rememberSaveable { - mutableStateOf(initialDeviceSettings.cropX) - } - var cropY by rememberSaveable { - mutableStateOf(initialDeviceSettings.cropY) - } - */ - - val videoEncoderOptions = remember { mutableStateListOf() } - val audioEncoderOptions = remember { mutableStateListOf() } - val videoEncoderTypeMap = remember { mutableStateMapOf() } - val audioEncoderTypeMap = remember { mutableStateMapOf() } - val cameraSizeOptions = remember { mutableStateListOf() } - var sessionStarted by remember { mutableStateOf(false) } - var refreshEncodersAction by remember { mutableStateOf<(() -> Unit)?>(null) } - var refreshCameraSizesAction by remember { mutableStateOf<(() -> Unit)?>(null) } - var clearLogsAction by remember { mutableStateOf<(() -> Unit)?>(null) } - var openReorderDevicesAction by remember { mutableStateOf<(() -> Unit)?>(null) } - var canClearLogs by remember { mutableStateOf(false) } - var showDeviceMenu by rememberSaveable { mutableStateOf(false) } - var lastExitBackPressAtMs by rememberSaveable { mutableLongStateOf(0L) } - var fullscreenOrientation by rememberSaveable { - mutableIntStateOf(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) - } - - var themeBaseIndex by appSettings.themeBaseIndex.asMutableState() - var monet by appSettings.monet.asMutableState() - val themeMode = resolveThemeMode(themeBaseIndex, monet) - val themeController = remember(themeMode) { ThemeController(colorSchemeMode = themeMode) } - - // Restore system orientation when MainPage leaves composition. - DisposableEffect(activity) { - onDispose { - activity?.requestedOrientation = initialOrientation - } - } - - val keepScreenOnWhenStreamingEnabled by appSettings.keepScreenOnWhenStreaming.asMutableState() - // Keep-screen-on is controlled globally, so fullscreen and preview share the same behavior. - DisposableEffect(activity, keepScreenOnWhenStreamingEnabled, sessionStarted) { - val window = activity?.window - val shouldKeepScreenOn = keepScreenOnWhenStreamingEnabled && sessionStarted - if (window != null && shouldKeepScreenOn) { - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } - onDispose { - if (window != null && shouldKeepScreenOn) { - window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + when (currentRootScreen) { + is RootScreen.Advanced -> true + is RootScreen.VirtualButtonOrder -> true + else -> false } - } - } - - // Fullscreen route can force orientation based on stream ratio; all other routes are portrait. - LaunchedEffect(activity, currentRootScreen, fullscreenOrientation) { - val targetOrientation = when (currentRootScreen) { - is RootScreen.Fullscreen -> fullscreenOrientation - else -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - } - activity?.requestedOrientation = targetOrientation - } - - val adbKeyName by appSettings.adbKeyName.asMutableState() - LaunchedEffect(adbKeyName) { - adbService.keyName = adbKeyName.ifBlank { AppDefaults.ADB_KEY_NAME } - } + }) fun popRoot() { if (rootBackStack.size > 1) { @@ -345,6 +141,7 @@ fun MainPage() { // 1) pop inner route // 2) switch tab back to Device // 3) double-back to exit and disconnect adb/scrcpy + var lastExitBackPressAtMs by rememberSaveable { mutableLongStateOf(0L) } fun handleBackNavigation() { if (rootBackStack.size > 1) { popRoot() @@ -394,6 +191,59 @@ fun MainPage() { } } + val appSettings = remember(context) { AppSettings(context) } + val scrcpyOptions = remember(context) { ScrcpyOptions(context) } + + val videoEncoderOptions = remember { mutableStateListOf() } + val audioEncoderOptions = remember { mutableStateListOf() } + val videoEncoderTypeMap = remember { mutableStateMapOf() } + val audioEncoderTypeMap = remember { mutableStateMapOf() } + val cameraSizeOptions = remember { mutableStateListOf() } + var sessionStarted by remember { mutableStateOf(false) } + var refreshEncodersAction by remember { mutableStateOf<(() -> Unit)?>(null) } + var refreshCameraSizesAction by remember { mutableStateOf<(() -> Unit)?>(null) } + var clearLogsAction by remember { mutableStateOf<(() -> Unit)?>(null) } + var openReorderDevicesAction by remember { mutableStateOf<(() -> Unit)?>(null) } + var canClearLogs by remember { mutableStateOf(false) } + var showDeviceMenu by rememberSaveable { mutableStateOf(false) } + var fullscreenOrientation by rememberSaveable { + mutableIntStateOf(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) + } + + var themeBaseIndex by appSettings.themeBaseIndex.asMutableState() + var monet by appSettings.monet.asMutableState() + val themeMode = resolveThemeMode(themeBaseIndex, monet) + val themeController = remember(themeMode) { ThemeController(colorSchemeMode = themeMode) } + + val keepScreenOnWhenStreamingEnabled by appSettings.keepScreenOnWhenStreaming.asMutableState() + // Keep-screen-on is controlled globally, so fullscreen and preview share the same behavior. + DisposableEffect(activity, keepScreenOnWhenStreamingEnabled, sessionStarted) { + val window = activity?.window + val shouldKeepScreenOn = keepScreenOnWhenStreamingEnabled && sessionStarted + if (window != null && shouldKeepScreenOn) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + onDispose { + if (window != null && shouldKeepScreenOn) { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + } + + // Fullscreen route can force orientation based on stream ratio; all other routes are portrait. + LaunchedEffect(activity, currentRootScreen, fullscreenOrientation) { + val targetOrientation = when (currentRootScreen) { + is RootScreen.Fullscreen -> fullscreenOrientation + else -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } + activity?.requestedOrientation = targetOrientation + } + + val adbKeyName by appSettings.adbKeyName.asState() + LaunchedEffect(adbKeyName) { + adbService.keyName = adbKeyName.ifBlank { AppSettings.ADB_KEY_NAME.defaultValue } + } + var customServerUri by appSettings.customServerUri.asMutableState() val picker = rememberLauncherForActivityResult( ActivityResultContracts.OpenDocument() @@ -477,7 +327,7 @@ fun MainPage() { }, ) }, - scrollBehavior = deviceScrollBehavior, + scrollBehavior = devicesPageScrollBehavior, ) }, ) { pagePadding -> @@ -487,15 +337,7 @@ fun MainPage() { adbService = adbService, scrcpy = scrcpy, snack = snackHostState, - scrollBehavior = deviceScrollBehavior, - /* - onNoControlChange = { - noControl = it - if (it) { - turnScreenOff = false - } - }, - */ + scrollBehavior = devicesPageScrollBehavior, videoEncoderOptions = videoEncoderOptions, onVideoEncoderOptionsChange = { videoEncoderOptions.clear() @@ -555,7 +397,7 @@ fun MainPage() { topBar = { TopAppBar( title = tab.title, - scrollBehavior = settingsScrollBehavior, + scrollBehavior = settingsPageScrollBehavior, ) }, ) { pagePadding -> @@ -576,7 +418,7 @@ fun MainPage() { ) ) }, - scrollBehavior = settingsScrollBehavior, + scrollBehavior = settingsPageScrollBehavior, ) } } @@ -605,101 +447,32 @@ fun MainPage() { ) } }, - scrollBehavior = advancedScrollBehavior, + scrollBehavior = advancedPageScrollBehavior, ) }, snackbarHost = { SnackbarHost(snackHostState) }, ) { pagePadding -> AdvancedConfigPage( contentPadding = pagePadding, - scrollBehavior = advancedScrollBehavior, - sessionStarted = sessionStarted, + scrollBehavior = advancedPageScrollBehavior, snackbarHostState = snackHostState, - audioEnabled = audioEnabled, - noControl = noControl, - onNoControlChange = { - noControl = it - if (it) { - turnScreenOff = false - } - }, - audioDup = audioDup, - onAudioDupChange = { audioDup = it }, - audioSourcePreset = audioSourcePreset, - onAudioSourcePresetChange = { audioSourcePreset = it }, - audioSourceCustom = audioSourceCustom, - onAudioSourceCustomChange = { audioSourceCustom = it }, - videoSourcePreset = videoSourcePreset, - onVideoSourcePresetChange = { videoSourcePreset = it }, - cameraIdInput = cameraIdInput, - onCameraIdInputChange = { cameraIdInput = it }, - cameraFacingPreset = cameraFacingPreset, - onCameraFacingPresetChange = { cameraFacingPreset = it }, - cameraSizePreset = cameraSizePreset, - onCameraSizePresetChange = { cameraSizePreset = it }, - cameraSizeCustom = cameraSizeCustom, - onCameraSizeCustomChange = { cameraSizeCustom = it }, cameraSizeDropdownItems = listOf("默认") + cameraSizeOptions + listOf("自定义"), - cameraSizeIndex = when (cameraSizePreset) { - "custom" -> cameraSizeOptions.size + 1 - in cameraSizeOptions -> cameraSizeOptions.indexOf(cameraSizePreset) + 1 - else -> 0 - }, - cameraArInput = cameraArInput, - onCameraArInputChange = { cameraArInput = it }, - cameraFpsInput = cameraFpsInput, - onCameraFpsInputChange = { cameraFpsInput = it }, - cameraHighSpeed = cameraHighSpeed, - onCameraHighSpeedChange = { cameraHighSpeed = it }, - noAudioPlayback = noAudioPlayback, - onNoAudioPlaybackChange = { noAudioPlayback = it }, - noVideo = noVideo, - onNoVideoChange = { noVideo = it }, - requireAudio = requireAudio, - onRequireAudioChange = { requireAudio = it }, - turnScreenOff = turnScreenOff, - onTurnScreenOffChange = { turnScreenOff = it }, - maxSizeInput = maxSizeInput, - onMaxSizeInputChange = { maxSizeInput = it }, - maxFpsInput = maxFpsInput, - onMaxFpsInputChange = { maxFpsInput = it }, + cameraSizeOptions = cameraSizeOptions, videoEncoderDropdownItems = videoEncoderDropdownItems, videoEncoderTypeMap = videoEncoderTypeMap, videoEncoderIndex = videoEncoderIndex, - onVideoEncoderChange = { videoEncoder = it }, - videoCodecOptions = videoCodecOptions, - onVideoCodecOptionsChange = { videoCodecOptions = it }, audioEncoderDropdownItems = audioEncoderDropdownItems, audioEncoderTypeMap = audioEncoderTypeMap, audioEncoderIndex = audioEncoderIndex, - onAudioEncoderChange = { audioEncoder = it }, - audioCodecOptions = audioCodecOptions, - onAudioCodecOptionsChange = { audioCodecOptions = it }, onRefreshEncoders = { refreshEncodersAction?.invoke() }, onRefreshCameraSizes = { refreshCameraSizesAction?.invoke() }, - newDisplayWidth = newDisplayWidth, - onNewDisplayWidthChange = { newDisplayWidth = it }, - newDisplayHeight = newDisplayHeight, - onNewDisplayHeightChange = { newDisplayHeight = it }, - newDisplayDpi = newDisplayDpi, - onNewDisplayDpiChange = { newDisplayDpi = it }, - displayIdInput = displayIdInput, - onDisplayIdInputChange = { displayIdInput = it }, - cropWidth = cropWidth, - onCropWidthChange = { cropWidth = it }, - cropHeight = cropHeight, - onCropHeightChange = { cropHeight = it }, - cropX = cropX, - onCropXChange = { cropX = it }, - cropY = cropY, - onCropYChange = { cropY = it }, ) } } entry(RootScreen.VirtualButtonOrder) { Scaffold( - modifier = Modifier.nestedScroll(advancedScrollBehavior.nestedScrollConnection), + modifier = Modifier.nestedScroll(advancedPageScrollBehavior.nestedScrollConnection), topBar = { TopAppBar( title = "虚拟按钮排序", @@ -711,19 +484,13 @@ fun MainPage() { ) } }, - scrollBehavior = advancedScrollBehavior, + scrollBehavior = advancedPageScrollBehavior, ) }, ) { pagePadding -> VirtualButtonOrderPage( contentPadding = pagePadding, - scrollBehavior = advancedScrollBehavior, - layoutString = virtualButtonsLayout, - onLayoutChange = { layout -> - virtualButtonsLayout = layout - }, - showPreviewText = showPreviewVirtualButtonText, - onShowPreviewTextChange = { showPreviewVirtualButtonText = it }, + scrollBehavior = advancedPageScrollBehavior, ) } } @@ -732,9 +499,6 @@ fun MainPage() { FullscreenControlPage( launch = screen.launch, nativeCore = nativeCore, - virtualButtonsLayout = virtualButtonsLayout, - showDebugInfo = fullscreenDebugInfoEnabled, - showVirtualButtons = showFullscreenVirtualButtons, onVideoSizeChanged = { width, height -> fullscreenOrientation = if (width >= height) { ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsPage.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsPage.kt index 3aeab6b..206c42a 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsPage.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsPage.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.core.net.toUri -import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlide @@ -212,7 +211,7 @@ fun SettingsScreen( TextField( value = serverRemotePath, onValueChange = { serverRemotePath = it }, - label = AppDefaults.SERVER_REMOTE_PATH, + label = AppSettings.SERVER_REMOTE_PATH.defaultValue, useLabelAsPlaceholder = true, singleLine = true, modifier = Modifier @@ -234,7 +233,7 @@ fun SettingsScreen( TextField( value = adbKeyName, onValueChange = { adbKeyName = it }, - label = AppDefaults.ADB_KEY_NAME, + label = AppSettings.ADB_KEY_NAME.defaultValue, useLabelAsPlaceholder = true, singleLine = true, modifier = Modifier diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderPage.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderPage.kt index 9f4f3cf..39b289b 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderPage.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderPage.kt @@ -6,7 +6,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn +import io.github.miuzarte.scrcpyforandroid.storage.AppSettings +import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions import io.github.miuzarte.scrcpyforandroid.widgets.ReorderableList import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions @@ -18,17 +21,16 @@ import top.yukonga.miuix.kmp.extra.SuperSwitch internal fun VirtualButtonOrderPage( contentPadding: PaddingValues, scrollBehavior: ScrollBehavior, - layoutString: String, - onLayoutChange: (String) -> Unit, - showPreviewText: Boolean, - onShowPreviewTextChange: (Boolean) -> Unit, ) { - var buttonItems by remember(layoutString) { - mutableStateOf(VirtualButtonActions.parseStoredLayout(layoutString)) - } + val context = LocalContext.current - fun emitChanges() { - onLayoutChange(VirtualButtonActions.encodeStoredLayout(buttonItems)) + val appSettings = remember(context) { AppSettings(context) } + val scrcpyOptions = remember(context) { ScrcpyOptions(context) } + + var previewVirtualButtonShowText by appSettings.previewVirtualButtonShowText.asMutableState() + var virtualButtonsLayout by appSettings.virtualButtonsLayout.asMutableState() + var buttonItems by remember(virtualButtonsLayout) { + mutableStateOf(VirtualButtonActions.parseStoredLayout(virtualButtonsLayout)) } AppPageLazyColumn( @@ -41,8 +43,8 @@ internal fun VirtualButtonOrderPage( SuperSwitch( title = "按钮显示文本", summary = "超过3个建议关闭,只对预览卡下方的虚拟按钮生效", - checked = showPreviewText, - onCheckedChange = onShowPreviewTextChange, + checked = previewVirtualButtonShowText, + onCheckedChange = { previewVirtualButtonShowText = it }, ) } } @@ -67,7 +69,7 @@ internal fun VirtualButtonOrderPage( buttonItems = buttonItems.toMutableList().apply { add(toIndex, removeAt(fromIndex)) } - emitChanges() + virtualButtonsLayout = VirtualButtonActions.encodeStoredLayout(buttonItems) }, showCheckbox = true, onCheckboxChange = { id, checked -> @@ -78,7 +80,7 @@ internal fun VirtualButtonOrderPage( item } } - emitChanges() + virtualButtonsLayout = VirtualButtonActions.encodeStoredLayout(buttonItems) }, )() } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/EventLogger.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/EventLogger.kt index 5f42a8c..84204d3 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/EventLogger.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/EventLogger.kt @@ -3,7 +3,7 @@ package io.github.miuzarte.scrcpyforandroid.services import android.util.Log import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.snapshots.SnapshotStateList -import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults +import io.github.miuzarte.scrcpyforandroid.constants.Defaults import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -16,14 +16,14 @@ import java.util.Locale */ object EventLogger { private const val LOG_TAG = "EventLogger" - + private val _eventLog: SnapshotStateList = mutableStateListOf() - + /** * Read-only access to the event log list. */ val eventLog: List get() = _eventLog - + /** * Log an event with timestamp and optional error. * @@ -34,12 +34,12 @@ object EventLogger { fun logEvent(message: String, level: Int = Log.INFO, error: Throwable? = null) { val timestamp = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date()) _eventLog.add(0, "[$timestamp] $message") - + // Rotate logs if exceeds max size - if (_eventLog.size > AppDefaults.EVENT_LOG_LINES) { - _eventLog.removeRange(AppDefaults.EVENT_LOG_LINES, _eventLog.size) + if (_eventLog.size > Defaults.EVENT_LOG_LINES) { + _eventLog.removeRange(Defaults.EVENT_LOG_LINES, _eventLog.size) } - + // Log to Android logcat when (level) { Log.ERROR -> if (error != null) Log.e(LOG_TAG, message, error) @@ -55,14 +55,14 @@ object EventLogger { else Log.i(LOG_TAG, message) } } - + /** * Clear all event logs. */ fun clearLogs() { _eventLog.clear() } - + /** * Check if there are any logs. */ diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/QuickDeviceStore.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/QuickDeviceStore.kt index e8e86d2..67fa3d3 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/QuickDeviceStore.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/QuickDeviceStore.kt @@ -2,8 +2,8 @@ package io.github.miuzarte.scrcpyforandroid.services import android.content.Context import androidx.core.content.edit -import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults import io.github.miuzarte.scrcpyforandroid.constants.AppPreferenceKeys +import io.github.miuzarte.scrcpyforandroid.constants.Defaults import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut @@ -21,7 +21,7 @@ internal fun loadQuickDevices(context: Context): List { 3 -> { val name = parts[0].trim() val host = parts[1].trim() - val port = parts[2].trim().toIntOrNull() ?: AppDefaults.ADB_PORT + val port = parts[2].trim().toIntOrNull() ?: Defaults.ADB_PORT if (host.isNotBlank()) { result.add( DeviceShortcut( @@ -38,8 +38,8 @@ internal fun loadQuickDevices(context: Context): List { // Backward compatibility with old format: name|host:port val name = parts[0].trim() val host = parts[1].substringBefore(":").trim() - val port = parts[1].substringAfter(":", AppDefaults.ADB_PORT.toString()).trim() - .toIntOrNull() ?: AppDefaults.ADB_PORT + val port = parts[1].substringAfter(":", Defaults.ADB_PORT.toString()).trim() + .toIntOrNull() ?: Defaults.ADB_PORT if (host.isNotBlank()) { result.add( DeviceShortcut( @@ -69,8 +69,8 @@ internal fun parseQuickTarget(raw: String): ConnectionTarget? { if (value.isEmpty()) return null val host = value.substringBefore(':').trim() if (host.isEmpty()) return null - val port = value.substringAfter(':', AppDefaults.ADB_PORT.toString()).trim().toIntOrNull() - ?: AppDefaults.ADB_PORT + val port = value.substringAfter(':', Defaults.ADB_PORT.toString()).trim().toIntOrNull() + ?: Defaults.ADB_PORT return ConnectionTarget(host, port) } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/AppSettings.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/AppSettings.kt index 614c4c5..a1e7d5e 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/AppSettings.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/AppSettings.kt @@ -7,59 +7,59 @@ import androidx.datastore.preferences.core.stringPreferencesKey class AppSettings(context: Context) : Settings(context, "AppSettings") { companion object { - private val THEME_BASE_INDEX = Pair( + val THEME_BASE_INDEX = Pair( intPreferencesKey("theme_base_index"), 0 ) - private val MONET = Pair( + val MONET = Pair( booleanPreferencesKey("monet"), false ) - private val FULLSCREEN_DEBUG_INFO = Pair( + val FULLSCREEN_DEBUG_INFO = Pair( booleanPreferencesKey("fullscreen_debug_info"), false ) - private val SHOW_FULLSCREEN_VIRTUAL_BUTTONS = Pair( + val SHOW_FULLSCREEN_VIRTUAL_BUTTONS = Pair( booleanPreferencesKey("show_fullscreen_virtual_buttons"), true ) - private val KEEP_SCREEN_ON_WHEN_STREAMING = Pair( + val KEEP_SCREEN_ON_WHEN_STREAMING = Pair( booleanPreferencesKey("keep_screen_on_when_streaming"), false ) - private val DEVICE_PREVIEW_CARD_HEIGHT_DP = Pair( + val DEVICE_PREVIEW_CARD_HEIGHT_DP = Pair( intPreferencesKey("device_preview_card_height_dp"), 320 ) - private val PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT = Pair( + val PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT = Pair( booleanPreferencesKey("preview_virtual_button_show_text"), true ) - private val VIRTUAL_BUTTONS_LAYOUT = Pair( + val VIRTUAL_BUTTONS_LAYOUT = Pair( stringPreferencesKey("virtual_buttons_layout"), "more:1,app_switch:1,home:0,back:1,menu:0,notification:0,volume_up:0,volume_down:0,volume_mute:0,power:0,screenshot:0" ) - private val CUSTOM_SERVER_URI = Pair( + val CUSTOM_SERVER_URI = Pair( stringPreferencesKey("custom_server_uri"), "" ) - private val SERVER_REMOTE_PATH = Pair( + val SERVER_REMOTE_PATH = Pair( stringPreferencesKey("server_remote_path"), "/data/local/tmp/scrcpy-server.jar" ) - private val ADB_KEY_NAME = Pair( + val ADB_KEY_NAME = Pair( stringPreferencesKey("adb_key_name"), "scrcpy" ) - private val ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN = Pair( + val ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN = Pair( booleanPreferencesKey("adb_pairing_auto_discover_on_dialog_open"), true ) - private val ADB_AUTO_RECONNECT_PAIRED_DEVICE = Pair( + val ADB_AUTO_RECONNECT_PAIRED_DEVICE = Pair( booleanPreferencesKey("adb_auto_reconnect_paired_device"), true ) - private val ADB_MDNS_LAN_DISCOVERY = Pair( + val ADB_MDNS_LAN_DISCOVERY = Pair( booleanPreferencesKey("adb_mdns_lan_discovery"), true ) @@ -87,24 +87,29 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") { val adbAutoReconnectPairedDevice by setting(ADB_AUTO_RECONNECT_PAIRED_DEVICE) val adbMdnsLanDiscovery by setting(ADB_MDNS_LAN_DISCOVERY) - override suspend fun toMap(): Map { - return mapOf( - THEME_BASE_INDEX.name to themeBaseIndex.get(), - MONET.name to monet.get(), - FULLSCREEN_DEBUG_INFO.name to fullscreenDebugInfo.get(), - SHOW_FULLSCREEN_VIRTUAL_BUTTONS.name to showFullscreenVirtualButtons.get(), - KEEP_SCREEN_ON_WHEN_STREAMING.name to keepScreenOnWhenStreaming.get(), - DEVICE_PREVIEW_CARD_HEIGHT_DP.name to devicePreviewCardHeightDp.get(), - PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT.name to previewVirtualButtonShowText.get(), - VIRTUAL_BUTTONS_LAYOUT.name to virtualButtonsLayout.get(), - CUSTOM_SERVER_URI.name to customServerUri.get(), - SERVER_REMOTE_PATH.name to serverRemotePath.get(), - ADB_KEY_NAME.name to adbKeyName.get(), - ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN.name to adbPairingAutoDiscoverOnDialogOpen.get(), - ADB_AUTO_RECONNECT_PAIRED_DEVICE.name to adbAutoReconnectPairedDevice.get(), - ADB_MDNS_LAN_DISCOVERY.name to adbMdnsLanDiscovery.get() - ) - } + override suspend fun toMap(): Map = mapOf( + // Theme Settings + THEME_BASE_INDEX.name to themeBaseIndex.get(), + MONET.name to monet.get(), + + // Scrcpy Settings + FULLSCREEN_DEBUG_INFO.name to fullscreenDebugInfo.get(), + SHOW_FULLSCREEN_VIRTUAL_BUTTONS.name to showFullscreenVirtualButtons.get(), + KEEP_SCREEN_ON_WHEN_STREAMING.name to keepScreenOnWhenStreaming.get(), + DEVICE_PREVIEW_CARD_HEIGHT_DP.name to devicePreviewCardHeightDp.get(), + PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT.name to previewVirtualButtonShowText.get(), + VIRTUAL_BUTTONS_LAYOUT.name to virtualButtonsLayout.get(), + + // Scrcpy Server Settings + CUSTOM_SERVER_URI.name to customServerUri.get(), + SERVER_REMOTE_PATH.name to serverRemotePath.get(), + + // ADB Settings + ADB_KEY_NAME.name to adbKeyName.get(), + ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN.name to adbPairingAutoDiscoverOnDialogOpen.get(), + ADB_AUTO_RECONNECT_PAIRED_DEVICE.name to adbAutoReconnectPairedDevice.get(), + ADB_MDNS_LAN_DISCOVERY.name to adbMdnsLanDiscovery.get() + ) // TODO? override fun validate(): Boolean = true diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/AppSettings_New.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/AppSettings_New.kt deleted file mode 100644 index ca8b37c..0000000 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/AppSettings_New.kt +++ /dev/null @@ -1,128 +0,0 @@ -package io.github.miuzarte.scrcpyforandroid.storage - -import android.content.Context -import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.intPreferencesKey -import androidx.datastore.preferences.core.stringPreferencesKey - -/** - * 使用委托方式的 AppSettings 示例 - * - * 使用方式: - * ``` - * // 获取值 - * val theme = appSettings.themeBaseIndex.get() - * - * // 设置值 - * appSettings.themeBaseIndex.set(1) - * - * // 观察变化(Flow) - * appSettings.themeBaseIndex.observe().collect { value -> } - * - * // 在 Composable 中观察(State) - * val theme by appSettings.themeBaseIndex.observeAsState() - * ``` - */ -class AppSettingsNew(context: Context) : Settings(context, "AppSettings") { - companion object { - private val THEME_BASE_INDEX = Pair( - intPreferencesKey("theme_base_index"), - 0 - ) - private val MONET = Pair( - booleanPreferencesKey("monet"), - false - ) - private val FULLSCREEN_DEBUG_INFO = Pair( - booleanPreferencesKey("fullscreen_debug_info"), - false - ) - private val SHOW_FULLSCREEN_VIRTUAL_BUTTONS = Pair( - booleanPreferencesKey("show_fullscreen_virtual_buttons"), - true - ) - private val KEEP_SCREEN_ON_WHEN_STREAMING = Pair( - booleanPreferencesKey("keep_screen_on_when_streaming"), - false - ) - private val DEVICE_PREVIEW_CARD_HEIGHT_DP = Pair( - intPreferencesKey("device_preview_card_height_dp"), - 320 - ) - private val PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT = Pair( - booleanPreferencesKey("preview_virtual_button_show_text"), - true - ) - private val VIRTUAL_BUTTONS_LAYOUT = Pair( - stringPreferencesKey("virtual_buttons_layout"), - "more:1,app_switch:1,home:0,back:1,menu:0,notification:0,volume_up:0,volume_down:0,volume_mute:0,power:0,screenshot:0" - ) - private val CUSTOM_SERVER_URI = Pair( - stringPreferencesKey("custom_server_uri"), - "" - ) - private val SERVER_REMOTE_PATH = Pair( - stringPreferencesKey("server_remote_path"), - "/data/local/tmp/scrcpy-server.jar" - ) - private val ADB_KEY_NAME = Pair( - stringPreferencesKey("adb_key_name"), - "scrcpy" - ) - private val ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN = Pair( - booleanPreferencesKey("adb_pairing_auto_discover_on_dialog_open"), - true - ) - private val ADB_AUTO_RECONNECT_PAIRED_DEVICE = Pair( - booleanPreferencesKey("adb_auto_reconnect_paired_device"), - true - ) - private val ADB_MDNS_LAN_DISCOVERY = Pair( - booleanPreferencesKey("adb_mdns_lan_discovery"), - true - ) - } - - // Theme Settings - val themeBaseIndex by setting(THEME_BASE_INDEX) - val monet by setting(MONET) - - // Scrcpy Settings - val fullscreenDebugInfo by setting(FULLSCREEN_DEBUG_INFO) - val showFullscreenVirtualButtons by setting(SHOW_FULLSCREEN_VIRTUAL_BUTTONS) - val keepScreenOnWhenStreaming by setting(KEEP_SCREEN_ON_WHEN_STREAMING) - val devicePreviewCardHeightDp by setting(DEVICE_PREVIEW_CARD_HEIGHT_DP) - val previewVirtualButtonShowText by setting(PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT) - val virtualButtonsLayout by setting(VIRTUAL_BUTTONS_LAYOUT) - - // Scrcpy Server Settings - val customServerUri by setting(CUSTOM_SERVER_URI) - val serverRemotePath by setting(SERVER_REMOTE_PATH) - - // ADB Settings - val adbKeyName by setting(ADB_KEY_NAME) - val adbPairingAutoDiscoverOnDialogOpen by setting(ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN) - val adbAutoReconnectPairedDevice by setting(ADB_AUTO_RECONNECT_PAIRED_DEVICE) - val adbMdnsLanDiscovery by setting(ADB_MDNS_LAN_DISCOVERY) - - override suspend fun toMap(): Map { - return mapOf( - THEME_BASE_INDEX.name to themeBaseIndex.get(), - MONET.name to monet.get(), - FULLSCREEN_DEBUG_INFO.name to fullscreenDebugInfo.get(), - SHOW_FULLSCREEN_VIRTUAL_BUTTONS.name to showFullscreenVirtualButtons.get(), - KEEP_SCREEN_ON_WHEN_STREAMING.name to keepScreenOnWhenStreaming.get(), - DEVICE_PREVIEW_CARD_HEIGHT_DP.name to devicePreviewCardHeightDp.get(), - PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT.name to previewVirtualButtonShowText.get(), - VIRTUAL_BUTTONS_LAYOUT.name to virtualButtonsLayout.get(), - CUSTOM_SERVER_URI.name to customServerUri.get(), - SERVER_REMOTE_PATH.name to serverRemotePath.get(), - ADB_KEY_NAME.name to adbKeyName.get(), - ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN.name to adbPairingAutoDiscoverOnDialogOpen.get(), - ADB_AUTO_RECONNECT_PAIRED_DEVICE.name to adbAutoReconnectPairedDevice.get(), - ADB_MDNS_LAN_DISCOVERY.name to adbMdnsLanDiscovery.get() - ) - } - - override fun validate(): Boolean = true -} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/PreferenceMigration.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/PreferenceMigration.kt new file mode 100644 index 0000000..952ae5d --- /dev/null +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/PreferenceMigration.kt @@ -0,0 +1,464 @@ +package io.github.miuzarte.scrcpyforandroid.storage + +import android.content.Context +import android.content.SharedPreferences +import android.util.Log +import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults +import io.github.miuzarte.scrcpyforandroid.constants.AppPreferenceKeys +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +private const val TAG = "PreferenceMigration" +private const val MIGRATION_COMPLETED_KEY = "migration_completed" + +/** + * 从旧的 SharedPreferences 迁移到新的 DataStore + */ +class PreferenceMigration(private val context: Context) { + + private val sharedPrefs: SharedPreferences by lazy { + context.getSharedPreferences(AppPreferenceKeys.PREFS_NAME, Context.MODE_PRIVATE) + } + + /** + * 检查是否已完成迁移 + */ + @Deprecated("做成设置内入口手动调用,这个没必要") + suspend fun isMigrationCompleted(): Boolean = withContext(Dispatchers.IO) { + sharedPrefs.getBoolean(MIGRATION_COMPLETED_KEY, false) + } + + /** + * 执行完整迁移 + * @return 迁移是否成功 + */ + suspend fun migrate(): Boolean = withContext(Dispatchers.IO) { + try { + if (isMigrationCompleted()) { + Log.i(TAG, "Migration already completed, skipping") + return@withContext true + } + + Log.i(TAG, "Starting migration from SharedPreferences to DataStore") + + // 迁移 AppSettings + migrateAppSettings() + + // 迁移 ScrcpyOptions + migrateScrcpyOptions() + + // 迁移 QuickDevices + migrateQuickDevices() + + // 标记迁移完成 + markMigrationCompleted() + + Log.i(TAG, "Migration completed successfully") + true + } catch (e: Exception) { + Log.e(TAG, "Migration failed", e) + false + } + } + + /** + * 迁移应用设置 + */ + private suspend fun migrateAppSettings() { + val appSettings = AppSettings(context) + + // Theme Settings + migrateInt( + AppPreferenceKeys.THEME_BASE_INDEX, + AppDefaults.THEME_BASE_INDEX, + appSettings.themeBaseIndex + ) + migrateBoolean( + AppPreferenceKeys.MONET, + AppDefaults.MONET, + appSettings.monet + ) + + // Scrcpy Settings + migrateBoolean( + AppPreferenceKeys.FULLSCREEN_DEBUG_INFO, + AppDefaults.FULLSCREEN_DEBUG_INFO, + appSettings.fullscreenDebugInfo + ) + migrateBoolean( + AppPreferenceKeys.SHOW_FULLSCREEN_VIRTUAL_BUTTONS, + AppDefaults.SHOW_FULLSCREEN_VIRTUAL_BUTTONS, + appSettings.showFullscreenVirtualButtons + ) + migrateBoolean( + AppPreferenceKeys.KEEP_SCREEN_ON_WHEN_STREAMING, + AppDefaults.KEEP_SCREEN_ON_WHEN_STREAMING, + appSettings.keepScreenOnWhenStreaming + ) + migrateInt( + AppPreferenceKeys.DEVICE_PREVIEW_CARD_HEIGHT_DP, + AppDefaults.DEVICE_PREVIEW_CARD_HEIGHT_DP, + appSettings.devicePreviewCardHeightDp + ) + migrateBoolean( + AppPreferenceKeys.PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT, + AppDefaults.PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT, + appSettings.previewVirtualButtonShowText + ) + migrateString( + AppPreferenceKeys.VIRTUAL_BUTTONS_LAYOUT, + AppDefaults.VIRTUAL_BUTTONS_LAYOUT, + appSettings.virtualButtonsLayout + ) + + // Scrcpy Server Settings + migrateString( + AppPreferenceKeys.CUSTOM_SERVER_URI, + AppDefaults.CUSTOM_SERVER_URI ?: "", + appSettings.customServerUri + ) + migrateString( + AppPreferenceKeys.SERVER_REMOTE_PATH, + AppDefaults.SERVER_REMOTE_PATH_INPUT, + appSettings.serverRemotePath + ) + + // ADB Settings + migrateString( + AppPreferenceKeys.ADB_KEY_NAME, + AppDefaults.ADB_KEY_NAME_INPUT, + appSettings.adbKeyName + ) + migrateBoolean( + AppPreferenceKeys.ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN, + AppDefaults.ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN, + appSettings.adbPairingAutoDiscoverOnDialogOpen + ) + migrateBoolean( + AppPreferenceKeys.ADB_AUTO_RECONNECT_PAIRED_DEVICE, + AppDefaults.ADB_AUTO_RECONNECT_PAIRED_DEVICE, + appSettings.adbAutoReconnectPairedDevice + ) + migrateBoolean( + AppPreferenceKeys.ADB_MDNS_LAN_DISCOVERY, + AppDefaults.ADB_MDNS_LAN_DISCOVERY, + appSettings.adbMdnsLanDiscovery + ) + + Log.d(TAG, "AppSettings migration completed") + } + + /** + * 迁移 Scrcpy 选项 + */ + private suspend fun migrateScrcpyOptions() { + val scrcpyOptions = ScrcpyOptions(context) + + // Audio & Video Codecs + migrateString( + AppPreferenceKeys.AUDIO_CODEC, + AppDefaults.AUDIO_CODEC, + scrcpyOptions.audioCodec + ) + migrateString( + AppPreferenceKeys.VIDEO_CODEC, + AppDefaults.VIDEO_CODEC, + scrcpyOptions.videoCodec + ) + + // Bit Rates + val audioBitRateKbps = sharedPrefs.getInt( + AppPreferenceKeys.AUDIO_BIT_RATE_KBPS, + AppDefaults.AUDIO_BIT_RATE_KBPS + ) + scrcpyOptions.audioBitRate.set(audioBitRateKbps * 1000) // Convert to bps + + val videoBitRateMbps = sharedPrefs.getFloat( + AppPreferenceKeys.VIDEO_BIT_RATE_MBPS, + AppDefaults.VIDEO_BIT_RATE_MBPS + ) + scrcpyOptions.videoBitRate.set((videoBitRateMbps * 1_000_000).toInt()) + + // Control Options + migrateBoolean( + AppPreferenceKeys.TURN_SCREEN_OFF, + AppDefaults.TURN_SCREEN_OFF, + scrcpyOptions.turnScreenOff + ) + migrateBoolean( + AppPreferenceKeys.NO_CONTROL, + AppDefaults.NO_CONTROL + ) { value -> + scrcpyOptions.control.set(!value) // Invert logic + } + migrateBoolean( + AppPreferenceKeys.NO_VIDEO, + AppDefaults.NO_VIDEO + ) { value -> + scrcpyOptions.video.set(!value) // Invert logic + } + + // Video Source + val videoSourcePreset = sharedPrefs.getString( + AppPreferenceKeys.VIDEO_SOURCE_PRESET, + AppDefaults.VIDEO_SOURCE_PRESET + ).orEmpty().ifBlank { AppDefaults.VIDEO_SOURCE_PRESET } + scrcpyOptions.videoSource.set(videoSourcePreset) + + migrateString( + AppPreferenceKeys.DISPLAY_ID, + AppDefaults.DISPLAY_ID + ) { value -> + value.toIntOrNull()?.let { scrcpyOptions.displayId.set(it) } + } + + // Camera Settings + migrateString( + AppPreferenceKeys.CAMERA_ID, + AppDefaults.CAMERA_ID, + scrcpyOptions.cameraId + ) + migrateString( + AppPreferenceKeys.CAMERA_FACING_PRESET, + AppDefaults.CAMERA_FACING_PRESET, + scrcpyOptions.cameraFacing + ) + migrateString( + AppPreferenceKeys.CAMERA_SIZE_PRESET, + AppDefaults.CAMERA_SIZE_PRESET + ) { value -> + if (value == "custom") { + val customSize = sharedPrefs.getString( + AppPreferenceKeys.CAMERA_SIZE_CUSTOM, + AppDefaults.CAMERA_SIZE_CUSTOM + ).orEmpty() + scrcpyOptions.cameraSize.set(customSize) + } else { + scrcpyOptions.cameraSize.set(value) + } + } + migrateString( + AppPreferenceKeys.CAMERA_AR, + AppDefaults.CAMERA_AR, + scrcpyOptions.cameraAr + ) + migrateString( + AppPreferenceKeys.CAMERA_FPS, + AppDefaults.CAMERA_FPS + ) { value -> + value.toIntOrNull()?.let { scrcpyOptions.cameraFps.set(it) } + } + migrateBoolean( + AppPreferenceKeys.CAMERA_HIGH_SPEED, + AppDefaults.CAMERA_HIGH_SPEED, + scrcpyOptions.cameraHighSpeed + ) + + // Audio Source + val audioSourcePreset = sharedPrefs.getString( + AppPreferenceKeys.AUDIO_SOURCE_PRESET, + AppDefaults.AUDIO_SOURCE_PRESET + ).orEmpty().ifBlank { AppDefaults.AUDIO_SOURCE_PRESET } + + if (audioSourcePreset == "custom") { + val customSource = sharedPrefs.getString( + AppPreferenceKeys.AUDIO_SOURCE_CUSTOM, + AppDefaults.AUDIO_SOURCE_CUSTOM + ).orEmpty() + scrcpyOptions.audioSource.set(customSource) + } else { + scrcpyOptions.audioSource.set(audioSourcePreset) + } + + migrateBoolean( + AppPreferenceKeys.AUDIO_DUP, + AppDefaults.AUDIO_DUP, + scrcpyOptions.audioDup + ) + migrateBoolean( + AppPreferenceKeys.NO_AUDIO_PLAYBACK, + AppDefaults.NO_AUDIO_PLAYBACK + ) { value -> + scrcpyOptions.audioPlayback.set(!value) // Invert logic + } + migrateBoolean( + AppPreferenceKeys.REQUIRE_AUDIO, + AppDefaults.REQUIRE_AUDIO, + scrcpyOptions.requireAudio + ) + + // Max Size & FPS + migrateString( + AppPreferenceKeys.MAX_SIZE_INPUT, + AppDefaults.MAX_SIZE_INPUT + ) { value -> + value.toIntOrNull()?.let { scrcpyOptions.maxSize.set(it) } + } + migrateString( + AppPreferenceKeys.MAX_FPS_INPUT, + AppDefaults.MAX_FPS_INPUT, + scrcpyOptions.maxFps + ) + + // Encoders & Codec Options + migrateString( + AppPreferenceKeys.VIDEO_ENCODER, + AppDefaults.VIDEO_ENCODER, + scrcpyOptions.videoEncoder + ) + migrateString( + AppPreferenceKeys.VIDEO_CODEC_OPTION, + AppDefaults.VIDEO_CODEC_OPTION, + scrcpyOptions.videoCodecOptions + ) + migrateString( + AppPreferenceKeys.AUDIO_ENCODER, + AppDefaults.AUDIO_ENCODER, + scrcpyOptions.audioEncoder + ) + migrateString( + AppPreferenceKeys.AUDIO_CODEC_OPTION, + AppDefaults.AUDIO_CODEC_OPTION, + scrcpyOptions.audioCodecOptions + ) + + // New Display + val newDisplayWidth = sharedPrefs.getString( + AppPreferenceKeys.NEW_DISPLAY_WIDTH, + AppDefaults.NEW_DISPLAY_WIDTH + ).orEmpty() + val newDisplayHeight = sharedPrefs.getString( + AppPreferenceKeys.NEW_DISPLAY_HEIGHT, + AppDefaults.NEW_DISPLAY_HEIGHT + ).orEmpty() + val newDisplayDpi = sharedPrefs.getString( + AppPreferenceKeys.NEW_DISPLAY_DPI, + AppDefaults.NEW_DISPLAY_DPI + ).orEmpty() + + if (newDisplayWidth.isNotBlank() && newDisplayHeight.isNotBlank()) { + val newDisplay = if (newDisplayDpi.isNotBlank()) { + "${newDisplayWidth}x${newDisplayHeight}/${newDisplayDpi}" + } else { + "${newDisplayWidth}x${newDisplayHeight}" + } + scrcpyOptions.newDisplay.set(newDisplay) + } + + // Crop + val cropWidth = sharedPrefs.getString( + AppPreferenceKeys.CROP_WIDTH, + AppDefaults.CROP_WIDTH + ).orEmpty() + val cropHeight = sharedPrefs.getString( + AppPreferenceKeys.CROP_HEIGHT, + AppDefaults.CROP_HEIGHT + ).orEmpty() + val cropX = sharedPrefs.getString( + AppPreferenceKeys.CROP_X, + AppDefaults.CROP_X + ).orEmpty() + val cropY = sharedPrefs.getString( + AppPreferenceKeys.CROP_Y, + AppDefaults.CROP_Y + ).orEmpty() + + if (cropWidth.isNotBlank() && cropHeight.isNotBlank() && + cropX.isNotBlank() && cropY.isNotBlank() + ) { + scrcpyOptions.crop.set("${cropWidth}:${cropHeight}:${cropX}:${cropY}") + } + + // Audio enabled flag (convert to audio option) + val audioEnabled = sharedPrefs.getBoolean( + AppPreferenceKeys.AUDIO_ENABLED, + AppDefaults.AUDIO_ENABLED + ) + scrcpyOptions.audio.set(audioEnabled) + + Log.d(TAG, "ScrcpyOptions migration completed") + } + + /** + * 迁移快速设备列表 + */ + private suspend fun migrateQuickDevices() { + val quickDevices = QuickDevices(context) + + // Migrate quick devices list + val quickDevicesRaw = sharedPrefs.getString( + AppPreferenceKeys.QUICK_DEVICES, + "" + ).orEmpty() + if (quickDevicesRaw.isNotBlank()) { + quickDevices.quickDevicesList.set(quickDevicesRaw) + } + + // Migrate quick connect input + migrateString( + AppPreferenceKeys.QUICK_CONNECT_INPUT, + AppDefaults.QUICK_CONNECT_INPUT, + quickDevices.quickConnectInput + ) + + Log.d(TAG, "QuickDevices migration completed") + } + + /** + * 标记迁移完成 + */ + private fun markMigrationCompleted() { + sharedPrefs.edit().putBoolean(MIGRATION_COMPLETED_KEY, true).apply() + } + + // Helper methods for different data types + + private suspend fun migrateString( + key: String, + defaultValue: String, + settingProperty: Settings.SettingProperty + ) { + val value = sharedPrefs.getString(key, defaultValue) + .orEmpty() + .ifBlank { defaultValue } + settingProperty.set(value) + } + + private suspend fun migrateString( + key: String, + defaultValue: String, + action: suspend (String) -> Unit + ) { + val value = sharedPrefs.getString(key, defaultValue) + .orEmpty() + .ifBlank { defaultValue } + action(value) + } + + private suspend fun migrateInt( + key: String, + defaultValue: Int, + settingProperty: Settings.SettingProperty + ) { + val value = sharedPrefs.getInt(key, defaultValue) + settingProperty.set(value) + } + + private suspend fun migrateBoolean( + key: String, + defaultValue: Boolean, + settingProperty: Settings.SettingProperty + ) { + val value = sharedPrefs.getBoolean(key, defaultValue) + settingProperty.set(value) + } + + private suspend fun migrateBoolean( + key: String, + defaultValue: Boolean, + action: suspend (Boolean) -> Unit + ) { + val value = sharedPrefs.getBoolean(key, defaultValue) + action(value) + } +} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/QuickDevices.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/QuickDevices.kt index e701d43..2d96a42 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/QuickDevices.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/QuickDevices.kt @@ -8,31 +8,28 @@ import kotlinx.coroutines.flow.map class QuickDevices(context: Context) : Settings(context, "QuickDevices") { companion object { - private val QUICK_DEVICES = Pair( - stringPreferencesKey("quick_devices"), + val QUICK_DEVICES_LIST = Pair( + stringPreferencesKey("quick_devices_list"), "", ) - private val QUICK_CONNECT_INPUT = Pair( + val QUICK_CONNECT_INPUT = Pair( stringPreferencesKey("quick_connect_input"), "", ) } - override suspend fun toMap(): Map { - return mapOf( - QUICK_DEVICES.name to getQuickDevicesRaw(), - QUICK_CONNECT_INPUT.name to getQuickConnectInput(), - ) - } + val quickDevicesList by setting(QUICK_DEVICES_LIST) + val quickConnectInput by setting(QUICK_CONNECT_INPUT) + + override suspend fun toMap(): Map = mapOf( + QUICK_DEVICES_LIST.name to quickDevicesList.get(), + QUICK_CONNECT_INPUT.name to quickConnectInput.get(), + ) override fun validate(): Boolean = true - private suspend fun getQuickDevicesRaw(): String = getValue(QUICK_DEVICES) - private suspend fun setQuickDevicesRaw(value: String) = setValue(QUICK_DEVICES, value) - private fun observeQuickDevicesRaw(): Flow = observe(QUICK_DEVICES) - suspend fun getQuickDevices(): List { - val raw = getQuickDevicesRaw() + val raw = quickDevicesList.get() if (raw.isBlank()) return emptyList() val result = mutableListOf() @@ -42,10 +39,10 @@ class QuickDevices(context: Context) : Settings(context, "QuickDevices") { return result } - suspend fun setQuickDevices(quickDevices: List) = - setQuickDevicesRaw(quickDevices.joinToString("\n") { it.marshalToString() }) + suspend fun setQuickDevices(list: List) = + quickDevicesList.set(list.joinToString("\n") { it.marshalToString() }) - fun observeQuickDevices(): Flow> = observeQuickDevicesRaw() + fun observeQuickDevices(): Flow> = quickDevicesList.observe() .map { raw -> if (raw.isBlank()) return@map emptyList() @@ -162,10 +159,6 @@ class QuickDevices(context: Context) : Settings(context, "QuickDevices") { * 清空所有快速设备 */ suspend fun clearQuickDevices() { - setQuickDevicesRaw("") + quickDevicesList.set("") } - - private suspend fun getQuickConnectInput(): String = getValue(QUICK_CONNECT_INPUT) - private suspend fun setQuickConnectInput(value: String) = setValue(QUICK_CONNECT_INPUT, value) - private fun observeQuickConnectInput(): Flow = observe(QUICK_CONNECT_INPUT) } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyOptions.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyOptions.kt index 076b5e2..c979f36 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyOptions.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyOptions.kt @@ -20,207 +20,207 @@ import kotlinx.coroutines.runBlocking class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { companion object { - private val CROP = Pair( + val CROP = Pair( stringPreferencesKey("crop"), "" ) - private val RECORD_FILENAME = Pair( + val RECORD_FILENAME = Pair( stringPreferencesKey("record_filename"), "" ) - private val VIDEO_CODEC_OPTIONS = Pair( + val VIDEO_CODEC_OPTIONS = Pair( stringPreferencesKey("video_codec_options"), "" ) - private val AUDIO_CODEC_OPTIONS = Pair( + val AUDIO_CODEC_OPTIONS = Pair( stringPreferencesKey("audio_codec_options"), "" ) - private val VIDEO_ENCODER = Pair( + val VIDEO_ENCODER = Pair( stringPreferencesKey("video_encoder"), "" ) - private val AUDIO_ENCODER = Pair( + val AUDIO_ENCODER = Pair( stringPreferencesKey("audio_encoder"), "" ) - private val CAMERA_ID = Pair( + val CAMERA_ID = Pair( stringPreferencesKey("camera_id"), "" ) - private val CAMERA_SIZE = Pair( + val CAMERA_SIZE = Pair( stringPreferencesKey("camera_size"), "" ) - private val CAMERA_AR = Pair( + val CAMERA_AR = Pair( stringPreferencesKey("camera_ar"), "" ) - private val CAMERA_FPS = Pair( + val CAMERA_FPS = Pair( intPreferencesKey("camera_fps"), 0 ) - private val LOG_LEVEL = Pair( + val LOG_LEVEL = Pair( stringPreferencesKey("log_level"), "info" ) - private val VIDEO_CODEC = Pair( + val VIDEO_CODEC = Pair( stringPreferencesKey("video_codec"), "h264" ) - private val AUDIO_CODEC = Pair( + val AUDIO_CODEC = Pair( stringPreferencesKey("audio_codec"), "opus" ) - private val VIDEO_SOURCE = Pair( + val VIDEO_SOURCE = Pair( stringPreferencesKey("video_source"), "display" ) - private val AUDIO_SOURCE = Pair( + val AUDIO_SOURCE = Pair( stringPreferencesKey("audio_source"), "output" ) - private val RECORD_FORMAT = Pair( + val RECORD_FORMAT = Pair( stringPreferencesKey("record_format"), "auto" ) - private val CAMERA_FACING = Pair( + val CAMERA_FACING = Pair( stringPreferencesKey("camera_facing"), "any" ) - private val MAX_SIZE = Pair( + val MAX_SIZE = Pair( intPreferencesKey("max_size"), 0 ) - private val VIDEO_BIT_RATE = Pair( + val VIDEO_BIT_RATE = Pair( intPreferencesKey("video_bit_rate"), 8000000 ) - private val AUDIO_BIT_RATE = Pair( + val AUDIO_BIT_RATE = Pair( intPreferencesKey("audio_bit_rate"), 128000 ) - private val MAX_FPS = Pair( + val MAX_FPS = Pair( stringPreferencesKey("max_fps"), "" ) - private val ANGLE = Pair( + val ANGLE = Pair( stringPreferencesKey("angle"), "" ) - private val CAPTURE_ORIENTATION = Pair( + val CAPTURE_ORIENTATION = Pair( intPreferencesKey("capture_orientation"), 0 ) - private val CAPTURE_ORIENTATION_LOCK = Pair( + val CAPTURE_ORIENTATION_LOCK = Pair( stringPreferencesKey("capture_orientation_lock"), "unlocked" ) - private val DISPLAY_ORIENTATION = Pair( + val DISPLAY_ORIENTATION = Pair( intPreferencesKey("display_orientation"), 0 ) - private val RECORD_ORIENTATION = Pair( + val RECORD_ORIENTATION = Pair( intPreferencesKey("record_orientation"), 0 ) - private val DISPLAY_IME_POLICY = Pair( + val DISPLAY_IME_POLICY = Pair( stringPreferencesKey("display_ime_policy"), "undefined" ) - private val DISPLAY_ID = Pair( + val DISPLAY_ID = Pair( intPreferencesKey("display_id"), 0 ) - private val SCREEN_OFF_TIMEOUT = Pair( + val SCREEN_OFF_TIMEOUT = Pair( longPreferencesKey("screen_off_timeout"), -1 ) - private val SHOW_TOUCHES = Pair( + val SHOW_TOUCHES = Pair( booleanPreferencesKey("show_touches"), false ) - private val FULLSCREEN = Pair( + val FULLSCREEN = Pair( booleanPreferencesKey("fullscreen"), false ) - private val CONTROL = Pair( + val CONTROL = Pair( booleanPreferencesKey("control"), true ) - private val VIDEO_PLAYBACK = Pair( + val VIDEO_PLAYBACK = Pair( booleanPreferencesKey("video_playback"), true ) - private val AUDIO_PLAYBACK = Pair( + val AUDIO_PLAYBACK = Pair( booleanPreferencesKey("audio_playback"), true ) - private val TURN_SCREEN_OFF = Pair( + val TURN_SCREEN_OFF = Pair( booleanPreferencesKey("turn_screen_off"), false ) - private val STAY_AWAKE = Pair( + val STAY_AWAKE = Pair( booleanPreferencesKey("stay_awake"), false ) - private val DISABLE_SCREENSAVER = Pair( + val DISABLE_SCREENSAVER = Pair( booleanPreferencesKey("disable_screensaver"), false ) - private val POWER_OFF_ON_CLOSE = Pair( + val POWER_OFF_ON_CLOSE = Pair( booleanPreferencesKey("power_off_on_close"), false ) - private val CLEANUP = Pair( + val CLEANUP = Pair( booleanPreferencesKey("cleanup"), true ) - private val POWER_ON = Pair( + val POWER_ON = Pair( booleanPreferencesKey("power_on"), true ) - private val VIDEO = Pair( + val VIDEO = Pair( booleanPreferencesKey("video"), true ) - private val AUDIO = Pair( + val AUDIO = Pair( booleanPreferencesKey("audio"), true ) - private val REQUIRE_AUDIO = Pair( + val REQUIRE_AUDIO = Pair( booleanPreferencesKey("require_audio"), false ) - private val KILL_ADB_ON_CLOSE = Pair( + val KILL_ADB_ON_CLOSE = Pair( booleanPreferencesKey("kill_adb_on_close"), false ) - private val CAMERA_HIGH_SPEED = Pair( + val CAMERA_HIGH_SPEED = Pair( booleanPreferencesKey("camera_high_speed"), false ) - private val LIST = Pair( + val LIST = Pair( stringPreferencesKey("list"), "null" ) - private val AUDIO_DUP = Pair( + val AUDIO_DUP = Pair( booleanPreferencesKey("audio_dup"), false ) - private val NEW_DISPLAY = Pair( + val NEW_DISPLAY = Pair( stringPreferencesKey("new_display"), "" ) - private val START_APP = Pair( + val START_APP = Pair( stringPreferencesKey("start_app"), "" ) - private val VD_DESTROY_CONTENT = Pair( + val VD_DESTROY_CONTENT = Pair( booleanPreferencesKey("vd_destroy_content"), true ) - private val VD_SYSTEM_DECORATIONS = Pair( + val VD_SYSTEM_DECORATIONS = Pair( booleanPreferencesKey("vd_system_decorations"), true ) @@ -278,61 +278,59 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { val vdDestroyContent by setting(VD_DESTROY_CONTENT) val vdSystemDecorations by setting(VD_SYSTEM_DECORATIONS) - override suspend fun toMap(): Map { - return mapOf( - CROP.name to crop.get(), - RECORD_FILENAME.name to recordFilename.get(), - VIDEO_CODEC_OPTIONS.name to videoCodecOptions.get(), - AUDIO_CODEC_OPTIONS.name to audioCodecOptions.get(), - VIDEO_ENCODER.name to videoEncoder.get(), - AUDIO_ENCODER.name to audioEncoder.get(), - CAMERA_ID.name to cameraId.get(), - CAMERA_SIZE.name to cameraSize.get(), - CAMERA_AR.name to cameraAr.get(), - CAMERA_FPS.name to cameraFps.get(), - LOG_LEVEL.name to logLevel.get(), - VIDEO_CODEC.name to videoCodec.get(), - AUDIO_CODEC.name to audioCodec.get(), - VIDEO_SOURCE.name to videoSource.get(), - AUDIO_SOURCE.name to audioSource.get(), - RECORD_FORMAT.name to recordFormat.get(), - CAMERA_FACING.name to cameraFacing.get(), - MAX_SIZE.name to maxSize.get(), - VIDEO_BIT_RATE.name to videoBitRate.get(), - AUDIO_BIT_RATE.name to audioBitRate.get(), - MAX_FPS.name to maxFps.get(), - ANGLE.name to angle.get(), - CAPTURE_ORIENTATION.name to captureOrientation.get(), - CAPTURE_ORIENTATION_LOCK.name to captureOrientationLock.get(), - DISPLAY_ORIENTATION.name to displayOrientation.get(), - RECORD_ORIENTATION.name to recordOrientation.get(), - DISPLAY_IME_POLICY.name to displayImePolicy.get(), - DISPLAY_ID.name to displayId.get(), - SCREEN_OFF_TIMEOUT.name to screenOffTimeout.get(), - SHOW_TOUCHES.name to showTouches.get(), - FULLSCREEN.name to fullscreen.get(), - CONTROL.name to control.get(), - VIDEO_PLAYBACK.name to videoPlayback.get(), - AUDIO_PLAYBACK.name to audioPlayback.get(), - TURN_SCREEN_OFF.name to turnScreenOff.get(), - STAY_AWAKE.name to stayAwake.get(), - DISABLE_SCREENSAVER.name to disableScreensaver.get(), - POWER_OFF_ON_CLOSE.name to powerOffOnClose.get(), - CLEANUP.name to cleanup.get(), - POWER_ON.name to powerOn.get(), - VIDEO.name to video.get(), - AUDIO.name to audio.get(), - REQUIRE_AUDIO.name to requireAudio.get(), - KILL_ADB_ON_CLOSE.name to killAdbOnClose.get(), - CAMERA_HIGH_SPEED.name to cameraHighSpeed.get(), - LIST.name to list.get(), - AUDIO_DUP.name to audioDup.get(), - NEW_DISPLAY.name to newDisplay.get(), - START_APP.name to startApp.get(), - VD_DESTROY_CONTENT.name to vdDestroyContent.get(), - VD_SYSTEM_DECORATIONS.name to vdSystemDecorations.get() - ) - } + override suspend fun toMap(): Map = mapOf( + CROP.name to crop.get(), + RECORD_FILENAME.name to recordFilename.get(), + VIDEO_CODEC_OPTIONS.name to videoCodecOptions.get(), + AUDIO_CODEC_OPTIONS.name to audioCodecOptions.get(), + VIDEO_ENCODER.name to videoEncoder.get(), + AUDIO_ENCODER.name to audioEncoder.get(), + CAMERA_ID.name to cameraId.get(), + CAMERA_SIZE.name to cameraSize.get(), + CAMERA_AR.name to cameraAr.get(), + CAMERA_FPS.name to cameraFps.get(), + LOG_LEVEL.name to logLevel.get(), + VIDEO_CODEC.name to videoCodec.get(), + AUDIO_CODEC.name to audioCodec.get(), + VIDEO_SOURCE.name to videoSource.get(), + AUDIO_SOURCE.name to audioSource.get(), + RECORD_FORMAT.name to recordFormat.get(), + CAMERA_FACING.name to cameraFacing.get(), + MAX_SIZE.name to maxSize.get(), + VIDEO_BIT_RATE.name to videoBitRate.get(), + AUDIO_BIT_RATE.name to audioBitRate.get(), + MAX_FPS.name to maxFps.get(), + ANGLE.name to angle.get(), + CAPTURE_ORIENTATION.name to captureOrientation.get(), + CAPTURE_ORIENTATION_LOCK.name to captureOrientationLock.get(), + DISPLAY_ORIENTATION.name to displayOrientation.get(), + RECORD_ORIENTATION.name to recordOrientation.get(), + DISPLAY_IME_POLICY.name to displayImePolicy.get(), + DISPLAY_ID.name to displayId.get(), + SCREEN_OFF_TIMEOUT.name to screenOffTimeout.get(), + SHOW_TOUCHES.name to showTouches.get(), + FULLSCREEN.name to fullscreen.get(), + CONTROL.name to control.get(), + VIDEO_PLAYBACK.name to videoPlayback.get(), + AUDIO_PLAYBACK.name to audioPlayback.get(), + TURN_SCREEN_OFF.name to turnScreenOff.get(), + STAY_AWAKE.name to stayAwake.get(), + DISABLE_SCREENSAVER.name to disableScreensaver.get(), + POWER_OFF_ON_CLOSE.name to powerOffOnClose.get(), + CLEANUP.name to cleanup.get(), + POWER_ON.name to powerOn.get(), + VIDEO.name to video.get(), + AUDIO.name to audio.get(), + REQUIRE_AUDIO.name to requireAudio.get(), + KILL_ADB_ON_CLOSE.name to killAdbOnClose.get(), + CAMERA_HIGH_SPEED.name to cameraHighSpeed.get(), + LIST.name to list.get(), + AUDIO_DUP.name to audioDup.get(), + NEW_DISPLAY.name to newDisplay.get(), + START_APP.name to startApp.get(), + VD_DESTROY_CONTENT.name to vdDestroyContent.get(), + VD_SYSTEM_DECORATIONS.name to vdSystemDecorations.get() + ) override fun validate(): Boolean = runBlocking { runCatching { @@ -342,61 +340,59 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { } // TODO: 处理空值 - suspend fun toClientOptions(): ClientOptions { - return ClientOptions( - crop = crop.get(), - recordFilename = recordFilename.get(), - videoCodecOptions = videoCodecOptions.get(), - audioCodecOptions = audioCodecOptions.get(), - videoEncoder = videoEncoder.get(), - audioEncoder = audioEncoder.get(), - cameraId = cameraId.get(), - cameraSize = cameraSize.get(), - cameraAr = cameraAr.get(), - cameraFps = cameraFps.get().toUShort(), - logLevel = LogLevel.valueOf(logLevel.get().uppercase()), - videoCodec = Codec.valueOf(videoCodec.get().uppercase()), - audioCodec = Codec.valueOf(audioCodec.get().uppercase()), - videoSource = VideoSource.valueOf(videoSource.get().uppercase()), - audioSource = AudioSource.valueOf(audioSource.get().uppercase()), - recordFormat = ClientOptions.RecordFormat.valueOf(recordFormat.get().uppercase()), - cameraFacing = CameraFacing.valueOf(cameraFacing.get().uppercase()), - maxSize = maxSize.get().toUShort(), - videoBitRate = videoBitRate.get().toUInt(), - audioBitRate = audioBitRate.get().toUInt(), - maxFps = maxFps.get(), - angle = angle.get(), - captureOrientation = Orientation.fromInt(captureOrientation.get()), - captureOrientationLock = OrientationLock.valueOf( - captureOrientationLock.get().uppercase() - ), - displayOrientation = Orientation.fromInt(displayOrientation.get()), - recordOrientation = Orientation.fromInt(recordOrientation.get()), - displayImePolicy = DisplayImePolicy.valueOf(displayImePolicy.get().uppercase()), - displayId = displayId.get().toUInt(), - screenOffTimeout = Tick(screenOffTimeout.get()), - showTouches = showTouches.get(), - fullscreen = fullscreen.get(), - control = control.get(), - videoPlayback = videoPlayback.get(), - audioPlayback = audioPlayback.get(), - turnScreenOff = turnScreenOff.get(), - stayAwake = stayAwake.get(), - disableScreensaver = disableScreensaver.get(), - powerOffOnClose = powerOffOnClose.get(), - cleanUp = cleanup.get(), - powerOn = powerOn.get(), - video = video.get(), - audio = audio.get(), - requireAudio = requireAudio.get(), - killAdbOnClose = killAdbOnClose.get(), - cameraHighSpeed = cameraHighSpeed.get(), - list = ListOptions.valueOf(list.get().uppercase()), - audioDup = audioDup.get(), - newDisplay = newDisplay.get(), - startApp = startApp.get(), - vdDestroyContent = vdDestroyContent.get(), - vdSystemDecorations = vdSystemDecorations.get() - ) - } + suspend fun toClientOptions() = ClientOptions( + crop = crop.get(), + recordFilename = recordFilename.get(), + videoCodecOptions = videoCodecOptions.get(), + audioCodecOptions = audioCodecOptions.get(), + videoEncoder = videoEncoder.get(), + audioEncoder = audioEncoder.get(), + cameraId = cameraId.get(), + cameraSize = cameraSize.get(), + cameraAr = cameraAr.get(), + cameraFps = cameraFps.get().toUShort(), + logLevel = LogLevel.valueOf(logLevel.get().uppercase()), + videoCodec = Codec.valueOf(videoCodec.get().uppercase()), + audioCodec = Codec.valueOf(audioCodec.get().uppercase()), + videoSource = VideoSource.valueOf(videoSource.get().uppercase()), + audioSource = AudioSource.valueOf(audioSource.get().uppercase()), + recordFormat = ClientOptions.RecordFormat.valueOf(recordFormat.get().uppercase()), + cameraFacing = CameraFacing.valueOf(cameraFacing.get().uppercase()), + maxSize = maxSize.get().toUShort(), + videoBitRate = videoBitRate.get().toUInt(), + audioBitRate = audioBitRate.get().toUInt(), + maxFps = maxFps.get(), + angle = angle.get(), + captureOrientation = Orientation.fromInt(captureOrientation.get()), + captureOrientationLock = OrientationLock.valueOf( + captureOrientationLock.get().uppercase() + ), + displayOrientation = Orientation.fromInt(displayOrientation.get()), + recordOrientation = Orientation.fromInt(recordOrientation.get()), + displayImePolicy = DisplayImePolicy.valueOf(displayImePolicy.get().uppercase()), + displayId = displayId.get().toUInt(), + screenOffTimeout = Tick(screenOffTimeout.get()), + showTouches = showTouches.get(), + fullscreen = fullscreen.get(), + control = control.get(), + videoPlayback = videoPlayback.get(), + audioPlayback = audioPlayback.get(), + turnScreenOff = turnScreenOff.get(), + stayAwake = stayAwake.get(), + disableScreensaver = disableScreensaver.get(), + powerOffOnClose = powerOffOnClose.get(), + cleanUp = cleanup.get(), + powerOn = powerOn.get(), + video = video.get(), + audio = audio.get(), + requireAudio = requireAudio.get(), + killAdbOnClose = killAdbOnClose.get(), + cameraHighSpeed = cameraHighSpeed.get(), + list = ListOptions.valueOf(list.get().uppercase()), + audioDup = audioDup.get(), + newDisplay = newDisplay.get(), + startApp = startApp.get(), + vdDestroyContent = vdDestroyContent.get(), + vdSystemDecorations = vdSystemDecorations.get() + ) } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt index 72036ed..63b9cf6 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt @@ -65,7 +65,7 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade.ScrcpySessionInfo -import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults +import io.github.miuzarte.scrcpyforandroid.constants.Defaults import io.github.miuzarte.scrcpyforandroid.constants.ScrcpyPresets import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics @@ -1367,7 +1367,7 @@ internal fun DeviceEditorScreen( TextButton( text = "保存", onClick = { - val p = port.toIntOrNull() ?: AppDefaults.ADB_PORT + val p = port.toIntOrNull() ?: Defaults.ADB_PORT val h = host.trim() if (h.isNotBlank()) { onSave( diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt index dcea852..e130f30 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt @@ -31,10 +31,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp -import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults import io.github.miuzarte.scrcpyforandroid.constants.UiAndroidKeycodes import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics +import io.github.miuzarte.scrcpyforandroid.storage.AppSettings import kotlinx.coroutines.launch import top.yukonga.miuix.kmp.basic.Button import top.yukonga.miuix.kmp.basic.ButtonDefaults @@ -135,7 +135,7 @@ object VirtualButtonActions { fun parseStoredLayout(raw: String): List { if (raw.isBlank()) - return parseStoredLayout(AppDefaults.VIRTUAL_BUTTONS_LAYOUT) + return parseStoredLayout(AppSettings.VIRTUAL_BUTTONS_LAYOUT.defaultValue) return raw.split(',').mapNotNull { item -> val parts = item.trim().split(':')