refactor: integrate data store; impl migration

This commit is contained in:
Miuzarte
2026-03-28 00:22:01 +08:00
parent 189d4a76b9
commit 82d06ceeae
20 changed files with 1198 additions and 1009 deletions

View File

View File

@@ -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"

View File

@@ -0,0 +1,6 @@
package io.github.miuzarte.scrcpyforandroid.constants
object Defaults {
const val EVENT_LOG_LINES = 512
const val ADB_PORT = 5555;
}

View File

@@ -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<DeviceShortcut>) : List<DeviceShortcut> 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<DeviceShortcut> = 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

View File

@@ -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<PrivateKey, ByteArray> {
// 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(

View File

@@ -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<String>,
cameraSizeDropdownItems: List<String>,
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<String>,
videoEncoderTypeMap: Map<String, String>,
videoEncoderIndex: Int,
onVideoEncoderChange: (String) -> Unit,
videoCodecOptions: String,
onVideoCodecOptionsChange: (String) -> Unit,
audioEncoderDropdownItems: List<String>,
audioEncoderTypeMap: Map<String, String>,
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("")
}
// [<width>x<height>][/<dpi>]
// 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>): 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>): Int {
if (raw.isBlank()) return 0
val value = raw.toIntOrNull() ?: return 0

View File

@@ -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<SnapshotStateList<DeviceShortcut>, String>(
save = { list ->
list.map { item ->
private val DeviceShortcutsSaver =
listSaver<DeviceShortcuts, String>(
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<SnapshotStateList<String>, 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<String?>(null) }
var editingDevice by rememberSaveable { mutableStateOf<DeviceShortcut?>(null) }
var activeDeviceActionId by rememberSaveable { mutableStateOf<String?>(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<String>() }
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"
}

View File

@@ -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 ->

View File

@@ -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<NavKey>(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<List<String>, 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<String>() }
val audioEncoderOptions = remember { mutableStateListOf<String>() }
val videoEncoderTypeMap = remember { mutableStateMapOf<String, String>() }
val audioEncoderTypeMap = remember { mutableStateMapOf<String, String>() }
val cameraSizeOptions = remember { mutableStateListOf<String>() }
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<String>() }
val audioEncoderOptions = remember { mutableStateListOf<String>() }
val videoEncoderTypeMap = remember { mutableStateMapOf<String, String>() }
val audioEncoderTypeMap = remember { mutableStateMapOf<String, String>() }
val cameraSizeOptions = remember { mutableStateListOf<String>() }
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

View File

@@ -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

View File

@@ -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)
},
)()
}

View File

@@ -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<String> = mutableStateListOf()
/**
* Read-only access to the event log list.
*/
val eventLog: List<String> 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.
*/

View File

@@ -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<DeviceShortcut> {
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<DeviceShortcut> {
// 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)
}

View File

@@ -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<String, Any> {
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<String, Any> = 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

View File

@@ -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<String, Any> {
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
}

View File

@@ -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<String>
) {
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<Int>
) {
val value = sharedPrefs.getInt(key, defaultValue)
settingProperty.set(value)
}
private suspend fun migrateBoolean(
key: String,
defaultValue: Boolean,
settingProperty: Settings.SettingProperty<Boolean>
) {
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)
}
}

View File

@@ -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<String, Any> {
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<String, Any> = 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<String> = observe(QUICK_DEVICES)
suspend fun getQuickDevices(): List<DeviceShortcut> {
val raw = getQuickDevicesRaw()
val raw = quickDevicesList.get()
if (raw.isBlank()) return emptyList()
val result = mutableListOf<DeviceShortcut>()
@@ -42,10 +39,10 @@ class QuickDevices(context: Context) : Settings(context, "QuickDevices") {
return result
}
suspend fun setQuickDevices(quickDevices: List<DeviceShortcut>) =
setQuickDevicesRaw(quickDevices.joinToString("\n") { it.marshalToString() })
suspend fun setQuickDevices(list: List<DeviceShortcut>) =
quickDevicesList.set(list.joinToString("\n") { it.marshalToString() })
fun observeQuickDevices(): Flow<List<DeviceShortcut>> = observeQuickDevicesRaw()
fun observeQuickDevices(): Flow<List<DeviceShortcut>> = 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<String> = observe(QUICK_CONNECT_INPUT)
}

View File

@@ -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<String, Any> {
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<String, Any> = 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()
)
}

View File

@@ -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(

View File

@@ -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<VirtualButtonItem> {
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(':')