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 { object AppPreferenceKeys {
const val PREFS_NAME = "scrcpy_app_prefs" 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 // Devices
const val QUICK_DEVICES = "quick_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 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( data class DeviceShortcut(
val name: String = "", val name: String = "",
val host: String, val host: String,
val port: Int = AppDefaults.ADB_PORT, val port: Int = Defaults.ADB_PORT,
val online: Boolean = false, val online: Boolean = false,
) { ) {
val id: String get() = "$host:$port" val id: String get() = "$host:$port"
fun marshalToString( fun marshalToString(
separator: String = "|", separator: String = DEFAULT_SEPARATOR,
): String = listOf( ): String = listOf(
name.trim(), host.trim(), port.toString() name.trim(), host.trim(), port.toString()
).joinToString( ).joinToString(
@@ -19,16 +131,17 @@ data class DeviceShortcut(
) )
companion object { companion object {
const val DEFAULT_SEPARATOR = "|"
fun unmarshalFrom( fun unmarshalFrom(
s: String, s: String,
delimiter: String = "|", separator: String = DEFAULT_SEPARATOR,
): DeviceShortcut? { ): DeviceShortcut? {
val parts = s.split(delimiter, limit = 3) val parts = s.split(separator, limit = 3)
return when (parts.size) { return when (parts.size) {
3 -> { 3 -> {
val name = parts[0].trim() val name = parts[0].trim()
val host = parts[1].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( if (host.isNotBlank()) DeviceShortcut(
name = name, name = name,
host = host, host = host,
@@ -45,7 +158,7 @@ data class DeviceShortcut(
data class ConnectionTarget( data class ConnectionTarget(
val host: String, val host: String,
val port: Int = AppDefaults.ADB_PORT, val port: Int = Defaults.ADB_PORT,
) { ) {
fun marshalToString(): String = "$host:$port" fun marshalToString(): String = "$host:$port"
@@ -55,17 +168,17 @@ data class ConnectionTarget(
return when (parts.size) { return when (parts.size) {
2 -> ConnectionTarget( 2 -> ConnectionTarget(
host = parts[0].trim(), host = parts[0].trim(),
port = parts[1].trim().toIntOrNull() ?: AppDefaults.ADB_PORT, port = parts[1].trim().toIntOrNull() ?: Defaults.ADB_PORT,
) )
1 -> ConnectionTarget( 1 -> ConnectionTarget(
host = parts[0].trim(), host = parts[0].trim(),
port = AppDefaults.ADB_PORT, port = Defaults.ADB_PORT,
) )
0 -> ConnectionTarget( 0 -> ConnectionTarget(
host = s.trim(), host = s.trim(),
port = AppDefaults.ADB_PORT, port = Defaults.ADB_PORT,
) )
else -> null else -> null

View File

@@ -5,8 +5,7 @@ import android.os.Build
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
import androidx.core.content.edit import androidx.core.content.edit
import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
import io.github.miuzarte.scrcpyforandroid.constants.AppPreferenceKeys
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.Closeable import java.io.Closeable
import java.io.EOFException import java.io.EOFException
@@ -51,7 +50,7 @@ internal class DirectAdbTransport(private val context: Context) {
val publicKeyX509: ByteArray get() = keys.second val publicKeyX509: ByteArray get() = keys.second
@Volatile @Volatile
var keyName: String = AppDefaults.ADB_KEY_NAME var keyName: String = AppSettings.ADB_KEY_NAME.defaultValue
fun connect(host: String, port: Int): DirectAdbConnection { fun connect(host: String, port: Int): DirectAdbConnection {
Log.i(TAG, "connect(): opening direct adbd transport to $host:$port") Log.i(TAG, "connect(): opening direct adbd transport to $host:$port")
@@ -60,7 +59,7 @@ internal class DirectAdbTransport(private val context: Context) {
port, port,
privateKey, privateKey,
publicKeyX509, publicKeyX509,
keyName.ifBlank { AppDefaults.ADB_KEY_NAME }) keyName.ifBlank { AppSettings.ADB_KEY_NAME.defaultValue })
conn.handshake() conn.handshake()
Log.i(TAG, "connect(): handshake success for $host:$port") Log.i(TAG, "connect(): handshake success for $host:$port")
return conn return conn
@@ -78,7 +77,7 @@ internal class DirectAdbTransport(private val context: Context) {
val pairingKey = AdbPairingKey( val pairingKey = AdbPairingKey(
privateKey = privateKey, privateKey = privateKey,
alias = keyName.ifBlank { AppDefaults.ADB_KEY_NAME }, alias = keyName.ifBlank { AppSettings.ADB_KEY_NAME.defaultValue },
) )
return DirectAdbPairingClient(targetHost, port, targetCode, pairingKey).use { return DirectAdbPairingClient(targetHost, port, targetCode, pairingKey).use {
it.start() it.start()
@@ -106,11 +105,12 @@ internal class DirectAdbTransport(private val context: Context) {
* Returns (privateKey, publicX509Bytes). * Returns (privateKey, publicX509Bytes).
*/ */
private fun loadOrCreate(): Pair<PrivateKey, ByteArray> { private fun loadOrCreate(): Pair<PrivateKey, ByteArray> {
// TODO: migrate to data store
val prefs = context.getSharedPreferences( val prefs = context.getSharedPreferences(
AppPreferenceKeys.NATIVE_ADB_KEY_PREFS_NAME, "nativecore_adb_rsa",
Context.MODE_PRIVATE Context.MODE_PRIVATE
) )
val privB64 = prefs.getString(AppPreferenceKeys.NATIVE_ADB_PRIVATE_KEY, null) val privB64 = prefs.getString("priv", null)
if (privB64 != null) { if (privB64 != null) {
try { try {
val kf = KeyFactory.getInstance("RSA") val kf = KeyFactory.getInstance("RSA")
@@ -128,7 +128,7 @@ internal class DirectAdbTransport(private val context: Context) {
val kp = kpg.generateKeyPair() val kp = kpg.generateKeyPair()
prefs.edit { prefs.edit {
putString( putString(
AppPreferenceKeys.NATIVE_ADB_PRIVATE_KEY, "priv",
Base64.encodeToString(kp.private.encoded, Base64.NO_WRAP) Base64.encodeToString(kp.private.encoded, Base64.NO_WRAP)
) )
} }
@@ -203,7 +203,7 @@ internal class DirectAdbConnection(
val port: Int, val port: Int,
private val privateKey: PrivateKey, private val privateKey: PrivateKey,
private val publicKeyX509: ByteArray, private val publicKeyX509: ByteArray,
private val keyName: String = AppDefaults.ADB_KEY_NAME, private val keyName: String = AppSettings.ADB_KEY_NAME.defaultValue,
) : AutoCloseable { ) : AutoCloseable {
private val sha1DigestInfoPrefix = byteArrayOf( 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.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -56,6 +58,7 @@ private val AUDIO_SOURCE_OPTIONS = listOf(
"custom" to "自定义", "custom" to "自定义",
) )
// TODO: Scrcpy.VideoSource
private val VIDEO_SOURCE_OPTIONS = listOf( private val VIDEO_SOURCE_OPTIONS = listOf(
"display" to "display", "display" to "display",
"camera" to "camera", "camera" to "camera",
@@ -75,73 +78,16 @@ internal fun AdvancedConfigPage(
contentPadding: PaddingValues, contentPadding: PaddingValues,
scrollBehavior: ScrollBehavior, scrollBehavior: ScrollBehavior,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
// sessionStarted: Boolean, cameraSizeOptions: SnapshotStateList<String>,
// 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,
cameraSizeDropdownItems: List<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>, videoEncoderDropdownItems: List<String>,
videoEncoderTypeMap: Map<String, String>, videoEncoderTypeMap: Map<String, String>,
videoEncoderIndex: Int, videoEncoderIndex: Int,
onVideoEncoderChange: (String) -> Unit,
videoCodecOptions: String,
onVideoCodecOptionsChange: (String) -> Unit,
audioEncoderDropdownItems: List<String>, audioEncoderDropdownItems: List<String>,
audioEncoderTypeMap: Map<String, String>, audioEncoderTypeMap: Map<String, String>,
audioEncoderIndex: Int, audioEncoderIndex: Int,
onAudioEncoderChange: (String) -> Unit,
audioCodecOptions: String,
onAudioCodecOptionsChange: (String) -> Unit,
onRefreshEncoders: () -> Unit, onRefreshEncoders: () -> Unit,
onRefreshCameraSizes: () -> Unit, onRefreshCameraSizes: () -> Unit,
newDisplayWidth: String,
newDisplayHeight: String,
newDisplayDpi: String,
displayIdInput: String,
cropWidth: String,
cropHeight: String,
cropX: String,
cropY: String,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -152,20 +98,6 @@ internal fun AdvancedConfigPage(
val scope = rememberCoroutineScope() 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 -> val videoEncoderEntries = videoEncoderDropdownItems.map { encoderName ->
if (encoderName == "默认") { if (encoderName == "默认") {
SpinnerEntry(title = 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 turnScreenOff by scrcpyOptions.turnScreenOff.asMutableState()
var control by scrcpyOptions.control.asMutableState() var control by scrcpyOptions.control.asMutableState()
var video by scrcpyOptions.video.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( AppPageLazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
@@ -237,13 +267,13 @@ internal fun AdvancedConfigPage(
items = videoSourceItems, items = videoSourceItems,
selectedIndex = videoSourceIndex, selectedIndex = videoSourceIndex,
onSelectedIndexChange = { onSelectedIndexChange = {
videoSourcePreset = VIDEO_SOURCE_OPTIONS[it].first videoSource = VIDEO_SOURCE_OPTIONS[it].first
}, },
) )
if (videoSourcePreset == "display") { if (videoSource == "display") {
TextField( TextField(
value = displayIdInput, value = displayId.toString(),
onValueChange = { displayIdInput = it }, onValueChange = { displayId = it.toInt() },
label = "--display-id", label = "--display-id",
singleLine = true, singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
@@ -253,10 +283,10 @@ internal fun AdvancedConfigPage(
.padding(bottom = UiSpacing.CardContent), .padding(bottom = UiSpacing.CardContent),
) )
} }
if (videoSourcePreset == "camera") { if (videoSource == "camera") {
TextField( TextField(
value = cameraIdInput, value = cameraId,
onValueChange = { cameraIdInput = it }, onValueChange = { cameraId = it },
label = "--camera-id", label = "--camera-id",
singleLine = true, singleLine = true,
modifier = Modifier modifier = Modifier
@@ -275,7 +305,7 @@ internal fun AdvancedConfigPage(
items = cameraFacingItems, items = cameraFacingItems,
selectedIndex = cameraFacingIndex, selectedIndex = cameraFacingIndex,
onSelectedIndexChange = { onSelectedIndexChange = {
cameraFacingPreset = CAMERA_FACING_OPTIONS[it].first cameraFacing = CAMERA_FACING_OPTIONS[it].first
}, },
) )
SuperDropdown( SuperDropdown(
@@ -283,21 +313,20 @@ internal fun AdvancedConfigPage(
summary = "--camera-size", summary = "--camera-size",
items = cameraSizeDropdownItems, items = cameraSizeDropdownItems,
selectedIndex = cameraSizeIndex.coerceIn( selectedIndex = cameraSizeIndex.coerceIn(
0, 0, (cameraSizeDropdownItems.size - 1).coerceAtLeast(0)
(cameraSizeDropdownItems.size - 1).coerceAtLeast(0)
), ),
onSelectedIndexChange = { onSelectedIndexChange = {
cameraSizePreset = when (it) { cameraSize = when (it) {
0 -> "" 0 -> ""
cameraSizeDropdownItems.lastIndex -> "custom" cameraSizeDropdownItems.lastIndex -> "custom"
else -> cameraSizeDropdownItems[it] else -> cameraSizeDropdownItems[it]
} }
}, },
) )
if (cameraSizePreset == "custom") { if (cameraSize == "custom") {
TextField( TextField(
value = cameraSizeCustom, value = cameraSize,
onValueChange = { cameraSizeCustom = it }, onValueChange = { cameraSize = it },
label = "--camera-size", label = "--camera-size",
singleLine = true, singleLine = true,
modifier = Modifier modifier = Modifier
@@ -307,8 +336,8 @@ internal fun AdvancedConfigPage(
) )
} }
TextField( TextField(
value = cameraArInput, value = cameraAr,
onValueChange = { cameraArInput = it }, onValueChange = { cameraAr = it },
label = "--camera-ar", label = "--camera-ar",
singleLine = true, singleLine = true,
modifier = Modifier modifier = Modifier
@@ -322,8 +351,7 @@ internal fun AdvancedConfigPage(
value = cameraFpsPresetIndex.toFloat(), value = cameraFpsPresetIndex.toFloat(),
onValueChange = { value -> onValueChange = { value ->
val idx = value.roundToInt().coerceIn(0, CAMERA_FPS_PRESETS.lastIndex) val idx = value.roundToInt().coerceIn(0, CAMERA_FPS_PRESETS.lastIndex)
val preset = CAMERA_FPS_PRESETS[idx] cameraFps = CAMERA_FPS_PRESETS[idx]
cameraFpsInput = if (preset == 0) "" else preset.toString()
}, },
valueRange = 0f..CAMERA_FPS_PRESETS.lastIndex.toFloat(), valueRange = 0f..CAMERA_FPS_PRESETS.lastIndex.toFloat(),
steps = (CAMERA_FPS_PRESETS.size - 2).coerceAtLeast(0), steps = (CAMERA_FPS_PRESETS.size - 2).coerceAtLeast(0),
@@ -332,14 +360,12 @@ internal fun AdvancedConfigPage(
showUnitWhenZeroState = false, showUnitWhenZeroState = false,
showKeyPoints = true, showKeyPoints = true,
keyPoints = CAMERA_FPS_PRESETS.indices.map { it.toFloat() }, keyPoints = CAMERA_FPS_PRESETS.indices.map { it.toFloat() },
displayText = cameraFpsInput, displayText = cameraFps.toString(),
inputHint = "0 或留空表示默认", inputHint = "0 或留空表示默认",
inputInitialValue = cameraFpsInput, inputInitialValue = cameraFps.toString(),
inputFilter = { it.filter(Char::isDigit) }, inputFilter = { it.filter(Char::isDigit) },
inputValueRange = 0f..Float.MAX_VALUE, inputValueRange = 0f..Float.MAX_VALUE,
onInputConfirm = { onInputConfirm = { cameraFps = it.toInt() },
cameraFpsInput = if (normalized == "0") "" else normalized
},
) )
SuperSwitch( SuperSwitch(
title = "高帧率模式", title = "高帧率模式",
@@ -358,12 +384,12 @@ internal fun AdvancedConfigPage(
summary = "--audio-source", summary = "--audio-source",
items = audioSourceItems, items = audioSourceItems,
selectedIndex = audioSourceIndex, selectedIndex = audioSourceIndex,
onSelectedIndexChange = { audioSourcePreset = AUDIO_SOURCE_OPTIONS[it].first }, onSelectedIndexChange = { audioSource = AUDIO_SOURCE_OPTIONS[it].first },
) )
if (audioSourcePreset == "custom") { if (audioSource == "custom") {
TextField( TextField(
value = audioSourceCustom, value = audioSource,
onValueChange = { audioSourceCustom = it }, onValueChange = { audioSource = it },
label = "--audio-source", label = "--audio-source",
singleLine = true, singleLine = true,
modifier = Modifier modifier = Modifier
@@ -381,8 +407,8 @@ internal fun AdvancedConfigPage(
SuperSwitch( SuperSwitch(
title = "仅转发不播放", title = "仅转发不播放",
summary = "--no-audio-playback", summary = "--no-audio-playback",
checked = noAudioPlayback, checked = !audioPlayback,
onCheckedChange = { noAudioPlayback = it }, onCheckedChange = { audioPlayback = !it },
) )
SuperSwitch( SuperSwitch(
title = "音频失败时终止 [TODO]", title = "音频失败时终止 [TODO]",
@@ -402,8 +428,7 @@ internal fun AdvancedConfigPage(
value = maxSizePresetIndex.toFloat(), value = maxSizePresetIndex.toFloat(),
onValueChange = { value -> onValueChange = { value ->
val idx = value.roundToInt().coerceIn(0, ScrcpyPresets.MaxSize.lastIndex) val idx = value.roundToInt().coerceIn(0, ScrcpyPresets.MaxSize.lastIndex)
val preset = ScrcpyPresets.MaxSize[idx] maxSize = ScrcpyPresets.MaxSize[idx]
maxSizeInput = if (preset == 0) "" else preset.toString()
}, },
valueRange = 0f..ScrcpyPresets.MaxSize.lastIndex.toFloat(), valueRange = 0f..ScrcpyPresets.MaxSize.lastIndex.toFloat(),
steps = (ScrcpyPresets.MaxSize.size - 2).coerceAtLeast(0), steps = (ScrcpyPresets.MaxSize.size - 2).coerceAtLeast(0),
@@ -412,12 +437,12 @@ internal fun AdvancedConfigPage(
showUnitWhenZeroState = false, showUnitWhenZeroState = false,
showKeyPoints = true, showKeyPoints = true,
keyPoints = ScrcpyPresets.MaxSize.indices.map { it.toFloat() }, keyPoints = ScrcpyPresets.MaxSize.indices.map { it.toFloat() },
displayText = maxSizeInput, displayText = maxSize.toString(),
inputHint = "0 或留空表示关闭", inputHint = "0 或留空表示关闭",
inputInitialValue = maxSizeInput, inputInitialValue = maxSize.toString(),
inputFilter = { it.filter(Char::isDigit) }, inputFilter = { it.filter(Char::isDigit) },
inputValueRange = 0f..Float.MAX_VALUE, inputValueRange = 0f..Float.MAX_VALUE,
onInputConfirm = { maxSizeInput = it.ifBlank { "" } }, onInputConfirm = { maxSize = it.toInt() },
) )
SuperSlide( SuperSlide(
title = "最大帧率", title = "最大帧率",
@@ -425,8 +450,7 @@ internal fun AdvancedConfigPage(
value = maxFpsPresetIndex.toFloat(), value = maxFpsPresetIndex.toFloat(),
onValueChange = { value -> onValueChange = { value ->
val idx = value.roundToInt().coerceIn(0, ScrcpyPresets.MaxFPS.lastIndex) val idx = value.roundToInt().coerceIn(0, ScrcpyPresets.MaxFPS.lastIndex)
val preset = ScrcpyPresets.MaxFPS[idx] maxFps = ScrcpyPresets.MaxFPS[idx].toString()
maxFpsInput = if (preset == 0) "" else preset.toString()
}, },
valueRange = 0f..ScrcpyPresets.MaxFPS.lastIndex.toFloat(), valueRange = 0f..ScrcpyPresets.MaxFPS.lastIndex.toFloat(),
steps = (ScrcpyPresets.MaxFPS.size - 2).coerceAtLeast(0), steps = (ScrcpyPresets.MaxFPS.size - 2).coerceAtLeast(0),
@@ -435,12 +459,12 @@ internal fun AdvancedConfigPage(
showUnitWhenZeroState = false, showUnitWhenZeroState = false,
showKeyPoints = true, showKeyPoints = true,
keyPoints = ScrcpyPresets.MaxFPS.indices.map { it.toFloat() }, keyPoints = ScrcpyPresets.MaxFPS.indices.map { it.toFloat() },
displayText = maxFpsInput, displayText = maxFps,
inputHint = "0 或留空表示关闭", inputHint = "0 或留空表示关闭",
inputInitialValue = maxFpsInput, inputInitialValue = maxFps,
inputFilter = { it.filter(Char::isDigit) }, inputFilter = { it.filter(Char::isDigit) },
inputValueRange = 0f..Float.MAX_VALUE, inputValueRange = 0f..Float.MAX_VALUE,
onInputConfirm = { maxFpsInput = it.ifBlank { "" } }, onInputConfirm = { maxFps = it },
) )
} }
} }
@@ -457,7 +481,7 @@ internal fun AdvancedConfigPage(
summary = "--video-encoder", summary = "--video-encoder",
items = videoEncoderEntries, items = videoEncoderEntries,
selectedIndex = videoEncoderIndex, selectedIndex = videoEncoderIndex,
onSelectedIndexChange = { videoEncoder = it }, onSelectedIndexChange = { videoEncoder = videoEncoderEntries[it].title ?: "" },
) )
TextField( TextField(
value = videoCodecOptions, value = videoCodecOptions,
@@ -474,7 +498,7 @@ internal fun AdvancedConfigPage(
summary = "--audio-encoder", summary = "--audio-encoder",
items = audioEncoderEntries, items = audioEncoderEntries,
selectedIndex = audioEncoderIndex, selectedIndex = audioEncoderIndex,
onSelectedIndexChange = { audioEncoder = it }, onSelectedIndexChange = { audioEncoder = audioEncoderEntries[it].title ?: "" },
) )
TextField( TextField(
value = audioCodecOptions, value = audioCodecOptions,
@@ -510,7 +534,7 @@ internal fun AdvancedConfigPage(
) { ) {
TextField( TextField(
value = newDisplayWidth, value = newDisplayWidth,
onValueChange = { newDisplayWidth = it }, onValueChange = { newDisplayWidth = it; updateNewDisplay() },
label = "width", label = "width",
singleLine = true, singleLine = true,
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
@@ -524,7 +548,7 @@ internal fun AdvancedConfigPage(
) )
TextField( TextField(
value = newDisplayHeight, value = newDisplayHeight,
onValueChange = { newDisplayHeight = it }, onValueChange = { newDisplayHeight = it; updateNewDisplay() },
label = "height", label = "height",
singleLine = true, singleLine = true,
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
@@ -538,7 +562,7 @@ internal fun AdvancedConfigPage(
) )
TextField( TextField(
value = newDisplayDpi, value = newDisplayDpi,
onValueChange = { newDisplayDpi = it }, onValueChange = { newDisplayDpi = it; updateNewDisplay() },
label = "dpi", label = "dpi",
singleLine = true, singleLine = true,
keyboardOptions = KeyboardOptions( 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 { private fun presetIndexFromInputForAdvancedPage(raw: String, presets: List<Int>): Int {
if (raw.isBlank()) return 0 if (raw.isBlank()) return 0
val value = raw.toIntOrNull() ?: 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.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue 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.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade.ScrcpySessionInfo 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.constants.UiSpacing
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics
import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut 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.nativecore.NativeAdbService
import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy 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
import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent
import io.github.miuzarte.scrcpyforandroid.services.fetchConnectedDeviceInfo 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.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.services.upsertQuickDevice
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings 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.storage.ScrcpyOptions
import io.github.miuzarte.scrcpyforandroid.widgets.ConfigPanel import io.github.miuzarte.scrcpyforandroid.widgets.ConfigPanel
import io.github.miuzarte.scrcpyforandroid.widgets.DeviceEditorScreen 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_CONNECT_TIMEOUT_MS = 3_000L
private const val ADB_KEEPALIVE_INTERVAL_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_KEEPALIVE_TIMEOUT_MS = 1_500L
private const val ADB_AUTO_RECONNECT_DISCOVER_TIMEOUT_MS = 1_200L private const val ADB_AUTO_RECONNECT_DISCOVER_TIMEOUT_MS = 2_000L
private const val ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS = 1_500L private const val ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS = 2_000L
private const val ADB_TCP_PROBE_TIMEOUT_MS = 600 private const val ADB_TCP_PROBE_TIMEOUT_MS = 500
private const val DEVICE_SHORTCUT_SEPARATOR = "\u001F" private const val DEVICE_SHORTCUT_SEPARATOR = "\u001F"
private val DeviceShortcutStateListSaver = private val DeviceShortcutsSaver =
listSaver<SnapshotStateList<DeviceShortcut>, String>( listSaver<DeviceShortcuts, String>(
save = { list -> save = { shortcuts ->
list.map { item -> // 将 DeviceShortcuts 中的每个设备转换为一行字符串
shortcuts.devices.map { device ->
listOf( listOf(
item.id, device.id,
item.name, device.name,
item.host, device.host,
item.port.toString(), device.port.toString(),
if (item.online) "1" else "0", if (device.online) "1" else "0"
).joinToString(DEVICE_SHORTCUT_SEPARATOR) ).joinToString(DEVICE_SHORTCUT_SEPARATOR)
} }
}, },
restore = { saved -> restore = { saved ->
saved.mapNotNull { line -> // 从保存的字符串列表恢复 DeviceShortcuts
DeviceShortcuts(saved.mapNotNull { line ->
val parts = line.split(DEVICE_SHORTCUT_SEPARATOR) val parts = line.split(DEVICE_SHORTCUT_SEPARATOR)
if (parts.size != 5) return@mapNotNull null if (parts.size != 5) return@mapNotNull null
val port = parts[3].toIntOrNull() ?: return@mapNotNull null val port = parts[3].toIntOrNull() ?: return@mapNotNull null
@@ -103,18 +99,12 @@ private val DeviceShortcutStateListSaver =
name = parts[1], name = parts[1],
host = parts[2], host = parts[2],
port = port, 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 @Composable
fun DeviceTabScreen( fun DeviceTabScreen(
contentPadding: PaddingValues, contentPadding: PaddingValues,
@@ -144,6 +134,7 @@ fun DeviceTabScreen(
val context = LocalContext.current val context = LocalContext.current
val appSettings = remember(context) { AppSettings(context) } val appSettings = remember(context) { AppSettings(context) }
val quickDevices = remember(context) { QuickDevices(context) }
val scrcpyOptions = remember(context) { ScrcpyOptions(context) } val scrcpyOptions = remember(context) { ScrcpyOptions(context) }
val haptics = rememberAppHaptics() val haptics = rememberAppHaptics()
@@ -152,7 +143,7 @@ fun DeviceTabScreen(
val virtualButtonLayout = remember(virtualButtonsLayout) { val virtualButtonLayout = remember(virtualButtonsLayout) {
VirtualButtonActions.splitLayout(VirtualButtonActions.parseStoredLayout(virtualButtonsLayout)) VirtualButtonActions.splitLayout(VirtualButtonActions.parseStoredLayout(virtualButtonsLayout))
} }
val initialSettings = remember(context) { loadDevicePageSettings(context) } // val initialSettings = remember(context) { loadDevicePageSettings(context) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val adbWorkerDispatcher = remember { val adbWorkerDispatcher = remember {
Executors.newSingleThreadExecutor { runnable -> Executors.newSingleThreadExecutor { runnable ->
@@ -173,7 +164,7 @@ fun DeviceTabScreen(
var statusLine by rememberSaveable { mutableStateOf("未连接") } var statusLine by rememberSaveable { mutableStateOf("未连接") }
var adbConnected by rememberSaveable { mutableStateOf(false) } var adbConnected by rememberSaveable { mutableStateOf(false) }
var currentTargetHost by rememberSaveable { mutableStateOf("") } 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 connectedDeviceLabel by rememberSaveable { mutableStateOf("未连接") }
var sessionInfoWidth by rememberSaveable { mutableIntStateOf(0) } var sessionInfoWidth by rememberSaveable { mutableIntStateOf(0) }
var sessionInfoHeight by rememberSaveable { mutableIntStateOf(0) } var sessionInfoHeight by rememberSaveable { mutableIntStateOf(0) }
@@ -203,33 +194,33 @@ fun DeviceTabScreen(
} }
} }
var previewControlsVisible by rememberSaveable { mutableStateOf(false) } 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 activeDeviceActionId by rememberSaveable { mutableStateOf<String?>(null) }
var showReorderSheet by rememberSaveable { mutableStateOf(false) } var showReorderSheet by rememberSaveable { mutableStateOf(false) }
var adbConnecting 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 audioForwardingSupported by rememberSaveable { mutableStateOf(true) }
var cameraMirroringSupported by rememberSaveable { mutableStateOf(true) } var cameraMirroringSupported by rememberSaveable { mutableStateOf(true) }
var videoBitRateMbps by rememberSaveable { mutableFloatStateOf(initialSettings.videoBitRateMbps) } val currentTarget =
var videoBitRateInput by rememberSaveable { mutableStateOf(initialSettings.videoBitRateInput) } if (currentTargetHost.isNotBlank())
var audioBitRateKbps by rememberSaveable { mutableIntStateOf(initialSettings.audioBitRateKbps) } ConnectionTarget(
val currentTarget = if (currentTargetHost.isNotBlank()) ConnectionTarget(
currentTargetHost, currentTargetHost,
currentTargetPort currentTargetPort
) else null ) else null
val quickDevices =
rememberSaveable(saver = DeviceShortcutStateListSaver) { mutableStateListOf() }
val sessionReconnectBlacklistHosts = remember { mutableSetOf<String>() } val sessionReconnectBlacklistHosts = remember { mutableSetOf<String>() }
LaunchedEffect(EventLogger.eventLog.size) { LaunchedEffect(EventLogger.eventLog.size) {
onCanClearLogsChange(EventLogger.hasLogs()) 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. * Disconnect the current ADB connection and stop any running scrcpy session.
* *
@@ -264,16 +255,16 @@ fun DeviceTabScreen(
} }
adbConnected = false adbConnected = false
currentTargetHost = "" currentTargetHost = ""
currentTargetPort = AppDefaults.ADB_PORT currentTargetPort = Defaults.ADB_PORT
audioForwardingSupported = true audioForwardingSupported = true
cameraMirroringSupported = true cameraMirroringSupported = true
sessionInfo = null sessionInfo = null
statusLine = "未连接" statusLine = "未连接"
connectedDeviceLabel = "未连接" connectedDeviceLabel = "未连接"
clearQuickOnlineForTarget?.let { target -> clearQuickOnlineForTarget?.let { target ->
if (target.host.isNotBlank()) { if (target.host.isNotBlank())
upsertQuickDevice(context, quickDevices, target.host, target.port, false) savedShortcuts =
} savedShortcuts.update(host = target.host, port = target.port, online = false)
} }
logMessage?.let { logEvent(it) } logMessage?.let { logEvent(it) }
if (!showSnackMessage.isNullOrBlank()) { if (!showSnackMessage.isNullOrBlank()) {
@@ -482,15 +473,15 @@ fun DeviceTabScreen(
if (audioEncoder.isNotBlank() && audioEncoder !in audioEncoderOptions) { if (audioEncoder.isNotBlank() && audioEncoder !in audioEncoderOptions) {
audioEncoder = "" 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()) { if (lists.videoEncoders.isEmpty() && lists.audioEncoders.isEmpty()) {
EventLogger.logEvent( logEvent(
"提示: 编码器为空,请检查 server 路径/版本与设备系统日志", "提示: 编码器为空,请检查 server 路径/版本与设备系统日志",
Log.WARN Log.WARN
) )
val preview = lists.rawOutput.lineSequence().take(20).joinToString(" | ") val preview = lists.rawOutput.lineSequence().take(20).joinToString(" | ")
if (preview.isNotBlank()) { if (preview.isNotBlank()) {
EventLogger.logEvent("编码器原始输出: $preview", Log.DEBUG) logEvent("编码器原始输出: $preview", Log.DEBUG)
} }
} }
}.onFailure { e -> }.onFailure { e ->
@@ -498,7 +489,7 @@ fun DeviceTabScreen(
onAudioEncoderOptionsChange(emptyList()) onAudioEncoderOptionsChange(emptyList())
onVideoEncoderTypeMapChange(emptyMap()) onVideoEncoderTypeMapChange(emptyMap())
onAudioEncoderTypeMapChange(emptyMap()) onAudioEncoderTypeMapChange(emptyMap())
EventLogger.logEvent( logEvent(
"读取编码器列表失败: ${e.message ?: e.javaClass.simpleName}", "读取编码器列表失败: ${e.message ?: e.javaClass.simpleName}",
Log.ERROR, Log.ERROR,
e e
@@ -518,16 +509,16 @@ fun DeviceTabScreen(
if (cameraSize.isNotBlank() && cameraSize != "custom" && cameraSize !in lists.sizes) { if (cameraSize.isNotBlank() && cameraSize != "custom" && cameraSize !in lists.sizes) {
cameraSize = "" cameraSize = ""
} }
EventLogger.logEvent("camera sizes 已刷新: count=${lists.sizes.size}") logEvent("camera sizes 已刷新: count=${lists.sizes.size}")
if (lists.sizes.isEmpty()) { if (lists.sizes.isEmpty()) {
val preview = lists.rawOutput.lineSequence().take(20).joinToString(" | ") val preview = lists.rawOutput.lineSequence().take(20).joinToString(" | ")
if (preview.isNotBlank()) { if (preview.isNotBlank()) {
EventLogger.logEvent("camera sizes 原始输出: $preview", Log.DEBUG) logEvent("camera sizes 原始输出: $preview", Log.DEBUG)
} }
} }
}.onFailure { e -> }.onFailure { e ->
onCameraSizeOptionsChange(emptyList()) onCameraSizeOptionsChange(emptyList())
EventLogger.logEvent( logEvent(
"读取 camera sizes 失败: ${e.message ?: e.javaClass.simpleName}", "读取 camera sizes 失败: ${e.message ?: e.javaClass.simpleName}",
Log.ERROR, Log.ERROR,
e e
@@ -548,9 +539,7 @@ fun DeviceTabScreen(
connectedDeviceLabel = info.model connectedDeviceLabel = info.model
applyConnectedDeviceCapabilities(info.sdkInt, info.androidRelease) applyConnectedDeviceCapabilities(info.sdkInt, info.androidRelease)
updateQuickDeviceNameIfEmpty(context, quickDevices, host, port, fullLabel) quickDevices.updateQuickDeviceNameIfEmpty(host, port, fullLabel)
connectHost = host
connectPort = port.toString()
statusLine = "$host:$port" 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}") 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() refreshCameraSizeLists()
} }
LaunchedEffect(videoBitRateInput) { LaunchedEffect(adbConnected, currentTargetHost, currentTargetPort, savedShortcuts.size) {
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) {
val activeId = if (adbConnected && currentTargetHost.isNotBlank()) { val activeId = if (adbConnected && currentTargetHost.isNotBlank()) {
"$currentTargetHost:$currentTargetPort" "$currentTargetHost:$currentTargetPort"
} else { } else {
null null
} }
for (index in quickDevices.indices) { for (index in savedShortcuts.indices) {
val item = quickDevices[index] val item = savedShortcuts[index]
val shouldOnline = activeId != null && item.id == activeId val shouldOnline = activeId != null && item.id == activeId
if (item.online != shouldOnline) { 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 continue
} }
val quickCandidates = quickDevices.toList() val quickCandidates = savedShortcuts.toList()
if (quickCandidates.isNotEmpty()) { if (quickCandidates.isNotEmpty()) {
for (target in quickCandidates) { for (target in quickCandidates) {
if (adbConnected || adbConnecting) break if (adbConnected || adbConnecting) break
@@ -653,12 +629,10 @@ fun DeviceTabScreen(
try { try {
runAutoAdbConnect(target.host, target.port) runAutoAdbConnect(target.host, target.port)
adbConnected = true adbConnected = true
upsertQuickDevice( savedShortcuts = savedShortcuts.update(
context, host = target.host,
quickDevices, port = target.port,
target.host, online = true
target.port,
true,
) )
handleAdbConnected(target.host, target.port) handleAdbConnected(target.host, target.port)
logEvent("ADB 快速探测连接成功: ${target.host}:${target.port}") logEvent("ADB 快速探测连接成功: ${target.host}:${target.port}")
@@ -686,24 +660,22 @@ fun DeviceTabScreen(
delay(ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS) delay(ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS)
continue continue
} }
val knownDevice = quickDevices.firstOrNull { it.host == discoveredHost } val knownDevice = savedShortcuts.firstOrNull { it.host == discoveredHost }
if (knownDevice == null) { if (knownDevice == null) {
delay(ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS) delay(ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS)
continue continue
} }
val portToReplace = quickDevices.firstOrNull { val portToReplace = savedShortcuts.firstOrNull {
it.host == discoveredHost && it.host == discoveredHost &&
it.port != AppDefaults.ADB_PORT && it.port != Defaults.ADB_PORT &&
it.port != discoveredPort it.port != discoveredPort
}?.port }?.port
if (portToReplace != null) { if (portToReplace != null) {
replaceQuickDevicePort( savedShortcuts = savedShortcuts.update(
context = context,
quickDevices = quickDevices,
host = discoveredHost, host = discoveredHost,
oldPort = portToReplace, port = portToReplace,
newPort = discoveredPort, newPort = discoveredPort,
online = false, online = false
) )
logEvent( logEvent(
"mDNS 发现新端口,已更新快速设备: $discoveredHost:$portToReplace -> $discoveredHost:$discoveredPort" "mDNS 发现新端口,已更新快速设备: $discoveredHost:$portToReplace -> $discoveredHost:$discoveredPort"
@@ -718,12 +690,10 @@ fun DeviceTabScreen(
try { try {
runAutoAdbConnect(discoveredHost, discoveredPort) runAutoAdbConnect(discoveredHost, discoveredPort)
adbConnected = true adbConnected = true
upsertQuickDevice( savedShortcuts = savedShortcuts.update(
context, host = discoveredHost,
quickDevices, port = discoveredPort,
discoveredHost, online = true
discoveredPort,
true
) )
handleAdbConnected(discoveredHost, discoveredPort) handleAdbConnected(discoveredHost, discoveredPort)
logEvent("ADB 自动重连成功: $discoveredHost:$discoveredPort") logEvent("ADB 自动重连成功: $discoveredHost:$discoveredPort")
@@ -790,8 +760,8 @@ fun DeviceTabScreen(
) { ) {
val list = remember { val list = remember {
ReorderableList( ReorderableList(
{ itemsProvider = {
quickDevices.map { device -> savedShortcuts.map { device ->
ReorderableList.Item( ReorderableList.Item(
id = device.id, id = device.id,
title = device.name.ifBlank { device.host }, title = device.name.ifBlank { device.host },
@@ -800,13 +770,7 @@ fun DeviceTabScreen(
} }
}, },
onSettle = { fromIndex, toIndex -> onSettle = { fromIndex, toIndex ->
if (fromIndex < 0) return@ReorderableList savedShortcuts = savedShortcuts.move(fromIndex, toIndex)
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)
}, },
) )
} }
@@ -822,34 +786,34 @@ fun DeviceTabScreen(
} }
} }
if (editingDeviceId != null) { if (editingDevice != null) {
val device = quickDevices.firstOrNull { it.id == editingDeviceId }
if (device != null) {
DeviceEditorScreen( DeviceEditorScreen(
contentPadding = contentPadding, contentPadding = contentPadding,
device = device, device = editingDevice!!,
onSave = { updated -> onSave = { updated ->
val idx = quickDevices.indexOfFirst { it.id == device.id } savedShortcuts = savedShortcuts.update(
if (idx >= 0) { id = editingDevice!!.id,
quickDevices[idx] = updated.copy(online = quickDevices[idx].online) host = updated.host,
saveQuickDevices(context, quickDevices) port = updated.port,
} online = updated.online,
editingDeviceId = null )
editingDevice = null
}, },
onDelete = { onDelete = {
quickDevices.removeAll { it.id == device.id } savedShortcuts = savedShortcuts.remove(id = editingDevice!!.id)
saveQuickDevices(context, quickDevices) editingDevice = null
editingDeviceId = null
}, },
onBack = { editingDeviceId = null }, onBack = { editingDevice = null },
) )
return return
} }
editingDeviceId = null
}
val devicePreviewCardHeightDp by appSettings.devicePreviewCardHeightDp.asState() val devicePreviewCardHeightDp by appSettings.devicePreviewCardHeightDp.asState()
val previewVirtualButtonShowText by appSettings.previewVirtualButtonShowText.asState() val previewVirtualButtonShowText by appSettings.previewVirtualButtonShowText.asState()
val audioBitRate by scrcpyOptions.audioBitRate.asState()
val videoBitRate by scrcpyOptions.videoBitRate.asState()
// 设备 // 设备
AppPageLazyColumn( AppPageLazyColumn(
contentPadding = contentPadding, 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 host = device.host
val port = device.port val port = device.port
val isConnectedTarget = adbConnected val isConnectedTarget = adbConnected
@@ -878,7 +842,7 @@ fun DeviceTabScreen(
actionText = if (!isConnectedTarget) "连接" else "断开", actionText = if (!isConnectedTarget) "连接" else "断开",
actionEnabled = !busy && !adbConnecting, actionEnabled = !busy && !adbConnecting,
actionInProgress = adbConnecting && activeDeviceActionId == device.id, actionInProgress = adbConnecting && activeDeviceActionId == device.id,
onLongPress = { editingDeviceId = device.id }, onLongPress = { editingDevice = device },
onContentClick = { onContentClick = {
scope.launch { scope.launch {
snack.showSnackbar("长按可编辑设备") snack.showSnackbar("长按可编辑设备")
@@ -893,7 +857,7 @@ fun DeviceTabScreen(
try { try {
connectWithTimeout(host, port) connectWithTimeout(host, port)
adbConnected = true adbConnected = true
upsertQuickDevice(context, quickDevices, host, port, true) savedShortcuts = savedShortcuts.update(host = host, port = port, online = true)
handleAdbConnected(host, port) handleAdbConnected(host, port)
} catch (e: Exception) { } catch (e: Exception) {
statusLine = "ADB 连接失败" statusLine = "ADB 连接失败"
@@ -926,13 +890,7 @@ fun DeviceTabScreen(
enabled = !adbConnecting, enabled = !adbConnecting,
onAddDevice = { onAddDevice = {
val target = parseQuickTarget(quickConnectInput) ?: return@QuickConnectCard val target = parseQuickTarget(quickConnectInput) ?: return@QuickConnectCard
upsertQuickDevice( savedShortcuts = savedShortcuts.update(host = target.host, port = target.port)
context,
quickDevices,
target.host,
target.port,
online = false
)
scope.launch { scope.launch {
snack.showSnackbar("已添加设备: ${target.host}:${target.port}") snack.showSnackbar("已添加设备: ${target.host}:${target.port}")
} }
@@ -944,7 +902,7 @@ fun DeviceTabScreen(
try { try {
connectWithTimeout(target.host, target.port) connectWithTimeout(target.host, target.port)
adbConnected = true 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) handleAdbConnected(target.host, target.port)
} catch (e: Exception) { } catch (e: Exception) {
statusLine = "ADB 连接失败" statusLine = "ADB 连接失败"
@@ -1006,13 +964,13 @@ fun DeviceTabScreen(
"off" "off"
} else { } else {
"${session.codec} ${session.width}x${session.height} " + "${session.codec} ${session.width}x${session.height} " +
"@${String.format("%.1f", videoBitRateMbps)}Mbps" "@${String.format("%.1f", videoBitRate / 1_000_000)}Mbps"
} }
val audioDetail = if (!audio) { val audioDetail = if (!audio) {
"off" "off"
} else { } else {
val playback = if (!options.audioPlayback) "(no-playback)" 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( logEvent(
"scrcpy 已启动: device=${session.deviceName}" + "scrcpy 已启动: device=${session.deviceName}" +
@@ -1085,24 +1043,3 @@ fun DeviceTabScreen(
item { Spacer(Modifier.height(UiSpacing.BottomContent)) } 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
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade.ScrcpySessionInfo import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade.ScrcpySessionInfo
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics 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.FullscreenControlScreen
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonBar import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonBar
@@ -38,9 +40,6 @@ data class FullscreenControlLaunch(
fun FullscreenControlPage( fun FullscreenControlPage(
launch: FullscreenControlLaunch, launch: FullscreenControlLaunch,
nativeCore: NativeCoreFacade, nativeCore: NativeCoreFacade,
virtualButtonsLayout: String,
showDebugInfo: Boolean,
showVirtualButtons: Boolean,
onVideoSizeChanged: (width: Int, height: Int) -> Unit, onVideoSizeChanged: (width: Int, height: Int) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
@@ -48,15 +47,25 @@ fun FullscreenControlPage(
BackHandler(enabled = true, onBack = onDismiss) BackHandler(enabled = true, onBack = onDismiss)
val context = LocalContext.current val context = LocalContext.current
val appSettings = remember(context) { AppSettings(context) }
val scrcpyOptions = remember(context) { ScrcpyOptions(context) }
val haptics = rememberAppHaptics() val haptics = rememberAppHaptics()
val activity = remember(context) { context as? Activity } 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)) 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( VirtualButtonBar(
outsideActions = virtualButtonLayout.first, outsideActions = buttonItems.first,
moreActions = virtualButtonLayout.second, moreActions = buttonItems.second,
) )
} }
var session by remember(launch) { var session by remember(launch) {
@@ -128,7 +137,7 @@ fun FullscreenControlPage(
session = session, session = session,
nativeCore = nativeCore, nativeCore = nativeCore,
onDismiss = onDismiss, onDismiss = onDismiss,
showDebugInfo = showDebugInfo, showDebugInfo = fullscreenDebugInfo,
currentFps = currentFps, currentFps = currentFps,
enableBackHandler = false, enableBackHandler = false,
onInjectTouch = { action, pointerId, x, y, pressure, buttons -> onInjectTouch = { action, pointerId, x, y, pressure, buttons ->
@@ -146,7 +155,7 @@ fun FullscreenControlPage(
}, },
) )
if (showVirtualButtons) { if (showFullscreenVirtualButtons) {
bar.Fullscreen( bar.Fullscreen(
modifier = Modifier.align(Alignment.BottomCenter), modifier = Modifier.align(Alignment.BottomCenter),
onAction = { action -> onAction = { action ->

View File

@@ -33,7 +33,6 @@ import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -47,7 +46,6 @@ import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberDecoratedNavEntries import androidx.navigation3.runtime.rememberDecoratedNavEntries
import androidx.navigation3.ui.NavDisplay import androidx.navigation3.ui.NavDisplay
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade 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.UiMotion
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
import io.github.miuzarte.scrcpyforandroid.nativecore.NativeAdbService import io.github.miuzarte.scrcpyforandroid.nativecore.NativeAdbService
@@ -95,245 +93,43 @@ private sealed interface RootScreen : NavKey {
@Composable @Composable
fun MainPage() { fun MainPage() {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope()
val activity = remember(context) { context as? Activity } val activity = remember(context) { context as? Activity }
val initialOrientation = remember(activity) { val initialOrientation = remember(activity) {
activity?.requestedOrientation ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED activity?.requestedOrientation ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
} }
DisposableEffect(activity) {
onDispose {
activity?.requestedOrientation = initialOrientation
}
}
val nativeCore = remember(context) { NativeCoreFacade.get(context.applicationContext) } val nativeCore = remember(context) { NativeCoreFacade.get(context.applicationContext) }
val adbService = remember(context) { NativeAdbService(context) } val adbService = remember(context) { NativeAdbService(context) }
val scrcpy = remember(context) { Scrcpy(context) } val scrcpy = remember(context) { Scrcpy(context) }
val snackHostState = remember { SnackbarHostState() } val snackHostState = remember { SnackbarHostState() }
val saveableStateHolder = rememberSaveableStateHolder()
val tabs = remember { MainTabDestination.entries } val tabs = remember { MainTabDestination.entries }
val pagerState = rememberPagerState( val pagerState = rememberPagerState(
initialPage = MainTabDestination.Device.ordinal, initialPage = MainTabDestination.Device.ordinal,
pageCount = { tabs.size }) pageCount = { tabs.size })
val currentTab = tabs[pagerState.currentPage] val currentTab = tabs[pagerState.currentPage]
val saveableStateHolder = rememberSaveableStateHolder()
val scope = rememberCoroutineScope()
val rootBackStack = remember { mutableStateListOf<NavKey>(RootScreen.Home) } val rootBackStack = remember { mutableStateListOf<NavKey>(RootScreen.Home) }
val currentRootScreen = rootBackStack.lastOrNull() as? RootScreen ?: RootScreen.Home val currentRootScreen = rootBackStack.lastOrNull() as? RootScreen ?: RootScreen.Home
val deviceScrollBehavior = val devicesPageScrollBehavior = MiuixScrollBehavior(
MiuixScrollBehavior(canScroll = { currentTab == MainTabDestination.Device }) canScroll = { currentTab == MainTabDestination.Device })
val settingsScrollBehavior = val settingsPageScrollBehavior = MiuixScrollBehavior(
MiuixScrollBehavior(canScroll = { currentTab == MainTabDestination.Settings }) canScroll = { currentTab == MainTabDestination.Settings })
val advancedScrollBehavior = MiuixScrollBehavior( val advancedPageScrollBehavior = MiuixScrollBehavior(
canScroll = { canScroll = {
currentRootScreen is RootScreen.Advanced || currentRootScreen is RootScreen.VirtualButtonOrder when (currentRootScreen) {
}, is RootScreen.Advanced -> true
) is RootScreen.VirtualButtonOrder -> true
val stringListSaver = listSaver<List<String>, String>( else -> false
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)
}
}
}
// 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() { fun popRoot() {
if (rootBackStack.size > 1) { if (rootBackStack.size > 1) {
@@ -345,6 +141,7 @@ fun MainPage() {
// 1) pop inner route // 1) pop inner route
// 2) switch tab back to Device // 2) switch tab back to Device
// 3) double-back to exit and disconnect adb/scrcpy // 3) double-back to exit and disconnect adb/scrcpy
var lastExitBackPressAtMs by rememberSaveable { mutableLongStateOf(0L) }
fun handleBackNavigation() { fun handleBackNavigation() {
if (rootBackStack.size > 1) { if (rootBackStack.size > 1) {
popRoot() 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() var customServerUri by appSettings.customServerUri.asMutableState()
val picker = rememberLauncherForActivityResult( val picker = rememberLauncherForActivityResult(
ActivityResultContracts.OpenDocument() ActivityResultContracts.OpenDocument()
@@ -477,7 +327,7 @@ fun MainPage() {
}, },
) )
}, },
scrollBehavior = deviceScrollBehavior, scrollBehavior = devicesPageScrollBehavior,
) )
}, },
) { pagePadding -> ) { pagePadding ->
@@ -487,15 +337,7 @@ fun MainPage() {
adbService = adbService, adbService = adbService,
scrcpy = scrcpy, scrcpy = scrcpy,
snack = snackHostState, snack = snackHostState,
scrollBehavior = deviceScrollBehavior, scrollBehavior = devicesPageScrollBehavior,
/*
onNoControlChange = {
noControl = it
if (it) {
turnScreenOff = false
}
},
*/
videoEncoderOptions = videoEncoderOptions, videoEncoderOptions = videoEncoderOptions,
onVideoEncoderOptionsChange = { onVideoEncoderOptionsChange = {
videoEncoderOptions.clear() videoEncoderOptions.clear()
@@ -555,7 +397,7 @@ fun MainPage() {
topBar = { topBar = {
TopAppBar( TopAppBar(
title = tab.title, title = tab.title,
scrollBehavior = settingsScrollBehavior, scrollBehavior = settingsPageScrollBehavior,
) )
}, },
) { pagePadding -> ) { pagePadding ->
@@ -576,7 +418,7 @@ fun MainPage() {
) )
) )
}, },
scrollBehavior = settingsScrollBehavior, scrollBehavior = settingsPageScrollBehavior,
) )
} }
} }
@@ -605,101 +447,32 @@ fun MainPage() {
) )
} }
}, },
scrollBehavior = advancedScrollBehavior, scrollBehavior = advancedPageScrollBehavior,
) )
}, },
snackbarHost = { SnackbarHost(snackHostState) }, snackbarHost = { SnackbarHost(snackHostState) },
) { pagePadding -> ) { pagePadding ->
AdvancedConfigPage( AdvancedConfigPage(
contentPadding = pagePadding, contentPadding = pagePadding,
scrollBehavior = advancedScrollBehavior, scrollBehavior = advancedPageScrollBehavior,
sessionStarted = sessionStarted,
snackbarHostState = snackHostState, 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("自定义"), cameraSizeDropdownItems = listOf("默认") + cameraSizeOptions + listOf("自定义"),
cameraSizeIndex = when (cameraSizePreset) { cameraSizeOptions = cameraSizeOptions,
"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 },
videoEncoderDropdownItems = videoEncoderDropdownItems, videoEncoderDropdownItems = videoEncoderDropdownItems,
videoEncoderTypeMap = videoEncoderTypeMap, videoEncoderTypeMap = videoEncoderTypeMap,
videoEncoderIndex = videoEncoderIndex, videoEncoderIndex = videoEncoderIndex,
onVideoEncoderChange = { videoEncoder = it },
videoCodecOptions = videoCodecOptions,
onVideoCodecOptionsChange = { videoCodecOptions = it },
audioEncoderDropdownItems = audioEncoderDropdownItems, audioEncoderDropdownItems = audioEncoderDropdownItems,
audioEncoderTypeMap = audioEncoderTypeMap, audioEncoderTypeMap = audioEncoderTypeMap,
audioEncoderIndex = audioEncoderIndex, audioEncoderIndex = audioEncoderIndex,
onAudioEncoderChange = { audioEncoder = it },
audioCodecOptions = audioCodecOptions,
onAudioCodecOptionsChange = { audioCodecOptions = it },
onRefreshEncoders = { refreshEncodersAction?.invoke() }, onRefreshEncoders = { refreshEncodersAction?.invoke() },
onRefreshCameraSizes = { refreshCameraSizesAction?.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) { entry(RootScreen.VirtualButtonOrder) {
Scaffold( Scaffold(
modifier = Modifier.nestedScroll(advancedScrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(advancedPageScrollBehavior.nestedScrollConnection),
topBar = { topBar = {
TopAppBar( TopAppBar(
title = "虚拟按钮排序", title = "虚拟按钮排序",
@@ -711,19 +484,13 @@ fun MainPage() {
) )
} }
}, },
scrollBehavior = advancedScrollBehavior, scrollBehavior = advancedPageScrollBehavior,
) )
}, },
) { pagePadding -> ) { pagePadding ->
VirtualButtonOrderPage( VirtualButtonOrderPage(
contentPadding = pagePadding, contentPadding = pagePadding,
scrollBehavior = advancedScrollBehavior, scrollBehavior = advancedPageScrollBehavior,
layoutString = virtualButtonsLayout,
onLayoutChange = { layout ->
virtualButtonsLayout = layout
},
showPreviewText = showPreviewVirtualButtonText,
onShowPreviewTextChange = { showPreviewVirtualButtonText = it },
) )
} }
} }
@@ -732,9 +499,6 @@ fun MainPage() {
FullscreenControlPage( FullscreenControlPage(
launch = screen.launch, launch = screen.launch,
nativeCore = nativeCore, nativeCore = nativeCore,
virtualButtonsLayout = virtualButtonsLayout,
showDebugInfo = fullscreenDebugInfoEnabled,
showVirtualButtons = showFullscreenVirtualButtons,
onVideoSizeChanged = { width, height -> onVideoSizeChanged = { width, height ->
fullscreenOrientation = if (width >= height) { fullscreenOrientation = if (width >= height) {
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE 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.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.core.net.toUri import androidx.core.net.toUri
import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlide import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlide
@@ -212,7 +211,7 @@ fun SettingsScreen(
TextField( TextField(
value = serverRemotePath, value = serverRemotePath,
onValueChange = { serverRemotePath = it }, onValueChange = { serverRemotePath = it },
label = AppDefaults.SERVER_REMOTE_PATH, label = AppSettings.SERVER_REMOTE_PATH.defaultValue,
useLabelAsPlaceholder = true, useLabelAsPlaceholder = true,
singleLine = true, singleLine = true,
modifier = Modifier modifier = Modifier
@@ -234,7 +233,7 @@ fun SettingsScreen(
TextField( TextField(
value = adbKeyName, value = adbKeyName,
onValueChange = { adbKeyName = it }, onValueChange = { adbKeyName = it },
label = AppDefaults.ADB_KEY_NAME, label = AppSettings.ADB_KEY_NAME.defaultValue,
useLabelAsPlaceholder = true, useLabelAsPlaceholder = true,
singleLine = true, singleLine = true,
modifier = Modifier modifier = Modifier

View File

@@ -6,7 +6,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn 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.ReorderableList
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions
@@ -18,17 +21,16 @@ import top.yukonga.miuix.kmp.extra.SuperSwitch
internal fun VirtualButtonOrderPage( internal fun VirtualButtonOrderPage(
contentPadding: PaddingValues, contentPadding: PaddingValues,
scrollBehavior: ScrollBehavior, scrollBehavior: ScrollBehavior,
layoutString: String,
onLayoutChange: (String) -> Unit,
showPreviewText: Boolean,
onShowPreviewTextChange: (Boolean) -> Unit,
) { ) {
var buttonItems by remember(layoutString) { val context = LocalContext.current
mutableStateOf(VirtualButtonActions.parseStoredLayout(layoutString))
}
fun emitChanges() { val appSettings = remember(context) { AppSettings(context) }
onLayoutChange(VirtualButtonActions.encodeStoredLayout(buttonItems)) 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( AppPageLazyColumn(
@@ -41,8 +43,8 @@ internal fun VirtualButtonOrderPage(
SuperSwitch( SuperSwitch(
title = "按钮显示文本", title = "按钮显示文本",
summary = "超过3个建议关闭只对预览卡下方的虚拟按钮生效", summary = "超过3个建议关闭只对预览卡下方的虚拟按钮生效",
checked = showPreviewText, checked = previewVirtualButtonShowText,
onCheckedChange = onShowPreviewTextChange, onCheckedChange = { previewVirtualButtonShowText = it },
) )
} }
} }
@@ -67,7 +69,7 @@ internal fun VirtualButtonOrderPage(
buttonItems = buttonItems.toMutableList().apply { buttonItems = buttonItems.toMutableList().apply {
add(toIndex, removeAt(fromIndex)) add(toIndex, removeAt(fromIndex))
} }
emitChanges() virtualButtonsLayout = VirtualButtonActions.encodeStoredLayout(buttonItems)
}, },
showCheckbox = true, showCheckbox = true,
onCheckboxChange = { id, checked -> onCheckboxChange = { id, checked ->
@@ -78,7 +80,7 @@ internal fun VirtualButtonOrderPage(
item item
} }
} }
emitChanges() virtualButtonsLayout = VirtualButtonActions.encodeStoredLayout(buttonItems)
}, },
)() )()
} }

View File

@@ -3,7 +3,7 @@ package io.github.miuzarte.scrcpyforandroid.services
import android.util.Log import android.util.Log
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.snapshots.SnapshotStateList 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.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@@ -36,8 +36,8 @@ object EventLogger {
_eventLog.add(0, "[$timestamp] $message") _eventLog.add(0, "[$timestamp] $message")
// Rotate logs if exceeds max size // Rotate logs if exceeds max size
if (_eventLog.size > AppDefaults.EVENT_LOG_LINES) { if (_eventLog.size > Defaults.EVENT_LOG_LINES) {
_eventLog.removeRange(AppDefaults.EVENT_LOG_LINES, _eventLog.size) _eventLog.removeRange(Defaults.EVENT_LOG_LINES, _eventLog.size)
} }
// Log to Android logcat // Log to Android logcat

View File

@@ -2,8 +2,8 @@ package io.github.miuzarte.scrcpyforandroid.services
import android.content.Context import android.content.Context
import androidx.core.content.edit 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.AppPreferenceKeys
import io.github.miuzarte.scrcpyforandroid.constants.Defaults
import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
@@ -21,7 +21,7 @@ internal fun loadQuickDevices(context: Context): List<DeviceShortcut> {
3 -> { 3 -> {
val name = parts[0].trim() val name = parts[0].trim()
val host = parts[1].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()) { if (host.isNotBlank()) {
result.add( result.add(
DeviceShortcut( DeviceShortcut(
@@ -38,8 +38,8 @@ internal fun loadQuickDevices(context: Context): List<DeviceShortcut> {
// Backward compatibility with old format: name|host:port // Backward compatibility with old format: name|host:port
val name = parts[0].trim() val name = parts[0].trim()
val host = parts[1].substringBefore(":").trim() val host = parts[1].substringBefore(":").trim()
val port = parts[1].substringAfter(":", AppDefaults.ADB_PORT.toString()).trim() val port = parts[1].substringAfter(":", Defaults.ADB_PORT.toString()).trim()
.toIntOrNull() ?: AppDefaults.ADB_PORT .toIntOrNull() ?: Defaults.ADB_PORT
if (host.isNotBlank()) { if (host.isNotBlank()) {
result.add( result.add(
DeviceShortcut( DeviceShortcut(
@@ -69,8 +69,8 @@ internal fun parseQuickTarget(raw: String): ConnectionTarget? {
if (value.isEmpty()) return null if (value.isEmpty()) return null
val host = value.substringBefore(':').trim() val host = value.substringBefore(':').trim()
if (host.isEmpty()) return null if (host.isEmpty()) return null
val port = value.substringAfter(':', AppDefaults.ADB_PORT.toString()).trim().toIntOrNull() val port = value.substringAfter(':', Defaults.ADB_PORT.toString()).trim().toIntOrNull()
?: AppDefaults.ADB_PORT ?: Defaults.ADB_PORT
return ConnectionTarget(host, port) return ConnectionTarget(host, port)
} }

View File

@@ -7,59 +7,59 @@ import androidx.datastore.preferences.core.stringPreferencesKey
class AppSettings(context: Context) : Settings(context, "AppSettings") { class AppSettings(context: Context) : Settings(context, "AppSettings") {
companion object { companion object {
private val THEME_BASE_INDEX = Pair( val THEME_BASE_INDEX = Pair(
intPreferencesKey("theme_base_index"), intPreferencesKey("theme_base_index"),
0 0
) )
private val MONET = Pair( val MONET = Pair(
booleanPreferencesKey("monet"), booleanPreferencesKey("monet"),
false false
) )
private val FULLSCREEN_DEBUG_INFO = Pair( val FULLSCREEN_DEBUG_INFO = Pair(
booleanPreferencesKey("fullscreen_debug_info"), booleanPreferencesKey("fullscreen_debug_info"),
false false
) )
private val SHOW_FULLSCREEN_VIRTUAL_BUTTONS = Pair( val SHOW_FULLSCREEN_VIRTUAL_BUTTONS = Pair(
booleanPreferencesKey("show_fullscreen_virtual_buttons"), booleanPreferencesKey("show_fullscreen_virtual_buttons"),
true true
) )
private val KEEP_SCREEN_ON_WHEN_STREAMING = Pair( val KEEP_SCREEN_ON_WHEN_STREAMING = Pair(
booleanPreferencesKey("keep_screen_on_when_streaming"), booleanPreferencesKey("keep_screen_on_when_streaming"),
false false
) )
private val DEVICE_PREVIEW_CARD_HEIGHT_DP = Pair( val DEVICE_PREVIEW_CARD_HEIGHT_DP = Pair(
intPreferencesKey("device_preview_card_height_dp"), intPreferencesKey("device_preview_card_height_dp"),
320 320
) )
private val PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT = Pair( val PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT = Pair(
booleanPreferencesKey("preview_virtual_button_show_text"), booleanPreferencesKey("preview_virtual_button_show_text"),
true true
) )
private val VIRTUAL_BUTTONS_LAYOUT = Pair( val VIRTUAL_BUTTONS_LAYOUT = Pair(
stringPreferencesKey("virtual_buttons_layout"), 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" "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"), stringPreferencesKey("custom_server_uri"),
"" ""
) )
private val SERVER_REMOTE_PATH = Pair( val SERVER_REMOTE_PATH = Pair(
stringPreferencesKey("server_remote_path"), stringPreferencesKey("server_remote_path"),
"/data/local/tmp/scrcpy-server.jar" "/data/local/tmp/scrcpy-server.jar"
) )
private val ADB_KEY_NAME = Pair( val ADB_KEY_NAME = Pair(
stringPreferencesKey("adb_key_name"), stringPreferencesKey("adb_key_name"),
"scrcpy" "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"), booleanPreferencesKey("adb_pairing_auto_discover_on_dialog_open"),
true true
) )
private val ADB_AUTO_RECONNECT_PAIRED_DEVICE = Pair( val ADB_AUTO_RECONNECT_PAIRED_DEVICE = Pair(
booleanPreferencesKey("adb_auto_reconnect_paired_device"), booleanPreferencesKey("adb_auto_reconnect_paired_device"),
true true
) )
private val ADB_MDNS_LAN_DISCOVERY = Pair( val ADB_MDNS_LAN_DISCOVERY = Pair(
booleanPreferencesKey("adb_mdns_lan_discovery"), booleanPreferencesKey("adb_mdns_lan_discovery"),
true true
) )
@@ -87,24 +87,29 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
val adbAutoReconnectPairedDevice by setting(ADB_AUTO_RECONNECT_PAIRED_DEVICE) val adbAutoReconnectPairedDevice by setting(ADB_AUTO_RECONNECT_PAIRED_DEVICE)
val adbMdnsLanDiscovery by setting(ADB_MDNS_LAN_DISCOVERY) val adbMdnsLanDiscovery by setting(ADB_MDNS_LAN_DISCOVERY)
override suspend fun toMap(): Map<String, Any> { override suspend fun toMap(): Map<String, Any> = mapOf(
return mapOf( // Theme Settings
THEME_BASE_INDEX.name to themeBaseIndex.get(), THEME_BASE_INDEX.name to themeBaseIndex.get(),
MONET.name to monet.get(), MONET.name to monet.get(),
// Scrcpy Settings
FULLSCREEN_DEBUG_INFO.name to fullscreenDebugInfo.get(), FULLSCREEN_DEBUG_INFO.name to fullscreenDebugInfo.get(),
SHOW_FULLSCREEN_VIRTUAL_BUTTONS.name to showFullscreenVirtualButtons.get(), SHOW_FULLSCREEN_VIRTUAL_BUTTONS.name to showFullscreenVirtualButtons.get(),
KEEP_SCREEN_ON_WHEN_STREAMING.name to keepScreenOnWhenStreaming.get(), KEEP_SCREEN_ON_WHEN_STREAMING.name to keepScreenOnWhenStreaming.get(),
DEVICE_PREVIEW_CARD_HEIGHT_DP.name to devicePreviewCardHeightDp.get(), DEVICE_PREVIEW_CARD_HEIGHT_DP.name to devicePreviewCardHeightDp.get(),
PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT.name to previewVirtualButtonShowText.get(), PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT.name to previewVirtualButtonShowText.get(),
VIRTUAL_BUTTONS_LAYOUT.name to virtualButtonsLayout.get(), VIRTUAL_BUTTONS_LAYOUT.name to virtualButtonsLayout.get(),
// Scrcpy Server Settings
CUSTOM_SERVER_URI.name to customServerUri.get(), CUSTOM_SERVER_URI.name to customServerUri.get(),
SERVER_REMOTE_PATH.name to serverRemotePath.get(), SERVER_REMOTE_PATH.name to serverRemotePath.get(),
// ADB Settings
ADB_KEY_NAME.name to adbKeyName.get(), ADB_KEY_NAME.name to adbKeyName.get(),
ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN.name to adbPairingAutoDiscoverOnDialogOpen.get(), ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN.name to adbPairingAutoDiscoverOnDialogOpen.get(),
ADB_AUTO_RECONNECT_PAIRED_DEVICE.name to adbAutoReconnectPairedDevice.get(), ADB_AUTO_RECONNECT_PAIRED_DEVICE.name to adbAutoReconnectPairedDevice.get(),
ADB_MDNS_LAN_DISCOVERY.name to adbMdnsLanDiscovery.get() ADB_MDNS_LAN_DISCOVERY.name to adbMdnsLanDiscovery.get()
) )
}
// TODO? // TODO?
override fun validate(): Boolean = true 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") { class QuickDevices(context: Context) : Settings(context, "QuickDevices") {
companion object { companion object {
private val QUICK_DEVICES = Pair( val QUICK_DEVICES_LIST = Pair(
stringPreferencesKey("quick_devices"), stringPreferencesKey("quick_devices_list"),
"", "",
) )
private val QUICK_CONNECT_INPUT = Pair( val QUICK_CONNECT_INPUT = Pair(
stringPreferencesKey("quick_connect_input"), stringPreferencesKey("quick_connect_input"),
"", "",
) )
} }
override suspend fun toMap(): Map<String, Any> { val quickDevicesList by setting(QUICK_DEVICES_LIST)
return mapOf( val quickConnectInput by setting(QUICK_CONNECT_INPUT)
QUICK_DEVICES.name to getQuickDevicesRaw(),
QUICK_CONNECT_INPUT.name to getQuickConnectInput(), 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 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> { suspend fun getQuickDevices(): List<DeviceShortcut> {
val raw = getQuickDevicesRaw() val raw = quickDevicesList.get()
if (raw.isBlank()) return emptyList() if (raw.isBlank()) return emptyList()
val result = mutableListOf<DeviceShortcut>() val result = mutableListOf<DeviceShortcut>()
@@ -42,10 +39,10 @@ class QuickDevices(context: Context) : Settings(context, "QuickDevices") {
return result return result
} }
suspend fun setQuickDevices(quickDevices: List<DeviceShortcut>) = suspend fun setQuickDevices(list: List<DeviceShortcut>) =
setQuickDevicesRaw(quickDevices.joinToString("\n") { it.marshalToString() }) quickDevicesList.set(list.joinToString("\n") { it.marshalToString() })
fun observeQuickDevices(): Flow<List<DeviceShortcut>> = observeQuickDevicesRaw() fun observeQuickDevices(): Flow<List<DeviceShortcut>> = quickDevicesList.observe()
.map { raw -> .map { raw ->
if (raw.isBlank()) return@map emptyList() if (raw.isBlank()) return@map emptyList()
@@ -162,10 +159,6 @@ class QuickDevices(context: Context) : Settings(context, "QuickDevices") {
* 清空所有快速设备 * 清空所有快速设备
*/ */
suspend fun clearQuickDevices() { 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") { class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
companion object { companion object {
private val CROP = Pair( val CROP = Pair(
stringPreferencesKey("crop"), stringPreferencesKey("crop"),
"" ""
) )
private val RECORD_FILENAME = Pair( val RECORD_FILENAME = Pair(
stringPreferencesKey("record_filename"), stringPreferencesKey("record_filename"),
"" ""
) )
private val VIDEO_CODEC_OPTIONS = Pair( val VIDEO_CODEC_OPTIONS = Pair(
stringPreferencesKey("video_codec_options"), stringPreferencesKey("video_codec_options"),
"" ""
) )
private val AUDIO_CODEC_OPTIONS = Pair( val AUDIO_CODEC_OPTIONS = Pair(
stringPreferencesKey("audio_codec_options"), stringPreferencesKey("audio_codec_options"),
"" ""
) )
private val VIDEO_ENCODER = Pair( val VIDEO_ENCODER = Pair(
stringPreferencesKey("video_encoder"), stringPreferencesKey("video_encoder"),
"" ""
) )
private val AUDIO_ENCODER = Pair( val AUDIO_ENCODER = Pair(
stringPreferencesKey("audio_encoder"), stringPreferencesKey("audio_encoder"),
"" ""
) )
private val CAMERA_ID = Pair( val CAMERA_ID = Pair(
stringPreferencesKey("camera_id"), stringPreferencesKey("camera_id"),
"" ""
) )
private val CAMERA_SIZE = Pair( val CAMERA_SIZE = Pair(
stringPreferencesKey("camera_size"), stringPreferencesKey("camera_size"),
"" ""
) )
private val CAMERA_AR = Pair( val CAMERA_AR = Pair(
stringPreferencesKey("camera_ar"), stringPreferencesKey("camera_ar"),
"" ""
) )
private val CAMERA_FPS = Pair( val CAMERA_FPS = Pair(
intPreferencesKey("camera_fps"), intPreferencesKey("camera_fps"),
0 0
) )
private val LOG_LEVEL = Pair( val LOG_LEVEL = Pair(
stringPreferencesKey("log_level"), stringPreferencesKey("log_level"),
"info" "info"
) )
private val VIDEO_CODEC = Pair( val VIDEO_CODEC = Pair(
stringPreferencesKey("video_codec"), stringPreferencesKey("video_codec"),
"h264" "h264"
) )
private val AUDIO_CODEC = Pair( val AUDIO_CODEC = Pair(
stringPreferencesKey("audio_codec"), stringPreferencesKey("audio_codec"),
"opus" "opus"
) )
private val VIDEO_SOURCE = Pair( val VIDEO_SOURCE = Pair(
stringPreferencesKey("video_source"), stringPreferencesKey("video_source"),
"display" "display"
) )
private val AUDIO_SOURCE = Pair( val AUDIO_SOURCE = Pair(
stringPreferencesKey("audio_source"), stringPreferencesKey("audio_source"),
"output" "output"
) )
private val RECORD_FORMAT = Pair( val RECORD_FORMAT = Pair(
stringPreferencesKey("record_format"), stringPreferencesKey("record_format"),
"auto" "auto"
) )
private val CAMERA_FACING = Pair( val CAMERA_FACING = Pair(
stringPreferencesKey("camera_facing"), stringPreferencesKey("camera_facing"),
"any" "any"
) )
private val MAX_SIZE = Pair( val MAX_SIZE = Pair(
intPreferencesKey("max_size"), intPreferencesKey("max_size"),
0 0
) )
private val VIDEO_BIT_RATE = Pair( val VIDEO_BIT_RATE = Pair(
intPreferencesKey("video_bit_rate"), intPreferencesKey("video_bit_rate"),
8000000 8000000
) )
private val AUDIO_BIT_RATE = Pair( val AUDIO_BIT_RATE = Pair(
intPreferencesKey("audio_bit_rate"), intPreferencesKey("audio_bit_rate"),
128000 128000
) )
private val MAX_FPS = Pair( val MAX_FPS = Pair(
stringPreferencesKey("max_fps"), stringPreferencesKey("max_fps"),
"" ""
) )
private val ANGLE = Pair( val ANGLE = Pair(
stringPreferencesKey("angle"), stringPreferencesKey("angle"),
"" ""
) )
private val CAPTURE_ORIENTATION = Pair( val CAPTURE_ORIENTATION = Pair(
intPreferencesKey("capture_orientation"), intPreferencesKey("capture_orientation"),
0 0
) )
private val CAPTURE_ORIENTATION_LOCK = Pair( val CAPTURE_ORIENTATION_LOCK = Pair(
stringPreferencesKey("capture_orientation_lock"), stringPreferencesKey("capture_orientation_lock"),
"unlocked" "unlocked"
) )
private val DISPLAY_ORIENTATION = Pair( val DISPLAY_ORIENTATION = Pair(
intPreferencesKey("display_orientation"), intPreferencesKey("display_orientation"),
0 0
) )
private val RECORD_ORIENTATION = Pair( val RECORD_ORIENTATION = Pair(
intPreferencesKey("record_orientation"), intPreferencesKey("record_orientation"),
0 0
) )
private val DISPLAY_IME_POLICY = Pair( val DISPLAY_IME_POLICY = Pair(
stringPreferencesKey("display_ime_policy"), stringPreferencesKey("display_ime_policy"),
"undefined" "undefined"
) )
private val DISPLAY_ID = Pair( val DISPLAY_ID = Pair(
intPreferencesKey("display_id"), intPreferencesKey("display_id"),
0 0
) )
private val SCREEN_OFF_TIMEOUT = Pair( val SCREEN_OFF_TIMEOUT = Pair(
longPreferencesKey("screen_off_timeout"), longPreferencesKey("screen_off_timeout"),
-1 -1
) )
private val SHOW_TOUCHES = Pair( val SHOW_TOUCHES = Pair(
booleanPreferencesKey("show_touches"), booleanPreferencesKey("show_touches"),
false false
) )
private val FULLSCREEN = Pair( val FULLSCREEN = Pair(
booleanPreferencesKey("fullscreen"), booleanPreferencesKey("fullscreen"),
false false
) )
private val CONTROL = Pair( val CONTROL = Pair(
booleanPreferencesKey("control"), booleanPreferencesKey("control"),
true true
) )
private val VIDEO_PLAYBACK = Pair( val VIDEO_PLAYBACK = Pair(
booleanPreferencesKey("video_playback"), booleanPreferencesKey("video_playback"),
true true
) )
private val AUDIO_PLAYBACK = Pair( val AUDIO_PLAYBACK = Pair(
booleanPreferencesKey("audio_playback"), booleanPreferencesKey("audio_playback"),
true true
) )
private val TURN_SCREEN_OFF = Pair( val TURN_SCREEN_OFF = Pair(
booleanPreferencesKey("turn_screen_off"), booleanPreferencesKey("turn_screen_off"),
false false
) )
private val STAY_AWAKE = Pair( val STAY_AWAKE = Pair(
booleanPreferencesKey("stay_awake"), booleanPreferencesKey("stay_awake"),
false false
) )
private val DISABLE_SCREENSAVER = Pair( val DISABLE_SCREENSAVER = Pair(
booleanPreferencesKey("disable_screensaver"), booleanPreferencesKey("disable_screensaver"),
false false
) )
private val POWER_OFF_ON_CLOSE = Pair( val POWER_OFF_ON_CLOSE = Pair(
booleanPreferencesKey("power_off_on_close"), booleanPreferencesKey("power_off_on_close"),
false false
) )
private val CLEANUP = Pair( val CLEANUP = Pair(
booleanPreferencesKey("cleanup"), booleanPreferencesKey("cleanup"),
true true
) )
private val POWER_ON = Pair( val POWER_ON = Pair(
booleanPreferencesKey("power_on"), booleanPreferencesKey("power_on"),
true true
) )
private val VIDEO = Pair( val VIDEO = Pair(
booleanPreferencesKey("video"), booleanPreferencesKey("video"),
true true
) )
private val AUDIO = Pair( val AUDIO = Pair(
booleanPreferencesKey("audio"), booleanPreferencesKey("audio"),
true true
) )
private val REQUIRE_AUDIO = Pair( val REQUIRE_AUDIO = Pair(
booleanPreferencesKey("require_audio"), booleanPreferencesKey("require_audio"),
false false
) )
private val KILL_ADB_ON_CLOSE = Pair( val KILL_ADB_ON_CLOSE = Pair(
booleanPreferencesKey("kill_adb_on_close"), booleanPreferencesKey("kill_adb_on_close"),
false false
) )
private val CAMERA_HIGH_SPEED = Pair( val CAMERA_HIGH_SPEED = Pair(
booleanPreferencesKey("camera_high_speed"), booleanPreferencesKey("camera_high_speed"),
false false
) )
private val LIST = Pair( val LIST = Pair(
stringPreferencesKey("list"), stringPreferencesKey("list"),
"null" "null"
) )
private val AUDIO_DUP = Pair( val AUDIO_DUP = Pair(
booleanPreferencesKey("audio_dup"), booleanPreferencesKey("audio_dup"),
false false
) )
private val NEW_DISPLAY = Pair( val NEW_DISPLAY = Pair(
stringPreferencesKey("new_display"), stringPreferencesKey("new_display"),
"" ""
) )
private val START_APP = Pair( val START_APP = Pair(
stringPreferencesKey("start_app"), stringPreferencesKey("start_app"),
"" ""
) )
private val VD_DESTROY_CONTENT = Pair( val VD_DESTROY_CONTENT = Pair(
booleanPreferencesKey("vd_destroy_content"), booleanPreferencesKey("vd_destroy_content"),
true true
) )
private val VD_SYSTEM_DECORATIONS = Pair( val VD_SYSTEM_DECORATIONS = Pair(
booleanPreferencesKey("vd_system_decorations"), booleanPreferencesKey("vd_system_decorations"),
true true
) )
@@ -278,8 +278,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
val vdDestroyContent by setting(VD_DESTROY_CONTENT) val vdDestroyContent by setting(VD_DESTROY_CONTENT)
val vdSystemDecorations by setting(VD_SYSTEM_DECORATIONS) val vdSystemDecorations by setting(VD_SYSTEM_DECORATIONS)
override suspend fun toMap(): Map<String, Any> { override suspend fun toMap(): Map<String, Any> = mapOf(
return mapOf(
CROP.name to crop.get(), CROP.name to crop.get(),
RECORD_FILENAME.name to recordFilename.get(), RECORD_FILENAME.name to recordFilename.get(),
VIDEO_CODEC_OPTIONS.name to videoCodecOptions.get(), VIDEO_CODEC_OPTIONS.name to videoCodecOptions.get(),
@@ -332,7 +331,6 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
VD_DESTROY_CONTENT.name to vdDestroyContent.get(), VD_DESTROY_CONTENT.name to vdDestroyContent.get(),
VD_SYSTEM_DECORATIONS.name to vdSystemDecorations.get() VD_SYSTEM_DECORATIONS.name to vdSystemDecorations.get()
) )
}
override fun validate(): Boolean = runBlocking { override fun validate(): Boolean = runBlocking {
runCatching { runCatching {
@@ -342,8 +340,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
} }
// TODO: 处理空值 // TODO: 处理空值
suspend fun toClientOptions(): ClientOptions { suspend fun toClientOptions() = ClientOptions(
return ClientOptions(
crop = crop.get(), crop = crop.get(),
recordFilename = recordFilename.get(), recordFilename = recordFilename.get(),
videoCodecOptions = videoCodecOptions.get(), videoCodecOptions = videoCodecOptions.get(),
@@ -398,5 +395,4 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
vdDestroyContent = vdDestroyContent.get(), vdDestroyContent = vdDestroyContent.get(),
vdSystemDecorations = vdSystemDecorations.get() vdSystemDecorations = vdSystemDecorations.get()
) )
}
} }

View File

@@ -65,7 +65,7 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade.ScrcpySessionInfo 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.ScrcpyPresets
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics
@@ -1367,7 +1367,7 @@ internal fun DeviceEditorScreen(
TextButton( TextButton(
text = "保存", text = "保存",
onClick = { onClick = {
val p = port.toIntOrNull() ?: AppDefaults.ADB_PORT val p = port.toIntOrNull() ?: Defaults.ADB_PORT
val h = host.trim() val h = host.trim()
if (h.isNotBlank()) { if (h.isNotBlank()) {
onSave( onSave(

View File

@@ -31,10 +31,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp 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.UiAndroidKeycodes
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import top.yukonga.miuix.kmp.basic.Button import top.yukonga.miuix.kmp.basic.Button
import top.yukonga.miuix.kmp.basic.ButtonDefaults import top.yukonga.miuix.kmp.basic.ButtonDefaults
@@ -135,7 +135,7 @@ object VirtualButtonActions {
fun parseStoredLayout(raw: String): List<VirtualButtonItem> { fun parseStoredLayout(raw: String): List<VirtualButtonItem> {
if (raw.isBlank()) if (raw.isBlank())
return parseStoredLayout(AppDefaults.VIRTUAL_BUTTONS_LAYOUT) return parseStoredLayout(AppSettings.VIRTUAL_BUTTONS_LAYOUT.defaultValue)
return raw.split(',').mapNotNull { item -> return raw.split(',').mapNotNull { item ->
val parts = item.trim().split(':') val parts = item.trim().split(':')