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