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