refactor: new preferences impl with data store
This commit is contained in:
@@ -97,6 +97,7 @@ dependencies {
|
||||
implementation("org.bouncycastle:bcpkix-jdk18on:1.80")
|
||||
implementation("org.conscrypt:conscrypt-android:2.5.2")
|
||||
implementation("sh.calvin.reorderable:reorderable:3.0.0")
|
||||
implementation("androidx.datastore:datastore-preferences:1.2.1")
|
||||
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.github.miuzarte.scrcpyforandroid.constants
|
||||
|
||||
object ScrcpyPresets {
|
||||
val MaxSize = listOf(0, 720, 1080, 1280, 1600, 1920, 2160, 2560, 3200, 3840)
|
||||
val MaxSize = listOf(0, 720, 1080, 1280, 1600, 1920, 2160, 2560, 3200, 3840) // px
|
||||
val MaxFPS = listOf(0, 24, 30, 45, 60, 90, 120)
|
||||
val AudioBitRate = listOf(32, 64, 96, 128, 160, 192, 256, 320, 384, 512)
|
||||
val AudioBitRate = listOf(32, 64, 96, 128, 160, 192, 256, 320, 384, 512) // Kbps
|
||||
}
|
||||
|
||||
@@ -1,18 +1,75 @@
|
||||
package io.github.miuzarte.scrcpyforandroid.models
|
||||
|
||||
internal data class ConnectionTarget(
|
||||
val host: String,
|
||||
val port: Int,
|
||||
)
|
||||
import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults
|
||||
|
||||
/**
|
||||
* A compact shortcut entry for quick-connect lists shown in the UI.
|
||||
* `online` indicates whether the device was reachable when last probed.
|
||||
*/
|
||||
internal data class DeviceShortcut(
|
||||
val id: String,
|
||||
val name: String,
|
||||
data class DeviceShortcut(
|
||||
val name: String = "",
|
||||
val host: String,
|
||||
val port: Int,
|
||||
val online: Boolean,
|
||||
)
|
||||
val port: Int = AppDefaults.ADB_PORT,
|
||||
val online: Boolean = false,
|
||||
) {
|
||||
val id: String get() = "$host:$port"
|
||||
|
||||
fun marshalToString(
|
||||
separator: String = "|",
|
||||
): String = listOf(
|
||||
name.trim(), host.trim(), port.toString()
|
||||
).joinToString(
|
||||
separator = separator
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun unmarshalFrom(
|
||||
s: String,
|
||||
delimiter: String = "|",
|
||||
): DeviceShortcut? {
|
||||
val parts = s.split(delimiter, limit = 3)
|
||||
return when (parts.size) {
|
||||
3 -> {
|
||||
val name = parts[0].trim()
|
||||
val host = parts[1].trim()
|
||||
val port = parts[2].trim().toIntOrNull() ?: AppDefaults.ADB_PORT
|
||||
if (host.isNotBlank()) DeviceShortcut(
|
||||
name = name,
|
||||
host = host,
|
||||
port = port,
|
||||
)
|
||||
else null
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ConnectionTarget(
|
||||
val host: String,
|
||||
val port: Int = AppDefaults.ADB_PORT,
|
||||
) {
|
||||
fun marshalToString(): String = "$host:$port"
|
||||
|
||||
companion object {
|
||||
fun unmarshalFrom(s: String): ConnectionTarget? {
|
||||
val parts = s.split(":", limit = 2)
|
||||
return when (parts.size) {
|
||||
2 -> ConnectionTarget(
|
||||
host = parts[0].trim(),
|
||||
port = parts[1].trim().toIntOrNull() ?: AppDefaults.ADB_PORT,
|
||||
)
|
||||
|
||||
1 -> ConnectionTarget(
|
||||
host = parts[0].trim(),
|
||||
port = AppDefaults.ADB_PORT,
|
||||
)
|
||||
|
||||
0 -> ConnectionTarget(
|
||||
host = s.trim(),
|
||||
port = AppDefaults.ADB_PORT,
|
||||
)
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,13 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
@@ -22,6 +26,8 @@ import io.github.miuzarte.scrcpyforandroid.constants.ScrcpyPresets
|
||||
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn
|
||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlide
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions
|
||||
import kotlinx.coroutines.launch
|
||||
import top.yukonga.miuix.kmp.basic.Card
|
||||
import top.yukonga.miuix.kmp.basic.ScrollBehavior
|
||||
@@ -69,10 +75,12 @@ internal fun AdvancedConfigPage(
|
||||
contentPadding: PaddingValues,
|
||||
scrollBehavior: ScrollBehavior,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
sessionStarted: Boolean,
|
||||
audioEnabled: Boolean,
|
||||
noControl: Boolean,
|
||||
onNoControlChange: (Boolean) -> Unit,
|
||||
// sessionStarted: Boolean,
|
||||
|
||||
// audioEnabled: Boolean,
|
||||
|
||||
// noControl: Boolean,
|
||||
// onNoControlChange: (Boolean) -> Unit,
|
||||
audioDup: Boolean,
|
||||
onAudioDupChange: (Boolean) -> Unit,
|
||||
audioSourcePreset: String,
|
||||
@@ -109,6 +117,7 @@ internal fun AdvancedConfigPage(
|
||||
onMaxSizeInputChange: (String) -> Unit,
|
||||
maxFpsInput: String,
|
||||
onMaxFpsInputChange: (String) -> Unit,
|
||||
|
||||
videoEncoderDropdownItems: List<String>,
|
||||
videoEncoderTypeMap: Map<String, String>,
|
||||
videoEncoderIndex: Int,
|
||||
@@ -121,27 +130,28 @@ internal fun AdvancedConfigPage(
|
||||
onAudioEncoderChange: (String) -> Unit,
|
||||
audioCodecOptions: String,
|
||||
onAudioCodecOptionsChange: (String) -> Unit,
|
||||
|
||||
onRefreshEncoders: () -> Unit,
|
||||
onRefreshCameraSizes: () -> Unit,
|
||||
|
||||
newDisplayWidth: String,
|
||||
onNewDisplayWidthChange: (String) -> Unit,
|
||||
newDisplayHeight: String,
|
||||
onNewDisplayHeightChange: (String) -> Unit,
|
||||
newDisplayDpi: String,
|
||||
onNewDisplayDpiChange: (String) -> Unit,
|
||||
displayIdInput: String,
|
||||
onDisplayIdInputChange: (String) -> Unit,
|
||||
cropWidth: String,
|
||||
onCropWidthChange: (String) -> Unit,
|
||||
cropHeight: String,
|
||||
onCropHeightChange: (String) -> Unit,
|
||||
cropX: String,
|
||||
onCropXChange: (String) -> Unit,
|
||||
cropY: String,
|
||||
onCropYChange: (String) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val appSettings = remember(context) { AppSettings(context) }
|
||||
val scrcpyOptions = remember(context) { ScrcpyOptions(context) }
|
||||
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val maxSizePresetIndex =
|
||||
presetIndexFromInputForAdvancedPage(maxSizeInput, ScrcpyPresets.MaxSize)
|
||||
val maxFpsPresetIndex = presetIndexFromInputForAdvancedPage(maxFpsInput, ScrcpyPresets.MaxFPS)
|
||||
@@ -179,6 +189,10 @@ internal fun AdvancedConfigPage(
|
||||
}
|
||||
}
|
||||
|
||||
var turnScreenOff by scrcpyOptions.turnScreenOff.asMutableState()
|
||||
var control by scrcpyOptions.control.asMutableState()
|
||||
var video by scrcpyOptions.video.asMutableState()
|
||||
|
||||
// 高级参数
|
||||
AppPageLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
@@ -190,30 +204,27 @@ internal fun AdvancedConfigPage(
|
||||
title = "启动后关闭屏幕",
|
||||
summary = "--turn-screen-off",
|
||||
checked = turnScreenOff,
|
||||
onCheckedChange = { value ->
|
||||
onTurnScreenOffChange(value)
|
||||
if (value) scope.launch {
|
||||
onCheckedChange = {
|
||||
turnScreenOff = it
|
||||
if (it) scope.launch {
|
||||
// github.com/Genymobile/scrcpy/issues/3376
|
||||
// github.com/Genymobile/scrcpy/issues/4587
|
||||
// github.com/Genymobile/scrcpy/issues/5676
|
||||
snackbarHostState.showSnackbar("注意:大部分设备在关闭屏幕后刷新率会降低/减半")
|
||||
}
|
||||
},
|
||||
enabled = !sessionStarted && !noControl,
|
||||
)
|
||||
SuperSwitch(
|
||||
title = "禁用控制",
|
||||
summary = "--no-control",
|
||||
checked = noControl,
|
||||
onCheckedChange = onNoControlChange,
|
||||
enabled = !sessionStarted,
|
||||
checked = !control,
|
||||
onCheckedChange = { control = !it },
|
||||
)
|
||||
SuperSwitch(
|
||||
title = "禁用视频",
|
||||
summary = "--no-video",
|
||||
checked = noVideo,
|
||||
onCheckedChange = onNoVideoChange,
|
||||
enabled = !sessionStarted,
|
||||
checked = !video,
|
||||
onCheckedChange = { video = !it },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -225,15 +236,14 @@ internal fun AdvancedConfigPage(
|
||||
summary = "--video-source",
|
||||
items = videoSourceItems,
|
||||
selectedIndex = videoSourceIndex,
|
||||
onSelectedIndexChange = { index ->
|
||||
onVideoSourcePresetChange(VIDEO_SOURCE_OPTIONS[index].first)
|
||||
onSelectedIndexChange = {
|
||||
videoSourcePreset = VIDEO_SOURCE_OPTIONS[it].first
|
||||
},
|
||||
enabled = !sessionStarted,
|
||||
)
|
||||
if (videoSourcePreset == "display") {
|
||||
TextField(
|
||||
value = displayIdInput,
|
||||
onValueChange = onDisplayIdInputChange,
|
||||
onValueChange = { displayIdInput = it },
|
||||
label = "--display-id",
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
@@ -246,7 +256,7 @@ internal fun AdvancedConfigPage(
|
||||
if (videoSourcePreset == "camera") {
|
||||
TextField(
|
||||
value = cameraIdInput,
|
||||
onValueChange = onCameraIdInputChange,
|
||||
onValueChange = { cameraIdInput = it },
|
||||
label = "--camera-id",
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
@@ -258,17 +268,15 @@ internal fun AdvancedConfigPage(
|
||||
title = "重新获取 Camera Sizes",
|
||||
summary = "--list-camera-sizes",
|
||||
onClick = onRefreshCameraSizes,
|
||||
enabled = !sessionStarted,
|
||||
)
|
||||
SuperDropdown(
|
||||
title = "摄像头朝向",
|
||||
summary = "--camera-facing",
|
||||
items = cameraFacingItems,
|
||||
selectedIndex = cameraFacingIndex,
|
||||
onSelectedIndexChange = { index ->
|
||||
onCameraFacingPresetChange(CAMERA_FACING_OPTIONS[index].first)
|
||||
onSelectedIndexChange = {
|
||||
cameraFacingPreset = CAMERA_FACING_OPTIONS[it].first
|
||||
},
|
||||
enabled = !sessionStarted,
|
||||
)
|
||||
SuperDropdown(
|
||||
title = "摄像头分辨率",
|
||||
@@ -278,21 +286,18 @@ internal fun AdvancedConfigPage(
|
||||
0,
|
||||
(cameraSizeDropdownItems.size - 1).coerceAtLeast(0)
|
||||
),
|
||||
onSelectedIndexChange = { index ->
|
||||
onCameraSizePresetChange(
|
||||
when (index) {
|
||||
0 -> ""
|
||||
cameraSizeDropdownItems.lastIndex -> "custom"
|
||||
else -> cameraSizeDropdownItems[index]
|
||||
},
|
||||
)
|
||||
onSelectedIndexChange = {
|
||||
cameraSizePreset = when (it) {
|
||||
0 -> ""
|
||||
cameraSizeDropdownItems.lastIndex -> "custom"
|
||||
else -> cameraSizeDropdownItems[it]
|
||||
}
|
||||
},
|
||||
enabled = !sessionStarted,
|
||||
)
|
||||
if (cameraSizePreset == "custom") {
|
||||
TextField(
|
||||
value = cameraSizeCustom,
|
||||
onValueChange = onCameraSizeCustomChange,
|
||||
onValueChange = { cameraSizeCustom = it },
|
||||
label = "--camera-size",
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
@@ -303,7 +308,7 @@ internal fun AdvancedConfigPage(
|
||||
}
|
||||
TextField(
|
||||
value = cameraArInput,
|
||||
onValueChange = onCameraArInputChange,
|
||||
onValueChange = { cameraArInput = it },
|
||||
label = "--camera-ar",
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
@@ -318,11 +323,10 @@ internal fun AdvancedConfigPage(
|
||||
onValueChange = { value ->
|
||||
val idx = value.roundToInt().coerceIn(0, CAMERA_FPS_PRESETS.lastIndex)
|
||||
val preset = CAMERA_FPS_PRESETS[idx]
|
||||
onCameraFpsInputChange(if (preset == 0) "" else preset.toString())
|
||||
cameraFpsInput = if (preset == 0) "" else preset.toString()
|
||||
},
|
||||
valueRange = 0f..CAMERA_FPS_PRESETS.lastIndex.toFloat(),
|
||||
steps = (CAMERA_FPS_PRESETS.size - 2).coerceAtLeast(0),
|
||||
enabled = !sessionStarted,
|
||||
unit = "fps",
|
||||
zeroStateText = "默认",
|
||||
showUnitWhenZeroState = false,
|
||||
@@ -334,16 +338,14 @@ internal fun AdvancedConfigPage(
|
||||
inputFilter = { it.filter(Char::isDigit) },
|
||||
inputValueRange = 0f..Float.MAX_VALUE,
|
||||
onInputConfirm = {
|
||||
val normalized = it.ifBlank { "" }
|
||||
onCameraFpsInputChange(if (normalized == "0") "" else normalized)
|
||||
cameraFpsInput = if (normalized == "0") "" else normalized
|
||||
},
|
||||
)
|
||||
SuperSwitch(
|
||||
title = "高帧率模式",
|
||||
summary = "--camera-high-speed",
|
||||
checked = cameraHighSpeed,
|
||||
onCheckedChange = onCameraHighSpeedChange,
|
||||
enabled = !sessionStarted,
|
||||
onCheckedChange = { cameraHighSpeed = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -356,15 +358,12 @@ internal fun AdvancedConfigPage(
|
||||
summary = "--audio-source",
|
||||
items = audioSourceItems,
|
||||
selectedIndex = audioSourceIndex,
|
||||
onSelectedIndexChange = { index ->
|
||||
onAudioSourcePresetChange(AUDIO_SOURCE_OPTIONS[index].first)
|
||||
},
|
||||
enabled = !sessionStarted && audioEnabled,
|
||||
onSelectedIndexChange = { audioSourcePreset = AUDIO_SOURCE_OPTIONS[it].first },
|
||||
)
|
||||
if (audioSourcePreset == "custom") {
|
||||
TextField(
|
||||
value = audioSourceCustom,
|
||||
onValueChange = onAudioSourceCustomChange,
|
||||
onValueChange = { audioSourceCustom = it },
|
||||
label = "--audio-source",
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
@@ -377,21 +376,19 @@ internal fun AdvancedConfigPage(
|
||||
title = "音频双路输出",
|
||||
summary = "--audio-dup",
|
||||
checked = audioDup,
|
||||
onCheckedChange = onAudioDupChange,
|
||||
enabled = !sessionStarted && audioEnabled,
|
||||
onCheckedChange = { audioDup = it },
|
||||
)
|
||||
SuperSwitch(
|
||||
title = "仅转发不播放",
|
||||
summary = "--no-audio-playback",
|
||||
checked = noAudioPlayback,
|
||||
onCheckedChange = onNoAudioPlaybackChange,
|
||||
enabled = !sessionStarted && audioEnabled,
|
||||
onCheckedChange = { noAudioPlayback = it },
|
||||
)
|
||||
SuperSwitch(
|
||||
title = "音频失败时终止 [TODO]",
|
||||
summary = "--require-audio",
|
||||
checked = requireAudio,
|
||||
onCheckedChange = onRequireAudioChange,
|
||||
onCheckedChange = { requireAudio = it },
|
||||
enabled = false,
|
||||
)
|
||||
}
|
||||
@@ -406,11 +403,10 @@ internal fun AdvancedConfigPage(
|
||||
onValueChange = { value ->
|
||||
val idx = value.roundToInt().coerceIn(0, ScrcpyPresets.MaxSize.lastIndex)
|
||||
val preset = ScrcpyPresets.MaxSize[idx]
|
||||
onMaxSizeInputChange(if (preset == 0) "" else preset.toString())
|
||||
maxSizeInput = if (preset == 0) "" else preset.toString()
|
||||
},
|
||||
valueRange = 0f..ScrcpyPresets.MaxSize.lastIndex.toFloat(),
|
||||
steps = (ScrcpyPresets.MaxSize.size - 2).coerceAtLeast(0),
|
||||
enabled = !sessionStarted,
|
||||
unit = "px",
|
||||
zeroStateText = "关闭",
|
||||
showUnitWhenZeroState = false,
|
||||
@@ -421,10 +417,7 @@ internal fun AdvancedConfigPage(
|
||||
inputInitialValue = maxSizeInput,
|
||||
inputFilter = { it.filter(Char::isDigit) },
|
||||
inputValueRange = 0f..Float.MAX_VALUE,
|
||||
onInputConfirm = {
|
||||
val normalized = it.ifBlank { "" }
|
||||
onMaxSizeInputChange(normalized)
|
||||
},
|
||||
onInputConfirm = { maxSizeInput = it.ifBlank { "" } },
|
||||
)
|
||||
SuperSlide(
|
||||
title = "最大帧率",
|
||||
@@ -433,11 +426,10 @@ internal fun AdvancedConfigPage(
|
||||
onValueChange = { value ->
|
||||
val idx = value.roundToInt().coerceIn(0, ScrcpyPresets.MaxFPS.lastIndex)
|
||||
val preset = ScrcpyPresets.MaxFPS[idx]
|
||||
onMaxFpsInputChange(if (preset == 0) "" else preset.toString())
|
||||
maxFpsInput = if (preset == 0) "" else preset.toString()
|
||||
},
|
||||
valueRange = 0f..ScrcpyPresets.MaxFPS.lastIndex.toFloat(),
|
||||
steps = (ScrcpyPresets.MaxFPS.size - 2).coerceAtLeast(0),
|
||||
enabled = !sessionStarted,
|
||||
unit = "fps",
|
||||
zeroStateText = "关闭",
|
||||
showUnitWhenZeroState = false,
|
||||
@@ -448,10 +440,7 @@ internal fun AdvancedConfigPage(
|
||||
inputInitialValue = maxFpsInput,
|
||||
inputFilter = { it.filter(Char::isDigit) },
|
||||
inputValueRange = 0f..Float.MAX_VALUE,
|
||||
onInputConfirm = {
|
||||
val normalized = it.ifBlank { "" }
|
||||
onMaxFpsInputChange(normalized)
|
||||
},
|
||||
onInputConfirm = { maxFpsInput = it.ifBlank { "" } },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -462,21 +451,17 @@ internal fun AdvancedConfigPage(
|
||||
title = "重新获取编码器列表",
|
||||
summary = "--list-encoders",
|
||||
onClick = onRefreshEncoders,
|
||||
enabled = !sessionStarted,
|
||||
)
|
||||
SuperSpinner(
|
||||
title = "视频编码器",
|
||||
summary = "--video-encoder",
|
||||
items = videoEncoderEntries,
|
||||
selectedIndex = videoEncoderIndex,
|
||||
onSelectedIndexChange = { index ->
|
||||
onVideoEncoderChange(if (index == 0) "" else videoEncoderDropdownItems[index])
|
||||
},
|
||||
enabled = !sessionStarted,
|
||||
onSelectedIndexChange = { videoEncoder = it },
|
||||
)
|
||||
TextField(
|
||||
value = videoCodecOptions,
|
||||
onValueChange = onVideoCodecOptionsChange,
|
||||
onValueChange = { videoCodecOptions = it },
|
||||
label = "--video-codec-options",
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
@@ -489,14 +474,11 @@ internal fun AdvancedConfigPage(
|
||||
summary = "--audio-encoder",
|
||||
items = audioEncoderEntries,
|
||||
selectedIndex = audioEncoderIndex,
|
||||
onSelectedIndexChange = { index ->
|
||||
onAudioEncoderChange(if (index == 0) "" else audioEncoderDropdownItems[index])
|
||||
},
|
||||
enabled = !sessionStarted && audioEnabled,
|
||||
onSelectedIndexChange = { audioEncoder = it },
|
||||
)
|
||||
TextField(
|
||||
value = audioCodecOptions,
|
||||
onValueChange = onAudioCodecOptionsChange,
|
||||
onValueChange = { audioCodecOptions = it },
|
||||
label = "--audio-codec-options",
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
@@ -528,7 +510,7 @@ internal fun AdvancedConfigPage(
|
||||
) {
|
||||
TextField(
|
||||
value = newDisplayWidth,
|
||||
onValueChange = onNewDisplayWidthChange,
|
||||
onValueChange = { newDisplayWidth = it },
|
||||
label = "width",
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
@@ -542,7 +524,7 @@ internal fun AdvancedConfigPage(
|
||||
)
|
||||
TextField(
|
||||
value = newDisplayHeight,
|
||||
onValueChange = onNewDisplayHeightChange,
|
||||
onValueChange = { newDisplayHeight = it },
|
||||
label = "height",
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
@@ -556,7 +538,7 @@ internal fun AdvancedConfigPage(
|
||||
)
|
||||
TextField(
|
||||
value = newDisplayDpi,
|
||||
onValueChange = onNewDisplayDpiChange,
|
||||
onValueChange = { newDisplayDpi = it },
|
||||
label = "dpi",
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
@@ -597,7 +579,7 @@ internal fun AdvancedConfigPage(
|
||||
) {
|
||||
TextField(
|
||||
value = cropWidth,
|
||||
onValueChange = onCropWidthChange,
|
||||
onValueChange = { cropWidth = it },
|
||||
label = "width",
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
@@ -611,7 +593,7 @@ internal fun AdvancedConfigPage(
|
||||
)
|
||||
TextField(
|
||||
value = cropHeight,
|
||||
onValueChange = onCropHeightChange,
|
||||
onValueChange = { cropHeight = it },
|
||||
label = "height",
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
@@ -630,7 +612,7 @@ internal fun AdvancedConfigPage(
|
||||
) {
|
||||
TextField(
|
||||
value = cropX,
|
||||
onValueChange = onCropXChange,
|
||||
onValueChange = { cropX = it },
|
||||
label = "x",
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
@@ -644,7 +626,7 @@ internal fun AdvancedConfigPage(
|
||||
)
|
||||
TextField(
|
||||
value = cropY,
|
||||
onValueChange = onCropYChange,
|
||||
onValueChange = { cropY = it },
|
||||
label = "y",
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
|
||||
@@ -32,14 +32,8 @@ import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget
|
||||
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
|
||||
import io.github.miuzarte.scrcpyforandroid.nativecore.NativeAdbService
|
||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.AudioSource
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.CameraFacing
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.ListOptions
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.VideoSource
|
||||
import io.github.miuzarte.scrcpyforandroid.services.DevicePageSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.services.EventLogger
|
||||
import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent
|
||||
import io.github.miuzarte.scrcpyforandroid.services.fetchConnectedDeviceInfo
|
||||
@@ -47,10 +41,11 @@ import io.github.miuzarte.scrcpyforandroid.services.loadDevicePageSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.services.loadQuickDevices
|
||||
import io.github.miuzarte.scrcpyforandroid.services.parseQuickTarget
|
||||
import io.github.miuzarte.scrcpyforandroid.services.replaceQuickDevicePort
|
||||
import io.github.miuzarte.scrcpyforandroid.services.saveDevicePageSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.services.saveQuickDevices
|
||||
import io.github.miuzarte.scrcpyforandroid.services.updateQuickDeviceNameIfEmpty
|
||||
import io.github.miuzarte.scrcpyforandroid.services.upsertQuickDevice
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.ConfigPanel
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.DeviceEditorScreen
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.DeviceTile
|
||||
@@ -105,7 +100,6 @@ private val DeviceShortcutStateListSaver =
|
||||
if (parts.size != 5) return@mapNotNull null
|
||||
val port = parts[3].toIntOrNull() ?: return@mapNotNull null
|
||||
DeviceShortcut(
|
||||
id = parts[0],
|
||||
name = parts[1],
|
||||
host = parts[2],
|
||||
port = port,
|
||||
@@ -129,75 +123,7 @@ fun DeviceTabScreen(
|
||||
scrcpy: Scrcpy,
|
||||
snack: SnackbarHostState,
|
||||
scrollBehavior: ScrollBehavior,
|
||||
virtualButtonsLayout: String,
|
||||
showPreviewVirtualButtonText: Boolean,
|
||||
previewCardHeightDp: Int,
|
||||
themeBaseIndex: Int,
|
||||
videoCodec: String,
|
||||
onVideoCodecChange: (String) -> Unit,
|
||||
audioEnabled: Boolean,
|
||||
onAudioEnabledChange: (Boolean) -> Unit,
|
||||
audioCodec: String,
|
||||
onAudioCodecChange: (String) -> Unit,
|
||||
noControl: Boolean,
|
||||
onNoControlChange: (Boolean) -> Unit,
|
||||
videoEncoder: String,
|
||||
onVideoEncoderChange: (String) -> Unit,
|
||||
videoCodecOptions: String,
|
||||
onVideoCodecOptionsChange: (String) -> Unit,
|
||||
audioEncoder: String,
|
||||
onAudioEncoderChange: (String) -> Unit,
|
||||
audioCodecOptions: String,
|
||||
onAudioCodecOptionsChange: (String) -> 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,
|
||||
cameraArInput: String,
|
||||
onCameraArInputChange: (String) -> Unit,
|
||||
cameraFpsInput: String,
|
||||
onCameraFpsInputChange: (String) -> Unit,
|
||||
cameraHighSpeed: Boolean,
|
||||
onCameraHighSpeedChange: (Boolean) -> Unit,
|
||||
noAudioPlayback: Boolean,
|
||||
onNoAudioPlaybackChange: (Boolean) -> Unit,
|
||||
noVideo: Boolean,
|
||||
requireAudio: Boolean,
|
||||
onRequireAudioChange: (Boolean) -> Unit,
|
||||
turnScreenOff: Boolean,
|
||||
onTurnScreenOffChange: (Boolean) -> Unit,
|
||||
maxSizeInput: String,
|
||||
onMaxSizeInputChange: (String) -> Unit,
|
||||
maxFpsInput: String,
|
||||
onMaxFpsInputChange: (String) -> Unit,
|
||||
newDisplayWidth: String,
|
||||
onNewDisplayWidthChange: (String) -> Unit,
|
||||
newDisplayHeight: String,
|
||||
onNewDisplayHeightChange: (String) -> Unit,
|
||||
newDisplayDpi: String,
|
||||
onNewDisplayDpiChange: (String) -> Unit,
|
||||
displayIdInput: String,
|
||||
onDisplayIdInputChange: (String) -> Unit,
|
||||
cropWidth: String,
|
||||
onCropWidthChange: (String) -> Unit,
|
||||
cropHeight: String,
|
||||
onCropHeightChange: (String) -> Unit,
|
||||
cropX: String,
|
||||
onCropXChange: (String) -> Unit,
|
||||
cropY: String,
|
||||
onCropYChange: (String) -> Unit,
|
||||
|
||||
videoEncoderOptions: List<String>,
|
||||
onVideoEncoderOptionsChange: (List<String>) -> Unit,
|
||||
onVideoEncoderTypeMapChange: (Map<String, String>) -> Unit,
|
||||
@@ -214,12 +140,15 @@ fun DeviceTabScreen(
|
||||
onOpenReorderDevicesActionChange: ((() -> Unit)?) -> Unit,
|
||||
onOpenAdvancedPage: () -> Unit,
|
||||
onOpenFullscreenPage: (ScrcpySessionInfo) -> Unit,
|
||||
adbPairingAutoDiscoverOnDialogOpen: Boolean,
|
||||
adbAutoReconnectPairedDevice: Boolean,
|
||||
adbMdnsLanDiscoveryEnabled: Boolean,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val appSettings = remember(context) { AppSettings(context) }
|
||||
val scrcpyOptions = remember(context) { ScrcpyOptions(context) }
|
||||
|
||||
val haptics = rememberAppHaptics()
|
||||
|
||||
val virtualButtonsLayout by appSettings.virtualButtonsLayout.asState()
|
||||
val virtualButtonLayout = remember(virtualButtonsLayout) {
|
||||
VirtualButtonActions.splitLayout(VirtualButtonActions.parseStoredLayout(virtualButtonsLayout))
|
||||
}
|
||||
@@ -285,8 +214,8 @@ fun DeviceTabScreen(
|
||||
var audioForwardingSupported by rememberSaveable { mutableStateOf(true) }
|
||||
var cameraMirroringSupported by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
var bitRateMbps by rememberSaveable { mutableFloatStateOf(initialSettings.videoBitRateMbps) }
|
||||
var bitRateInput by rememberSaveable { mutableStateOf(initialSettings.videoBitRateInput) }
|
||||
var videoBitRateMbps by rememberSaveable { mutableFloatStateOf(initialSettings.videoBitRateMbps) }
|
||||
var videoBitRateInput by rememberSaveable { mutableStateOf(initialSettings.videoBitRateInput) }
|
||||
var audioBitRateKbps by rememberSaveable { mutableIntStateOf(initialSettings.audioBitRateKbps) }
|
||||
val currentTarget = if (currentTargetHost.isNotBlank()) ConnectionTarget(
|
||||
currentTargetHost,
|
||||
@@ -364,11 +293,14 @@ fun DeviceTabScreen(
|
||||
disconnectAdbConnection(clearQuickOnlineForTarget = current)
|
||||
}
|
||||
|
||||
var audio by scrcpyOptions.audio.asMutableState()
|
||||
var videoSource by scrcpyOptions.videoSource.asMutableState()
|
||||
|
||||
fun applyConnectedDeviceCapabilities(sdkInt: Int, release: String) {
|
||||
val audioSupported = sdkInt !in 0..<30
|
||||
audioForwardingSupported = audioSupported
|
||||
if (!audioSupported && audioEnabled) {
|
||||
onAudioEnabledChange(false)
|
||||
if (!audioSupported && audio) {
|
||||
audio = false
|
||||
logEvent(
|
||||
"设备 Android ${release.ifBlank { "?" }} (SDK $sdkInt) 不支持音频转发,已自动关闭",
|
||||
Log.WARN
|
||||
@@ -376,8 +308,8 @@ fun DeviceTabScreen(
|
||||
}
|
||||
val cameraSupported = sdkInt !in 0..<31
|
||||
cameraMirroringSupported = cameraSupported
|
||||
if (!cameraSupported && videoSourcePreset == "camera") {
|
||||
onVideoSourcePresetChange("display")
|
||||
if (!cameraSupported && videoSource == "camera") {
|
||||
videoSource = "display"
|
||||
logEvent(
|
||||
"设备 Android ${release.ifBlank { "?" }} (SDK $sdkInt) 不支持 camera mirroring,已切换为 display",
|
||||
Log.WARN
|
||||
@@ -531,6 +463,9 @@ fun DeviceTabScreen(
|
||||
}
|
||||
}
|
||||
|
||||
var videoEncoder by scrcpyOptions.videoEncoder.asMutableState()
|
||||
var audioEncoder by scrcpyOptions.audioEncoder.asMutableState()
|
||||
|
||||
suspend fun refreshEncoderLists() {
|
||||
if (!adbConnected) return
|
||||
runCatching {
|
||||
@@ -542,10 +477,10 @@ fun DeviceTabScreen(
|
||||
onVideoEncoderTypeMapChange(lists.videoEncoderTypes)
|
||||
onAudioEncoderTypeMapChange(lists.audioEncoderTypes)
|
||||
if (videoEncoder.isNotBlank() && videoEncoder !in videoEncoderOptions) {
|
||||
onVideoEncoderChange("")
|
||||
videoEncoder = ""
|
||||
}
|
||||
if (audioEncoder.isNotBlank() && audioEncoder !in audioEncoderOptions) {
|
||||
onAudioEncoderChange("")
|
||||
audioEncoder = ""
|
||||
}
|
||||
EventLogger.logEvent("编码器列表已刷新: video=${lists.videoEncoders.size} audio=${lists.audioEncoders.size}")
|
||||
if (lists.videoEncoders.isEmpty() && lists.audioEncoders.isEmpty()) {
|
||||
@@ -571,6 +506,8 @@ fun DeviceTabScreen(
|
||||
}
|
||||
}
|
||||
|
||||
var cameraSize by scrcpyOptions.cameraSize.asMutableState()
|
||||
|
||||
suspend fun refreshCameraSizeLists() {
|
||||
if (!adbConnected) return
|
||||
runCatching {
|
||||
@@ -578,8 +515,8 @@ fun DeviceTabScreen(
|
||||
}.onSuccess { result ->
|
||||
val lists = result as Scrcpy.ListResult.CameraSizes
|
||||
onCameraSizeOptionsChange(lists.sizes)
|
||||
if (cameraSizePreset.isNotBlank() && cameraSizePreset != "custom" && cameraSizePreset !in lists.sizes) {
|
||||
onCameraSizePresetChange("")
|
||||
if (cameraSize.isNotBlank() && cameraSize != "custom" && cameraSize !in lists.sizes) {
|
||||
cameraSize = ""
|
||||
}
|
||||
EventLogger.logEvent("camera sizes 已刷新: count=${lists.sizes.size}")
|
||||
if (lists.sizes.isEmpty()) {
|
||||
@@ -624,88 +561,11 @@ fun DeviceTabScreen(
|
||||
refreshCameraSizeLists()
|
||||
}
|
||||
|
||||
LaunchedEffect(bitRateInput) {
|
||||
val parsed = bitRateInput.toFloatOrNull() ?: return@LaunchedEffect
|
||||
bitRateMbps = parsed.coerceAtLeast(0.1f)
|
||||
LaunchedEffect(videoBitRateInput) {
|
||||
val parsed = videoBitRateInput.toFloatOrNull() ?: return@LaunchedEffect
|
||||
videoBitRateMbps = parsed.coerceAtLeast(0.1f)
|
||||
}
|
||||
|
||||
LaunchedEffect(
|
||||
quickConnectInput,
|
||||
audioBitRateKbps,
|
||||
bitRateMbps,
|
||||
bitRateInput,
|
||||
turnScreenOff,
|
||||
noControl,
|
||||
noVideo,
|
||||
videoSourcePreset,
|
||||
displayIdInput,
|
||||
cameraIdInput,
|
||||
cameraFacingPreset,
|
||||
cameraSizePreset,
|
||||
cameraSizeCustom,
|
||||
cameraArInput,
|
||||
cameraFpsInput,
|
||||
cameraHighSpeed,
|
||||
audioSourcePreset,
|
||||
audioSourceCustom,
|
||||
audioDup,
|
||||
noAudioPlayback,
|
||||
requireAudio,
|
||||
maxSizeInput,
|
||||
maxFpsInput,
|
||||
videoEncoder,
|
||||
videoCodecOptions,
|
||||
audioEncoder,
|
||||
audioCodecOptions,
|
||||
newDisplayWidth,
|
||||
newDisplayHeight,
|
||||
newDisplayDpi,
|
||||
cropWidth,
|
||||
cropHeight,
|
||||
cropX,
|
||||
cropY,
|
||||
) {
|
||||
saveDevicePageSettings(
|
||||
context,
|
||||
DevicePageSettings(
|
||||
quickConnectInput = quickConnectInput,
|
||||
audioBitRateKbps = audioBitRateKbps,
|
||||
audioBitRateInput = audioBitRateKbps.toString(),
|
||||
videoBitRateMbps = bitRateMbps,
|
||||
videoBitRateInput = bitRateInput,
|
||||
turnScreenOff = turnScreenOff,
|
||||
noControl = noControl,
|
||||
noVideo = noVideo,
|
||||
videoSourcePreset = videoSourcePreset,
|
||||
displayIdInput = displayIdInput,
|
||||
cameraIdInput = cameraIdInput,
|
||||
cameraFacingPreset = cameraFacingPreset,
|
||||
cameraSizePreset = cameraSizePreset,
|
||||
cameraSizeCustom = cameraSizeCustom,
|
||||
cameraAr = cameraArInput,
|
||||
cameraFps = cameraFpsInput,
|
||||
cameraHighSpeed = cameraHighSpeed,
|
||||
audioSourcePreset = audioSourcePreset,
|
||||
audioSourceCustom = audioSourceCustom,
|
||||
audioDup = audioDup,
|
||||
noAudioPlayback = noAudioPlayback,
|
||||
requireAudio = requireAudio,
|
||||
maxSizeInput = maxSizeInput,
|
||||
maxFpsInput = maxFpsInput,
|
||||
videoEncoder = videoEncoder,
|
||||
videoCodecOptions = videoCodecOptions,
|
||||
audioEncoder = audioEncoder,
|
||||
audioCodecOptions = audioCodecOptions,
|
||||
newDisplayWidth = newDisplayWidth,
|
||||
newDisplayHeight = newDisplayHeight,
|
||||
newDisplayDpi = newDisplayDpi,
|
||||
cropWidth = cropWidth,
|
||||
cropHeight = cropHeight,
|
||||
cropX = cropX,
|
||||
cropY = cropY,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (quickDevices.isEmpty()) {
|
||||
@@ -762,7 +622,10 @@ fun DeviceTabScreen(
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(adbConnected, adbAutoReconnectPairedDevice, adbMdnsLanDiscoveryEnabled) {
|
||||
val adbPairingAutoDiscoverOnDialogOpen by appSettings.adbPairingAutoDiscoverOnDialogOpen.asState()
|
||||
val adbAutoReconnectPairedDevice by appSettings.adbAutoReconnectPairedDevice.asState()
|
||||
val adbMdnsLanDiscovery by appSettings.adbMdnsLanDiscovery.asState()
|
||||
LaunchedEffect(adbConnected, adbAutoReconnectPairedDevice, adbMdnsLanDiscovery) {
|
||||
if (adbConnected || !adbAutoReconnectPairedDevice) return@LaunchedEffect
|
||||
|
||||
// Background auto reconnect pipeline:
|
||||
@@ -809,7 +672,7 @@ fun DeviceTabScreen(
|
||||
val discovered = withContext(Dispatchers.IO) {
|
||||
adbService.discoverConnectService(
|
||||
timeoutMs = ADB_AUTO_RECONNECT_DISCOVER_TIMEOUT_MS,
|
||||
includeLanDevices = adbMdnsLanDiscoveryEnabled,
|
||||
includeLanDevices = adbMdnsLanDiscovery,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -985,6 +848,8 @@ fun DeviceTabScreen(
|
||||
editingDeviceId = null
|
||||
}
|
||||
|
||||
val devicePreviewCardHeightDp by appSettings.devicePreviewCardHeightDp.asState()
|
||||
val previewVirtualButtonShowText by appSettings.previewVirtualButtonShowText.asState()
|
||||
// 设备
|
||||
AppPageLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
@@ -998,7 +863,6 @@ fun DeviceTabScreen(
|
||||
sessionInfo = sessionInfo,
|
||||
busyLabel = null,
|
||||
connectedDeviceLabel = connectedDeviceLabel,
|
||||
themeBaseIndex = themeBaseIndex,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1099,7 +963,7 @@ fun DeviceTabScreen(
|
||||
autoDiscoverOnDialogOpen = adbPairingAutoDiscoverOnDialogOpen,
|
||||
onDiscoverTarget = {
|
||||
adbService.discoverPairingService(
|
||||
includeLanDevices = adbMdnsLanDiscoveryEnabled,
|
||||
includeLanDevices = adbMdnsLanDiscovery,
|
||||
)
|
||||
},
|
||||
onPair = { host, port, code ->
|
||||
@@ -1128,139 +992,34 @@ fun DeviceTabScreen(
|
||||
item {
|
||||
ConfigPanel(
|
||||
busy = busy,
|
||||
bitRateMbps = bitRateMbps,
|
||||
onBitRateSliderChange = {
|
||||
bitRateMbps = it
|
||||
@SuppressLint("DefaultLocale")
|
||||
bitRateInput = String.format("%.1f", it)
|
||||
},
|
||||
onBitRateInputChange = { bitRateInput = it },
|
||||
audioBitRateKbps = audioBitRateKbps,
|
||||
onAudioBitRateChange = { audioBitRateKbps = it },
|
||||
videoCodec = videoCodec,
|
||||
onVideoCodecChange = onVideoCodecChange,
|
||||
audioEnabled = audioEnabled,
|
||||
onAudioEnabledChange = onAudioEnabledChange,
|
||||
audioForwardingSupported = audioForwardingSupported,
|
||||
audioCodec = audioCodec,
|
||||
onAudioCodecChange = onAudioCodecChange,
|
||||
onOpenAdvanced = onOpenAdvancedPage,
|
||||
onStartStopHaptic = { haptics.contextClick() },
|
||||
onStart = {
|
||||
runBusy("启动 scrcpy") {
|
||||
if (noVideo && !audioEnabled) {
|
||||
throw IllegalArgumentException("--no-video 需要同时启用音频")
|
||||
}
|
||||
if (audioEnabled && audioSourcePreset == "custom" && audioSourceCustom.isBlank()) {
|
||||
throw IllegalArgumentException("audio-source 选择自定义时不能为空")
|
||||
}
|
||||
val resolvedVideoSource = videoSourcePreset.trim().ifBlank { "display" }
|
||||
if (resolvedVideoSource == "camera" && !cameraMirroringSupported) {
|
||||
throw IllegalArgumentException("camera mirroring 需要 Android 12+ (SDK 31+)")
|
||||
}
|
||||
val resolvedCameraSize = when (cameraSizePreset) {
|
||||
"custom" -> cameraSizeCustom.trim()
|
||||
else -> cameraSizePreset.trim()
|
||||
}
|
||||
if (resolvedVideoSource == "camera" && cameraSizePreset == "custom" && resolvedCameraSize.isBlank()) {
|
||||
throw IllegalArgumentException("camera-size 选择自定义时不能为空")
|
||||
}
|
||||
val resolvedCameraId = cameraIdInput.trim()
|
||||
val resolvedCameraFacing = cameraFacingPreset.trim()
|
||||
if (resolvedVideoSource == "camera" && resolvedCameraId.isNotBlank() && resolvedCameraFacing.isNotBlank()) {
|
||||
throw IllegalArgumentException("camera-id 与 camera-facing 不能同时设置")
|
||||
}
|
||||
val resolvedCameraAr = cameraArInput.trim()
|
||||
val resolvedCameraFps =
|
||||
cameraFpsInput.filter(Char::isDigit).toIntOrNull() ?: 0
|
||||
if (resolvedVideoSource == "camera" && cameraHighSpeed && resolvedCameraFps <= 0) {
|
||||
throw IllegalArgumentException("启用 --camera-high-speed 时,--camera-fps 不能为 0")
|
||||
}
|
||||
val maxSize =
|
||||
maxSizeInput.filter(Char::isDigit).toIntOrNull()?.takeIf { it > 0 }
|
||||
?: 0
|
||||
val maxFps =
|
||||
maxFpsInput.filter(Char::isDigit).toIntOrNull()?.toFloat() ?: 0f
|
||||
if (resolvedVideoSource == "camera" && resolvedCameraSize.isNotBlank() && (maxSize > 0 || resolvedCameraAr.isNotBlank())) {
|
||||
throw IllegalArgumentException("显式 camera-size 时不能同时设置 --max-size 或 --camera-ar")
|
||||
}
|
||||
val bitRateBps = (bitRateMbps * 1_000_000).toInt()
|
||||
val audioBitRateBps = (audioBitRateKbps.coerceAtLeast(1)) * 1_000
|
||||
val resolvedAudioSource = when (audioSourcePreset) {
|
||||
"custom" -> audioSourceCustom.trim()
|
||||
else -> audioSourcePreset.trim()
|
||||
}
|
||||
val newDisplayArg = buildNewDisplayArg(
|
||||
newDisplayWidth.filter(Char::isDigit),
|
||||
newDisplayHeight.filter(Char::isDigit),
|
||||
newDisplayDpi.filter(Char::isDigit),
|
||||
)
|
||||
val displayId = displayIdInput.filter(Char::isDigit).toIntOrNull()
|
||||
?.takeIf { it > 0 }
|
||||
val crop = buildCropArg(
|
||||
cropWidth.filter(Char::isDigit),
|
||||
cropHeight.filter(Char::isDigit),
|
||||
cropX.filter(Char::isDigit),
|
||||
cropY.filter(Char::isDigit),
|
||||
)
|
||||
val effectiveTurnScreenOff = turnScreenOff && !noControl
|
||||
|
||||
val options = ClientOptions(
|
||||
crop = crop,
|
||||
videoCodecOptions = videoCodecOptions,
|
||||
audioCodecOptions = audioCodecOptions,
|
||||
videoEncoder = videoEncoder,
|
||||
audioEncoder = audioEncoder,
|
||||
cameraId = resolvedCameraId,
|
||||
cameraSize = resolvedCameraSize,
|
||||
cameraAr = resolvedCameraAr,
|
||||
cameraFps = resolvedCameraFps.toUShort(),
|
||||
videoCodec = Codec.fromString(videoCodec),
|
||||
audioCodec = Codec.fromString(audioCodec),
|
||||
videoSource = if (resolvedVideoSource == "camera") VideoSource.CAMERA else VideoSource.DISPLAY,
|
||||
audioSource = if (resolvedAudioSource.isNotBlank()) AudioSource.fromString(
|
||||
resolvedAudioSource
|
||||
) else AudioSource.AUTO,
|
||||
cameraFacing = if (resolvedCameraFacing.isNotBlank()) CameraFacing.fromString(
|
||||
resolvedCameraFacing
|
||||
) else CameraFacing.ANY,
|
||||
maxSize = maxSize.toUShort(),
|
||||
videoBitRate = bitRateBps.toUInt(),
|
||||
audioBitRate = audioBitRateBps.toUInt(),
|
||||
maxFps = if (maxFps > 0f) maxFps.toString() else "",
|
||||
displayId = (displayId ?: 0).toUInt(),
|
||||
control = !noControl,
|
||||
video = !noVideo,
|
||||
audio = audioEnabled,
|
||||
requireAudio = requireAudio,
|
||||
audioPlayback = !noAudioPlayback,
|
||||
turnScreenOff = effectiveTurnScreenOff,
|
||||
audioDup = audioDup,
|
||||
newDisplay = newDisplayArg,
|
||||
cameraHighSpeed = cameraHighSpeed,
|
||||
)
|
||||
|
||||
// Start scrcpy using Scrcpy class
|
||||
val session = scrcpy.start(
|
||||
options = options,
|
||||
)
|
||||
|
||||
val options = scrcpyOptions.toClientOptions()
|
||||
val session = scrcpy.start(options)
|
||||
sessionInfo = session
|
||||
statusLine = "scrcpy 运行中"
|
||||
@SuppressLint("DefaultLocale")
|
||||
val videoDetail = if (noVideo) {
|
||||
val videoDetail = if (!options.video) {
|
||||
"off"
|
||||
} else {
|
||||
"${session.codec} ${session.width}x${session.height} " +
|
||||
"@${String.format("%.1f", bitRateMbps)}Mbps"
|
||||
"@${String.format("%.1f", videoBitRateMbps)}Mbps"
|
||||
}
|
||||
val audioDetail = if (!audioEnabled) {
|
||||
val audioDetail = if (!audio) {
|
||||
"off"
|
||||
} else {
|
||||
val playback = if (noAudioPlayback) "(no-playback)" else ""
|
||||
"$audioCodec ${audioBitRateKbps}kbps source=${resolvedAudioSource.ifBlank { "default" }}$playback"
|
||||
val playback = if (!options.audioPlayback) "(no-playback)" else ""
|
||||
"${options.audioCodec} ${audioBitRateKbps}kbps source=${options.audioSource}$playback"
|
||||
}
|
||||
logEvent("scrcpy 已启动: device=${session.deviceName}, video=$videoDetail, audio=$audioDetail, control=${!noControl}, turnScreenOff=$effectiveTurnScreenOff, maxSize=${if (maxSize > 0) maxSize else "auto"}, maxFps=${if (maxFps > 0f) maxFps else "auto"}")
|
||||
logEvent(
|
||||
"scrcpy 已启动: device=${session.deviceName}" +
|
||||
", video=$videoDetail, audio=$audioDetail" +
|
||||
", control=${options.control}, turnScreenOff=${options.turnScreenOff}" +
|
||||
", maxSize=${options.maxSize}, maxFps=${options.maxFps}"
|
||||
)
|
||||
scope.launch {
|
||||
snack.showSnackbar("scrcpy 已启动")
|
||||
}
|
||||
@@ -1293,7 +1052,7 @@ fun DeviceTabScreen(
|
||||
PreviewCard(
|
||||
sessionInfo = sessionInfo,
|
||||
nativeCore = nativeCore,
|
||||
previewHeightDp = previewCardHeightDp.coerceAtLeast(120),
|
||||
previewHeightDp = devicePreviewCardHeightDp.coerceAtLeast(120),
|
||||
controlsVisible = previewControlsVisible,
|
||||
onTapped = {
|
||||
previewControlsVisible = !previewControlsVisible
|
||||
@@ -1302,15 +1061,15 @@ fun DeviceTabScreen(
|
||||
val info = sessionInfo ?: return@PreviewCard
|
||||
onOpenFullscreenPage(info)
|
||||
},
|
||||
onOpenFullscreenHaptic = { haptics.contextClick() },
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
VirtualButtonCard(
|
||||
busy = busy,
|
||||
outsideActions = virtualButtonLayout.first,
|
||||
moreActions = virtualButtonLayout.second,
|
||||
showText = showPreviewVirtualButtonText,
|
||||
showText = previewVirtualButtonShowText,
|
||||
onAction = ::sendVirtualButtonAction,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -52,10 +52,8 @@ import io.github.miuzarte.scrcpyforandroid.constants.UiMotion
|
||||
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
||||
import io.github.miuzarte.scrcpyforandroid.nativecore.NativeAdbService
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
|
||||
import io.github.miuzarte.scrcpyforandroid.services.MainSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.services.loadDevicePageSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.services.loadMainSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.services.saveMainSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -97,6 +95,7 @@ private sealed interface RootScreen : NavKey {
|
||||
@Composable
|
||||
fun MainPage() {
|
||||
val context = LocalContext.current
|
||||
|
||||
val activity = remember(context) { context as? Activity }
|
||||
val initialOrientation = remember(activity) {
|
||||
activity?.requestedOrientation ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
@@ -106,8 +105,6 @@ fun MainPage() {
|
||||
val adbService = remember(context) { NativeAdbService(context) }
|
||||
val scrcpy = remember(context) { Scrcpy(context) }
|
||||
|
||||
val initialSettings = remember(context) { loadMainSettings(context) }
|
||||
val initialDeviceSettings = remember(context) { loadDevicePageSettings(context) }
|
||||
val snackHostState = remember { SnackbarHostState() }
|
||||
val tabs = remember { MainTabDestination.entries }
|
||||
val pagerState = rememberPagerState(
|
||||
@@ -132,20 +129,49 @@ fun MainPage() {
|
||||
restore = { restored -> restored.toList() },
|
||||
)
|
||||
|
||||
var audioEnabled by rememberSaveable { mutableStateOf(initialSettings.audioEnabled) }
|
||||
var audioCodec by rememberSaveable { mutableStateOf(initialSettings.audioCodec) }
|
||||
var videoCodec by rememberSaveable { mutableStateOf(initialSettings.videoCodec) }
|
||||
var themeBaseIndex by rememberSaveable { mutableIntStateOf(initialSettings.themeBaseIndex) }
|
||||
var monetEnabled by rememberSaveable { mutableStateOf(initialSettings.monetEnabled) }
|
||||
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) }
|
||||
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)
|
||||
}
|
||||
@@ -155,36 +181,102 @@ fun MainPage() {
|
||||
var adbMdnsLanDiscoveryEnabled by rememberSaveable {
|
||||
mutableStateOf(initialSettings.adbMdnsLanDiscoveryEnabled)
|
||||
}
|
||||
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 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>() }
|
||||
@@ -201,7 +293,10 @@ fun MainPage() {
|
||||
var fullscreenOrientation by rememberSaveable {
|
||||
mutableIntStateOf(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
|
||||
}
|
||||
val themeMode = resolveThemeMode(themeBaseIndex, monetEnabled)
|
||||
|
||||
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.
|
||||
@@ -211,6 +306,7 @@ fun MainPage() {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -234,49 +330,7 @@ fun MainPage() {
|
||||
activity?.requestedOrientation = targetOrientation
|
||||
}
|
||||
|
||||
LaunchedEffect(
|
||||
audioEnabled,
|
||||
audioCodec,
|
||||
videoCodec,
|
||||
themeBaseIndex,
|
||||
monetEnabled,
|
||||
fullscreenDebugInfoEnabled,
|
||||
showFullscreenVirtualButtons,
|
||||
showPreviewVirtualButtonText,
|
||||
keepScreenOnWhenStreamingEnabled,
|
||||
devicePreviewCardHeightDp,
|
||||
virtualButtonsLayout,
|
||||
customServerUri,
|
||||
serverRemotePath,
|
||||
adbKeyName,
|
||||
adbPairingAutoDiscoverOnDialogOpen,
|
||||
adbAutoReconnectPairedDevice,
|
||||
adbMdnsLanDiscoveryEnabled,
|
||||
) {
|
||||
saveMainSettings(
|
||||
context,
|
||||
MainSettings(
|
||||
audioEnabled = audioEnabled,
|
||||
audioCodec = audioCodec,
|
||||
videoCodec = videoCodec,
|
||||
themeBaseIndex = themeBaseIndex,
|
||||
monetEnabled = monetEnabled,
|
||||
fullscreenDebugInfoEnabled = fullscreenDebugInfoEnabled,
|
||||
showFullscreenVirtualButtons = showFullscreenVirtualButtons,
|
||||
showPreviewVirtualButtonText = showPreviewVirtualButtonText,
|
||||
keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled,
|
||||
devicePreviewCardHeightDp = devicePreviewCardHeightDp,
|
||||
virtualButtonsLayout = virtualButtonsLayout,
|
||||
customServerUri = customServerUri,
|
||||
serverRemotePath = serverRemotePath,
|
||||
adbKeyName = adbKeyName,
|
||||
adbPairingAutoDiscoverOnDialogOpen = adbPairingAutoDiscoverOnDialogOpen,
|
||||
adbAutoReconnectPairedDevice = adbAutoReconnectPairedDevice,
|
||||
adbMdnsLanDiscoveryEnabled = adbMdnsLanDiscoveryEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val adbKeyName by appSettings.adbKeyName.asMutableState()
|
||||
LaunchedEffect(adbKeyName) {
|
||||
adbService.keyName = adbKeyName.ifBlank { AppDefaults.ADB_KEY_NAME }
|
||||
}
|
||||
@@ -340,17 +394,19 @@ fun MainPage() {
|
||||
}
|
||||
}
|
||||
|
||||
val picker =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? ->
|
||||
if (uri == null) return@rememberLauncherForActivityResult
|
||||
runCatching {
|
||||
context.contentResolver.takePersistableUriPermission(
|
||||
uri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
)
|
||||
}
|
||||
customServerUri = uri.toString()
|
||||
var customServerUri by appSettings.customServerUri.asMutableState()
|
||||
val picker = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.OpenDocument()
|
||||
) { uri: Uri? ->
|
||||
if (uri == null) return@rememberLauncherForActivityResult
|
||||
runCatching {
|
||||
context.contentResolver.takePersistableUriPermission(
|
||||
uri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
)
|
||||
}
|
||||
customServerUri = uri.toString()
|
||||
}
|
||||
|
||||
val rootEntryProvider = entryProvider<NavKey> {
|
||||
entry(RootScreen.Home) {
|
||||
@@ -432,80 +488,14 @@ fun MainPage() {
|
||||
scrcpy = scrcpy,
|
||||
snack = snackHostState,
|
||||
scrollBehavior = deviceScrollBehavior,
|
||||
virtualButtonsLayout = virtualButtonsLayout,
|
||||
showPreviewVirtualButtonText = showPreviewVirtualButtonText,
|
||||
previewCardHeightDp = devicePreviewCardHeightDp,
|
||||
themeBaseIndex = themeBaseIndex,
|
||||
videoCodec = videoCodec,
|
||||
onVideoCodecChange = { videoCodec = it },
|
||||
audioEnabled = audioEnabled,
|
||||
onAudioEnabledChange = { audioEnabled = it },
|
||||
audioCodec = audioCodec,
|
||||
onAudioCodecChange = { audioCodec = it },
|
||||
noControl = noControl,
|
||||
/*
|
||||
onNoControlChange = {
|
||||
noControl = it
|
||||
if (it) {
|
||||
turnScreenOff = false
|
||||
}
|
||||
},
|
||||
videoEncoder = videoEncoder,
|
||||
onVideoEncoderChange = { videoEncoder = it },
|
||||
videoCodecOptions = videoCodecOptions,
|
||||
onVideoCodecOptionsChange = { videoCodecOptions = it },
|
||||
audioEncoder = audioEncoder,
|
||||
onAudioEncoderChange = { audioEncoder = it },
|
||||
audioCodecOptions = audioCodecOptions,
|
||||
onAudioCodecOptionsChange = { audioCodecOptions = it },
|
||||
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 },
|
||||
cameraArInput = cameraArInput,
|
||||
onCameraArInputChange = { cameraArInput = it },
|
||||
cameraFpsInput = cameraFpsInput,
|
||||
onCameraFpsInputChange = { cameraFpsInput = it },
|
||||
cameraHighSpeed = cameraHighSpeed,
|
||||
onCameraHighSpeedChange = { cameraHighSpeed = it },
|
||||
noAudioPlayback = noAudioPlayback,
|
||||
onNoAudioPlaybackChange = { noAudioPlayback = it },
|
||||
noVideo = noVideo,
|
||||
requireAudio = requireAudio,
|
||||
onRequireAudioChange = { requireAudio = it },
|
||||
turnScreenOff = turnScreenOff,
|
||||
onTurnScreenOffChange = { turnScreenOff = it },
|
||||
maxSizeInput = maxSizeInput,
|
||||
onMaxSizeInputChange = { maxSizeInput = it },
|
||||
maxFpsInput = maxFpsInput,
|
||||
onMaxFpsInputChange = { maxFpsInput = it },
|
||||
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 },
|
||||
*/
|
||||
videoEncoderOptions = videoEncoderOptions,
|
||||
onVideoEncoderOptionsChange = {
|
||||
videoEncoderOptions.clear()
|
||||
@@ -558,9 +548,6 @@ fun MainPage() {
|
||||
),
|
||||
)
|
||||
},
|
||||
adbPairingAutoDiscoverOnDialogOpen = adbPairingAutoDiscoverOnDialogOpen,
|
||||
adbAutoReconnectPairedDevice = adbAutoReconnectPairedDevice,
|
||||
adbMdnsLanDiscoveryEnabled = adbMdnsLanDiscoveryEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -574,33 +561,12 @@ fun MainPage() {
|
||||
) { pagePadding ->
|
||||
SettingsScreen(
|
||||
contentPadding = pagePadding,
|
||||
themeBaseIndex = themeBaseIndex,
|
||||
onThemeBaseIndexChange = { themeBaseIndex = it },
|
||||
monetEnabled = monetEnabled,
|
||||
onMonetEnabledChange = { monetEnabled = it },
|
||||
fullscreenDebugInfoEnabled = fullscreenDebugInfoEnabled,
|
||||
onFullscreenDebugInfoEnabledChange = {
|
||||
fullscreenDebugInfoEnabled = it
|
||||
},
|
||||
keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled,
|
||||
onKeepScreenOnWhenStreamingEnabledChange = {
|
||||
keepScreenOnWhenStreamingEnabled = it
|
||||
},
|
||||
devicePreviewCardHeightDp = devicePreviewCardHeightDp,
|
||||
onDevicePreviewCardHeightDpChange = {
|
||||
devicePreviewCardHeightDp = it.coerceAtLeast(120)
|
||||
},
|
||||
onOpenReorderDevices = {
|
||||
openReorderDevicesAction?.invoke()
|
||||
},
|
||||
onOpenVirtualButtonOrder = {
|
||||
rootBackStack.add(RootScreen.VirtualButtonOrder)
|
||||
},
|
||||
showFullscreenVirtualButtons = showFullscreenVirtualButtons,
|
||||
onShowFullscreenVirtualButtonsChange = {
|
||||
showFullscreenVirtualButtons = it
|
||||
},
|
||||
customServerUri = customServerUri,
|
||||
onPickServer = {
|
||||
picker.launch(
|
||||
arrayOf(
|
||||
@@ -610,19 +576,6 @@ fun MainPage() {
|
||||
)
|
||||
)
|
||||
},
|
||||
onClearServer = { customServerUri = null },
|
||||
serverRemotePath = serverRemotePath,
|
||||
onServerRemotePathChange = { serverRemotePath = it },
|
||||
adbKeyName = adbKeyName,
|
||||
onAdbKeyNameChange = { adbKeyName = it },
|
||||
adbPairingAutoDiscoverOnDialogOpen = adbPairingAutoDiscoverOnDialogOpen,
|
||||
onAdbPairingAutoDiscoverOnDialogOpenChange = {
|
||||
adbPairingAutoDiscoverOnDialogOpen = it
|
||||
},
|
||||
adbAutoReconnectPairedDevice = adbAutoReconnectPairedDevice,
|
||||
onAdbAutoReconnectPairedDeviceChange = {
|
||||
adbAutoReconnectPairedDevice = it
|
||||
},
|
||||
scrollBehavior = settingsScrollBehavior,
|
||||
)
|
||||
}
|
||||
@@ -632,6 +585,8 @@ fun MainPage() {
|
||||
}
|
||||
}
|
||||
|
||||
val videoEncoder by scrcpyOptions.videoEncoder.asState()
|
||||
val audioEncoder by scrcpyOptions.audioEncoder.asState()
|
||||
entry(RootScreen.Advanced) {
|
||||
val videoEncoderDropdownItems = listOf("默认") + videoEncoderOptions
|
||||
val audioEncoderDropdownItems = listOf("默认") + audioEncoderOptions
|
||||
@@ -808,11 +763,11 @@ fun MainPage() {
|
||||
@Composable
|
||||
private fun DeviceMenuPopup(
|
||||
show: Boolean,
|
||||
canClearLogs: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
onReorderDevices: () -> Unit,
|
||||
onOpenVirtualButtonOrder: () -> Unit,
|
||||
onClearLogs: () -> Unit,
|
||||
canClearLogs: Boolean,
|
||||
) {
|
||||
SuperListPopup(
|
||||
show = show,
|
||||
|
||||
@@ -11,6 +11,9 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Clear
|
||||
import androidx.compose.material.icons.rounded.FileOpen
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -19,6 +22,8 @@ import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults
|
||||
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn
|
||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlide
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.SectionSmallTitle
|
||||
import top.yukonga.miuix.kmp.basic.Card
|
||||
import top.yukonga.miuix.kmp.basic.Icon
|
||||
@@ -43,11 +48,11 @@ private val THEME_BASE_OPTIONS = listOf(
|
||||
ThemeModeOption("深色", ColorSchemeMode.Dark),
|
||||
)
|
||||
|
||||
fun resolveThemeMode(baseIndex: Int, monetEnabled: Boolean): ColorSchemeMode {
|
||||
fun resolveThemeMode(baseIndex: Int, monet: Boolean): ColorSchemeMode {
|
||||
return when (baseIndex.coerceIn(0, 2)) {
|
||||
0 -> if (monetEnabled) ColorSchemeMode.MonetSystem else ColorSchemeMode.System
|
||||
1 -> if (monetEnabled) ColorSchemeMode.MonetLight else ColorSchemeMode.Light
|
||||
else -> if (monetEnabled) ColorSchemeMode.MonetDark else ColorSchemeMode.Dark
|
||||
0 -> if (monet) ColorSchemeMode.MonetSystem else ColorSchemeMode.System
|
||||
1 -> if (monet) ColorSchemeMode.MonetLight else ColorSchemeMode.Light
|
||||
else -> if (monet) ColorSchemeMode.MonetDark else ColorSchemeMode.Dark
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,36 +64,37 @@ private fun resolveThemeLabel(baseIndex: Int, monetEnabled: Boolean): String {
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
contentPadding: PaddingValues,
|
||||
themeBaseIndex: Int,
|
||||
onThemeBaseIndexChange: (Int) -> Unit,
|
||||
monetEnabled: Boolean,
|
||||
onMonetEnabledChange: (Boolean) -> Unit,
|
||||
fullscreenDebugInfoEnabled: Boolean,
|
||||
onFullscreenDebugInfoEnabledChange: (Boolean) -> Unit,
|
||||
keepScreenOnWhenStreamingEnabled: Boolean,
|
||||
onKeepScreenOnWhenStreamingEnabledChange: (Boolean) -> Unit,
|
||||
devicePreviewCardHeightDp: Int,
|
||||
onDevicePreviewCardHeightDpChange: (Int) -> Unit,
|
||||
showFullscreenVirtualButtons: Boolean,
|
||||
onOpenReorderDevices: () -> Unit,
|
||||
onOpenVirtualButtonOrder: () -> Unit,
|
||||
onShowFullscreenVirtualButtonsChange: (Boolean) -> Unit,
|
||||
customServerUri: String?,
|
||||
onPickServer: () -> Unit,
|
||||
onClearServer: () -> Unit,
|
||||
serverRemotePath: String,
|
||||
onServerRemotePathChange: (String) -> Unit,
|
||||
adbKeyName: String,
|
||||
onAdbKeyNameChange: (String) -> Unit,
|
||||
adbPairingAutoDiscoverOnDialogOpen: Boolean,
|
||||
onAdbPairingAutoDiscoverOnDialogOpenChange: (Boolean) -> Unit,
|
||||
adbAutoReconnectPairedDevice: Boolean,
|
||||
onAdbAutoReconnectPairedDeviceChange: (Boolean) -> Unit,
|
||||
scrollBehavior: ScrollBehavior,
|
||||
) {
|
||||
val baseModeItems = THEME_BASE_OPTIONS.map { it.label }
|
||||
val context = LocalContext.current
|
||||
|
||||
val appSettings = remember(context) { AppSettings(context) }
|
||||
val scrcpyOptions = remember(context) { ScrcpyOptions(context) }
|
||||
|
||||
val baseModeItems = THEME_BASE_OPTIONS.map { it.label }
|
||||
|
||||
// 主题
|
||||
var themeBaseIndex by appSettings.themeBaseIndex.asMutableState()
|
||||
var monet by appSettings.monet.asMutableState()
|
||||
|
||||
// 投屏
|
||||
var fullscreenDebugInfo by appSettings.fullscreenDebugInfo.asMutableState()
|
||||
var keepScreenOnWhenStreaming by appSettings.keepScreenOnWhenStreaming.asMutableState()
|
||||
var devicePreviewCardHeightDp by appSettings.devicePreviewCardHeightDp.asMutableState()
|
||||
var showFullscreenVirtualButtons by appSettings.showFullscreenVirtualButtons.asMutableState()
|
||||
|
||||
// scrcpy-server
|
||||
var customServerUri by appSettings.customServerUri.asMutableState()
|
||||
var serverRemotePath by appSettings.serverRemotePath.asMutableState()
|
||||
|
||||
// ADB
|
||||
var adbKeyName by appSettings.adbKeyName.asMutableState()
|
||||
var adbPairingAutoDiscoverOnDialogOpen by appSettings.adbPairingAutoDiscoverOnDialogOpen.asMutableState()
|
||||
var adbAutoReconnectPairedDevice by appSettings.adbAutoReconnectPairedDevice.asMutableState()
|
||||
|
||||
// 设置
|
||||
AppPageLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
@@ -99,16 +105,16 @@ fun SettingsScreen(
|
||||
Card {
|
||||
SuperDropdown(
|
||||
title = "外观模式",
|
||||
summary = resolveThemeLabel(themeBaseIndex, monetEnabled),
|
||||
summary = resolveThemeLabel(themeBaseIndex, monet),
|
||||
items = baseModeItems,
|
||||
selectedIndex = themeBaseIndex.coerceIn(0, baseModeItems.lastIndex),
|
||||
onSelectedIndexChange = onThemeBaseIndexChange,
|
||||
onSelectedIndexChange = { themeBaseIndex = it },
|
||||
)
|
||||
SuperSwitch(
|
||||
title = "Monet",
|
||||
summary = "开启后使用 Monet 动态配色",
|
||||
checked = monetEnabled,
|
||||
onCheckedChange = onMonetEnabledChange,
|
||||
checked = monet,
|
||||
onCheckedChange = { monet = it },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -117,23 +123,21 @@ fun SettingsScreen(
|
||||
SuperSwitch(
|
||||
title = "启用调试信息",
|
||||
summary = "在全屏界面显示触点数量、设备分辨率和实时 FPS",
|
||||
checked = fullscreenDebugInfoEnabled,
|
||||
onCheckedChange = onFullscreenDebugInfoEnabledChange,
|
||||
checked = fullscreenDebugInfo,
|
||||
onCheckedChange = { fullscreenDebugInfo = it },
|
||||
)
|
||||
SuperSwitch(
|
||||
title = "投屏时保持屏幕常亮",
|
||||
summary = "Scrcpy 启动后保持本机屏幕常亮,避免锁屏导致 ADB 断开",
|
||||
checked = keepScreenOnWhenStreamingEnabled,
|
||||
onCheckedChange = onKeepScreenOnWhenStreamingEnabledChange,
|
||||
checked = keepScreenOnWhenStreaming,
|
||||
onCheckedChange = { keepScreenOnWhenStreaming = it },
|
||||
)
|
||||
SuperSlide(
|
||||
title = "预览卡高度",
|
||||
summary = "设备页预览卡高度",
|
||||
value = devicePreviewCardHeightDp.toFloat(),
|
||||
onValueChange = {
|
||||
onDevicePreviewCardHeightDpChange(
|
||||
it.roundToInt().coerceAtLeast(120)
|
||||
)
|
||||
devicePreviewCardHeightDp = it.roundToInt().coerceAtLeast(120)
|
||||
},
|
||||
valueRange = 160f..600f,
|
||||
steps = 439,
|
||||
@@ -142,9 +146,10 @@ fun SettingsScreen(
|
||||
inputInitialValue = devicePreviewCardHeightDp.toString(),
|
||||
inputFilter = { it.filter(Char::isDigit) },
|
||||
inputValueRange = 120f..Float.MAX_VALUE,
|
||||
onInputConfirm = { raw ->
|
||||
raw.toIntOrNull()
|
||||
?.let { onDevicePreviewCardHeightDpChange(it.coerceAtLeast(120)) }
|
||||
onInputConfirm = { input ->
|
||||
input.toIntOrNull()?.let {
|
||||
devicePreviewCardHeightDp = it.coerceAtLeast(120)
|
||||
}
|
||||
},
|
||||
)
|
||||
SuperArrow(
|
||||
@@ -161,7 +166,7 @@ fun SettingsScreen(
|
||||
title = "全屏显示虚拟按钮",
|
||||
summary = "在全屏控制页底部显示返回键、主页键等虚拟按钮",
|
||||
checked = showFullscreenVirtualButtons,
|
||||
onCheckedChange = onShowFullscreenVirtualButtonsChange,
|
||||
onCheckedChange = { showFullscreenVirtualButtons = it },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -176,20 +181,21 @@ fun SettingsScreen(
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
TextField(
|
||||
value = customServerUri ?: "",
|
||||
value = customServerUri,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = "scrcpy-server-v3.3.4",
|
||||
useLabelAsPlaceholder = customServerUri == null,
|
||||
useLabelAsPlaceholder = customServerUri.isBlank(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = UiSpacing.CardContent)
|
||||
.padding(bottom = UiSpacing.CardContent),
|
||||
trailingIcon = {
|
||||
Row(modifier = Modifier.padding(end = UiSpacing.SectionTitleLeadingGap)) {
|
||||
if (customServerUri != null) IconButton(onClick = onClearServer) {
|
||||
Icon(Icons.Rounded.Clear, contentDescription = "清空")
|
||||
}
|
||||
if (customServerUri.isNotBlank())
|
||||
IconButton(onClick = { customServerUri = "" }) {
|
||||
Icon(Icons.Rounded.Clear, contentDescription = "清空")
|
||||
}
|
||||
IconButton(onClick = onPickServer) {
|
||||
Icon(Icons.Rounded.FileOpen, contentDescription = "选择文件")
|
||||
}
|
||||
@@ -205,7 +211,7 @@ fun SettingsScreen(
|
||||
)
|
||||
TextField(
|
||||
value = serverRemotePath,
|
||||
onValueChange = onServerRemotePathChange,
|
||||
onValueChange = { serverRemotePath = it },
|
||||
label = AppDefaults.SERVER_REMOTE_PATH,
|
||||
useLabelAsPlaceholder = true,
|
||||
singleLine = true,
|
||||
@@ -227,7 +233,7 @@ fun SettingsScreen(
|
||||
)
|
||||
TextField(
|
||||
value = adbKeyName,
|
||||
onValueChange = onAdbKeyNameChange,
|
||||
onValueChange = { adbKeyName = it },
|
||||
label = AppDefaults.ADB_KEY_NAME,
|
||||
useLabelAsPlaceholder = true,
|
||||
singleLine = true,
|
||||
@@ -240,13 +246,13 @@ fun SettingsScreen(
|
||||
title = "配对时自动启用发现服务",
|
||||
summary = "打开配对弹窗后自动搜索可用配对端口",
|
||||
checked = adbPairingAutoDiscoverOnDialogOpen,
|
||||
onCheckedChange = onAdbPairingAutoDiscoverOnDialogOpenChange,
|
||||
onCheckedChange = { adbPairingAutoDiscoverOnDialogOpen = it },
|
||||
)
|
||||
SuperSwitch(
|
||||
title = "自动重连已配对设备",
|
||||
summary = "自动发现开启无线调试的设备,更新快速设备的随机端口并尝试连接(效果比较随缘)",
|
||||
checked = adbAutoReconnectPairedDevice,
|
||||
onCheckedChange = onAdbAutoReconnectPairedDeviceChange,
|
||||
onCheckedChange = { adbAutoReconnectPairedDevice = it },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ class Shared {
|
||||
AAC("aac"),
|
||||
FLAC("flac"),
|
||||
RAW("raw"); // wav raw
|
||||
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String): Codec {
|
||||
return entries.find { it.string.equals(value, ignoreCase = true) } ?: H264
|
||||
@@ -79,7 +79,7 @@ class Shared {
|
||||
enum class VideoSource(val string: String) {
|
||||
DISPLAY("display"), // default, ignore when passing
|
||||
CAMERA("camera");
|
||||
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String): VideoSource {
|
||||
return entries.find { it.string.equals(value, ignoreCase = true) } ?: DISPLAY
|
||||
@@ -100,7 +100,7 @@ class Shared {
|
||||
VOICE_CALL_UPLINK("voice-call-uplink"),
|
||||
VOICE_CALL_DOWNLINK("voice-call-downlink"),
|
||||
VOICE_PERFORMANCE("voice-performance");
|
||||
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String): AudioSource {
|
||||
return entries.find { it.string.equals(value, ignoreCase = true) } ?: AUTO
|
||||
@@ -113,7 +113,7 @@ class Shared {
|
||||
FRONT("front"),
|
||||
BACK("back"),
|
||||
EXTERNAL("external");
|
||||
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String): CameraFacing {
|
||||
return entries.find { it.string.equals(value, ignoreCase = true) } ?: ANY
|
||||
@@ -142,6 +142,13 @@ class Shared {
|
||||
else -> error("Invalid rotation value")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromInt(value: Int): Orientation {
|
||||
return entries.getOrNull(value)
|
||||
?: error("Invalid orientation value: $value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class OrientationLock(val string: String) {
|
||||
|
||||
@@ -25,7 +25,6 @@ internal fun loadQuickDevices(context: Context): List<DeviceShortcut> {
|
||||
if (host.isNotBlank()) {
|
||||
result.add(
|
||||
DeviceShortcut(
|
||||
id = "$host:$port",
|
||||
name = name,
|
||||
host = host,
|
||||
port = port,
|
||||
@@ -44,7 +43,6 @@ internal fun loadQuickDevices(context: Context): List<DeviceShortcut> {
|
||||
if (host.isNotBlank()) {
|
||||
result.add(
|
||||
DeviceShortcut(
|
||||
id = "$host:$port",
|
||||
name = name,
|
||||
host = host,
|
||||
port = port,
|
||||
@@ -83,11 +81,9 @@ internal fun upsertQuickDevice(
|
||||
port: Int,
|
||||
online: Boolean,
|
||||
) {
|
||||
val id = "$host:$port"
|
||||
val idx = quickDevices.indexOfFirst { it.id == id }
|
||||
val idx = quickDevices.indexOfFirst { it.id == "$host:$port" }
|
||||
val existingName = if (idx >= 0) quickDevices[idx].name else ""
|
||||
val item = DeviceShortcut(
|
||||
id = id,
|
||||
name = existingName,
|
||||
host = host,
|
||||
port = port,
|
||||
@@ -124,7 +120,6 @@ internal fun replaceQuickDevicePort(
|
||||
|
||||
val old = quickDevices[idx]
|
||||
val updated = old.copy(
|
||||
id = "$host:$newPort",
|
||||
port = newPort,
|
||||
online = online,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
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
|
||||
|
||||
class AppSettings(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()
|
||||
)
|
||||
}
|
||||
|
||||
// TODO?
|
||||
override fun validate(): Boolean = true
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
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,171 @@
|
||||
package io.github.miuzarte.scrcpyforandroid.storage
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class QuickDevices(context: Context) : Settings(context, "QuickDevices") {
|
||||
companion object {
|
||||
private val QUICK_DEVICES = Pair(
|
||||
stringPreferencesKey("quick_devices"),
|
||||
"",
|
||||
)
|
||||
private val QUICK_CONNECT_INPUT = Pair(
|
||||
stringPreferencesKey("quick_connect_input"),
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun toMap(): Map<String, Any> {
|
||||
return mapOf(
|
||||
QUICK_DEVICES.name to getQuickDevicesRaw(),
|
||||
QUICK_CONNECT_INPUT.name to getQuickConnectInput(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun validate(): Boolean = true
|
||||
|
||||
private suspend fun getQuickDevicesRaw(): String = getValue(QUICK_DEVICES)
|
||||
private suspend fun setQuickDevicesRaw(value: String) = setValue(QUICK_DEVICES, value)
|
||||
private fun observeQuickDevicesRaw(): Flow<String> = observe(QUICK_DEVICES)
|
||||
|
||||
suspend fun getQuickDevices(): List<DeviceShortcut> {
|
||||
val raw = getQuickDevicesRaw()
|
||||
if (raw.isBlank()) return emptyList()
|
||||
|
||||
val result = mutableListOf<DeviceShortcut>()
|
||||
raw.lineSequence().forEach { line ->
|
||||
DeviceShortcut.unmarshalFrom(line)?.let { result.add(it) }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun setQuickDevices(quickDevices: List<DeviceShortcut>) =
|
||||
setQuickDevicesRaw(quickDevices.joinToString("\n") { it.marshalToString() })
|
||||
|
||||
fun observeQuickDevices(): Flow<List<DeviceShortcut>> = observeQuickDevicesRaw()
|
||||
.map { raw ->
|
||||
if (raw.isBlank()) return@map emptyList()
|
||||
|
||||
val result = mutableListOf<DeviceShortcut>()
|
||||
raw.lineSequence().forEach { line ->
|
||||
DeviceShortcut.unmarshalFrom(line)?.let { result.add(it) }
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入或更新快速设备
|
||||
*/
|
||||
suspend fun upsertQuickDevice(
|
||||
host: String,
|
||||
port: Int,
|
||||
online: Boolean,
|
||||
index: Int? = 0,
|
||||
) {
|
||||
val quickDevices = getQuickDevices().toMutableList()
|
||||
val id = "$host:$port"
|
||||
val idx = quickDevices.indexOfFirst { it.id == id }
|
||||
val existingName = if (idx >= 0) quickDevices[idx].name else ""
|
||||
val item = DeviceShortcut(
|
||||
name = existingName,
|
||||
host = host,
|
||||
port = port,
|
||||
online = online,
|
||||
)
|
||||
if (idx >= 0) {
|
||||
quickDevices[idx] = item
|
||||
} else {
|
||||
if (index != null)
|
||||
quickDevices.add(index, item)
|
||||
else quickDevices.add(item)
|
||||
}
|
||||
setQuickDevices(quickDevices)
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果设备名称为空,则更新为备用名称
|
||||
*/
|
||||
suspend fun updateQuickDeviceNameIfEmpty(
|
||||
host: String,
|
||||
port: Int,
|
||||
fallbackName: String,
|
||||
) {
|
||||
val quickDevices = getQuickDevices().toMutableList()
|
||||
val idx = quickDevices.indexOfFirst { it.host == host && it.port == port }
|
||||
if (idx >= 0 && quickDevices[idx].name.isBlank()) {
|
||||
quickDevices[idx] = quickDevices[idx].copy(name = fallbackName)
|
||||
setQuickDevices(quickDevices)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换快速设备的端口
|
||||
*/
|
||||
suspend fun replaceQuickDevicePort(
|
||||
host: String,
|
||||
oldPort: Int,
|
||||
newPort: Int,
|
||||
online: Boolean,
|
||||
) {
|
||||
val quickDevices = getQuickDevices().toMutableList()
|
||||
val idx = quickDevices.indexOfFirst { it.host == host && it.port == oldPort }
|
||||
if (idx < 0) return
|
||||
|
||||
val old = quickDevices[idx]
|
||||
val updated = old.copy(
|
||||
port = newPort,
|
||||
online = online,
|
||||
)
|
||||
|
||||
quickDevices[idx] = updated
|
||||
val dedup = quickDevices.distinctBy { it.id }
|
||||
setQuickDevices(dedup)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除快速设备
|
||||
*/
|
||||
suspend fun removeQuickDevice(id: String) {
|
||||
val quickDevices = getQuickDevices().toMutableList()
|
||||
quickDevices.removeAll { it.id == id }
|
||||
setQuickDevices(quickDevices)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新设备在线状态
|
||||
*/
|
||||
suspend fun updateDeviceOnlineStatus(host: String, port: Int, online: Boolean) {
|
||||
val quickDevices = getQuickDevices().toMutableList()
|
||||
val idx = quickDevices.indexOfFirst { it.host == host && it.port == port }
|
||||
if (idx >= 0) {
|
||||
quickDevices[idx] = quickDevices[idx].copy(online = online)
|
||||
setQuickDevices(quickDevices)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新设备名称
|
||||
*/
|
||||
suspend fun updateDeviceName(id: String, name: String) {
|
||||
val quickDevices = getQuickDevices().toMutableList()
|
||||
val idx = quickDevices.indexOfFirst { it.id == id }
|
||||
if (idx >= 0) {
|
||||
quickDevices[idx] = quickDevices[idx].copy(name = name)
|
||||
setQuickDevices(quickDevices)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有快速设备
|
||||
*/
|
||||
suspend fun clearQuickDevices() {
|
||||
setQuickDevicesRaw("")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
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.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.AudioSource
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.CameraFacing
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.DisplayImePolicy
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.ListOptions
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.LogLevel
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Orientation
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.OrientationLock
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Tick
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.VideoSource
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
||||
companion object {
|
||||
private val CROP = Pair(
|
||||
stringPreferencesKey("crop"),
|
||||
""
|
||||
)
|
||||
private val RECORD_FILENAME = Pair(
|
||||
stringPreferencesKey("record_filename"),
|
||||
""
|
||||
)
|
||||
private val VIDEO_CODEC_OPTIONS = Pair(
|
||||
stringPreferencesKey("video_codec_options"),
|
||||
""
|
||||
)
|
||||
private val AUDIO_CODEC_OPTIONS = Pair(
|
||||
stringPreferencesKey("audio_codec_options"),
|
||||
""
|
||||
)
|
||||
private val VIDEO_ENCODER = Pair(
|
||||
stringPreferencesKey("video_encoder"),
|
||||
""
|
||||
)
|
||||
private val AUDIO_ENCODER = Pair(
|
||||
stringPreferencesKey("audio_encoder"),
|
||||
""
|
||||
)
|
||||
private val CAMERA_ID = Pair(
|
||||
stringPreferencesKey("camera_id"),
|
||||
""
|
||||
)
|
||||
private val CAMERA_SIZE = Pair(
|
||||
stringPreferencesKey("camera_size"),
|
||||
""
|
||||
)
|
||||
private val CAMERA_AR = Pair(
|
||||
stringPreferencesKey("camera_ar"),
|
||||
""
|
||||
)
|
||||
private val CAMERA_FPS = Pair(
|
||||
intPreferencesKey("camera_fps"),
|
||||
0
|
||||
)
|
||||
private val LOG_LEVEL = Pair(
|
||||
stringPreferencesKey("log_level"),
|
||||
"info"
|
||||
)
|
||||
private val VIDEO_CODEC = Pair(
|
||||
stringPreferencesKey("video_codec"),
|
||||
"h264"
|
||||
)
|
||||
private val AUDIO_CODEC = Pair(
|
||||
stringPreferencesKey("audio_codec"),
|
||||
"opus"
|
||||
)
|
||||
private val VIDEO_SOURCE = Pair(
|
||||
stringPreferencesKey("video_source"),
|
||||
"display"
|
||||
)
|
||||
private val AUDIO_SOURCE = Pair(
|
||||
stringPreferencesKey("audio_source"),
|
||||
"output"
|
||||
)
|
||||
private val RECORD_FORMAT = Pair(
|
||||
stringPreferencesKey("record_format"),
|
||||
"auto"
|
||||
)
|
||||
private val CAMERA_FACING = Pair(
|
||||
stringPreferencesKey("camera_facing"),
|
||||
"any"
|
||||
)
|
||||
private val MAX_SIZE = Pair(
|
||||
intPreferencesKey("max_size"),
|
||||
0
|
||||
)
|
||||
private val VIDEO_BIT_RATE = Pair(
|
||||
intPreferencesKey("video_bit_rate"),
|
||||
8000000
|
||||
)
|
||||
private val AUDIO_BIT_RATE = Pair(
|
||||
intPreferencesKey("audio_bit_rate"),
|
||||
128000
|
||||
)
|
||||
private val MAX_FPS = Pair(
|
||||
stringPreferencesKey("max_fps"),
|
||||
""
|
||||
)
|
||||
private val ANGLE = Pair(
|
||||
stringPreferencesKey("angle"),
|
||||
""
|
||||
)
|
||||
private val CAPTURE_ORIENTATION = Pair(
|
||||
intPreferencesKey("capture_orientation"),
|
||||
0
|
||||
)
|
||||
private val CAPTURE_ORIENTATION_LOCK = Pair(
|
||||
stringPreferencesKey("capture_orientation_lock"),
|
||||
"unlocked"
|
||||
)
|
||||
private val DISPLAY_ORIENTATION = Pair(
|
||||
intPreferencesKey("display_orientation"),
|
||||
0
|
||||
)
|
||||
private val RECORD_ORIENTATION = Pair(
|
||||
intPreferencesKey("record_orientation"),
|
||||
0
|
||||
)
|
||||
private val DISPLAY_IME_POLICY = Pair(
|
||||
stringPreferencesKey("display_ime_policy"),
|
||||
"undefined"
|
||||
)
|
||||
private val DISPLAY_ID = Pair(
|
||||
intPreferencesKey("display_id"),
|
||||
0
|
||||
)
|
||||
private val SCREEN_OFF_TIMEOUT = Pair(
|
||||
longPreferencesKey("screen_off_timeout"),
|
||||
-1
|
||||
)
|
||||
private val SHOW_TOUCHES = Pair(
|
||||
booleanPreferencesKey("show_touches"),
|
||||
false
|
||||
)
|
||||
private val FULLSCREEN = Pair(
|
||||
booleanPreferencesKey("fullscreen"),
|
||||
false
|
||||
)
|
||||
private val CONTROL = Pair(
|
||||
booleanPreferencesKey("control"),
|
||||
true
|
||||
)
|
||||
private val VIDEO_PLAYBACK = Pair(
|
||||
booleanPreferencesKey("video_playback"),
|
||||
true
|
||||
)
|
||||
private val AUDIO_PLAYBACK = Pair(
|
||||
booleanPreferencesKey("audio_playback"),
|
||||
true
|
||||
)
|
||||
private val TURN_SCREEN_OFF = Pair(
|
||||
booleanPreferencesKey("turn_screen_off"),
|
||||
false
|
||||
)
|
||||
private val STAY_AWAKE = Pair(
|
||||
booleanPreferencesKey("stay_awake"),
|
||||
false
|
||||
)
|
||||
private val DISABLE_SCREENSAVER = Pair(
|
||||
booleanPreferencesKey("disable_screensaver"),
|
||||
false
|
||||
)
|
||||
private val POWER_OFF_ON_CLOSE = Pair(
|
||||
booleanPreferencesKey("power_off_on_close"),
|
||||
false
|
||||
)
|
||||
private val CLEANUP = Pair(
|
||||
booleanPreferencesKey("cleanup"),
|
||||
true
|
||||
)
|
||||
private val POWER_ON = Pair(
|
||||
booleanPreferencesKey("power_on"),
|
||||
true
|
||||
)
|
||||
private val VIDEO = Pair(
|
||||
booleanPreferencesKey("video"),
|
||||
true
|
||||
)
|
||||
private val AUDIO = Pair(
|
||||
booleanPreferencesKey("audio"),
|
||||
true
|
||||
)
|
||||
private val REQUIRE_AUDIO = Pair(
|
||||
booleanPreferencesKey("require_audio"),
|
||||
false
|
||||
)
|
||||
private val KILL_ADB_ON_CLOSE = Pair(
|
||||
booleanPreferencesKey("kill_adb_on_close"),
|
||||
false
|
||||
)
|
||||
private val CAMERA_HIGH_SPEED = Pair(
|
||||
booleanPreferencesKey("camera_high_speed"),
|
||||
false
|
||||
)
|
||||
private val LIST = Pair(
|
||||
stringPreferencesKey("list"),
|
||||
"null"
|
||||
)
|
||||
private val AUDIO_DUP = Pair(
|
||||
booleanPreferencesKey("audio_dup"),
|
||||
false
|
||||
)
|
||||
private val NEW_DISPLAY = Pair(
|
||||
stringPreferencesKey("new_display"),
|
||||
""
|
||||
)
|
||||
private val START_APP = Pair(
|
||||
stringPreferencesKey("start_app"),
|
||||
""
|
||||
)
|
||||
private val VD_DESTROY_CONTENT = Pair(
|
||||
booleanPreferencesKey("vd_destroy_content"),
|
||||
true
|
||||
)
|
||||
private val VD_SYSTEM_DECORATIONS = Pair(
|
||||
booleanPreferencesKey("vd_system_decorations"),
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
val crop by setting(CROP)
|
||||
val recordFilename by setting(RECORD_FILENAME)
|
||||
val videoCodecOptions by setting(VIDEO_CODEC_OPTIONS)
|
||||
val audioCodecOptions by setting(AUDIO_CODEC_OPTIONS)
|
||||
val videoEncoder by setting(VIDEO_ENCODER)
|
||||
val audioEncoder by setting(AUDIO_ENCODER)
|
||||
val cameraId by setting(CAMERA_ID)
|
||||
val cameraSize by setting(CAMERA_SIZE)
|
||||
val cameraAr by setting(CAMERA_AR)
|
||||
val cameraFps by setting(CAMERA_FPS)
|
||||
val logLevel by setting(LOG_LEVEL)
|
||||
val videoCodec by setting(VIDEO_CODEC)
|
||||
val audioCodec by setting(AUDIO_CODEC)
|
||||
val videoSource by setting(VIDEO_SOURCE)
|
||||
val audioSource by setting(AUDIO_SOURCE)
|
||||
val recordFormat by setting(RECORD_FORMAT)
|
||||
val cameraFacing by setting(CAMERA_FACING)
|
||||
val maxSize by setting(MAX_SIZE)
|
||||
val videoBitRate by setting(VIDEO_BIT_RATE)
|
||||
val audioBitRate by setting(AUDIO_BIT_RATE)
|
||||
val maxFps by setting(MAX_FPS)
|
||||
val angle by setting(ANGLE)
|
||||
val captureOrientation by setting(CAPTURE_ORIENTATION)
|
||||
val captureOrientationLock by setting(CAPTURE_ORIENTATION_LOCK)
|
||||
val displayOrientation by setting(DISPLAY_ORIENTATION)
|
||||
val recordOrientation by setting(RECORD_ORIENTATION)
|
||||
val displayImePolicy by setting(DISPLAY_IME_POLICY)
|
||||
val displayId by setting(DISPLAY_ID)
|
||||
val screenOffTimeout by setting(SCREEN_OFF_TIMEOUT)
|
||||
val showTouches by setting(SHOW_TOUCHES)
|
||||
val fullscreen by setting(FULLSCREEN)
|
||||
val control by setting(CONTROL)
|
||||
val videoPlayback by setting(VIDEO_PLAYBACK)
|
||||
val audioPlayback by setting(AUDIO_PLAYBACK)
|
||||
val turnScreenOff by setting(TURN_SCREEN_OFF)
|
||||
val stayAwake by setting(STAY_AWAKE)
|
||||
val disableScreensaver by setting(DISABLE_SCREENSAVER)
|
||||
val powerOffOnClose by setting(POWER_OFF_ON_CLOSE)
|
||||
val cleanup by setting(CLEANUP)
|
||||
val powerOn by setting(POWER_ON)
|
||||
val video by setting(VIDEO)
|
||||
val audio by setting(AUDIO)
|
||||
val requireAudio by setting(REQUIRE_AUDIO)
|
||||
val killAdbOnClose by setting(KILL_ADB_ON_CLOSE)
|
||||
val cameraHighSpeed by setting(CAMERA_HIGH_SPEED)
|
||||
val list by setting(LIST)
|
||||
val audioDup by setting(AUDIO_DUP)
|
||||
val newDisplay by setting(NEW_DISPLAY)
|
||||
val startApp by setting(START_APP)
|
||||
val vdDestroyContent by setting(VD_DESTROY_CONTENT)
|
||||
val vdSystemDecorations by setting(VD_SYSTEM_DECORATIONS)
|
||||
|
||||
override suspend fun toMap(): Map<String, Any> {
|
||||
return mapOf(
|
||||
CROP.name to crop.get(),
|
||||
RECORD_FILENAME.name to recordFilename.get(),
|
||||
VIDEO_CODEC_OPTIONS.name to videoCodecOptions.get(),
|
||||
AUDIO_CODEC_OPTIONS.name to audioCodecOptions.get(),
|
||||
VIDEO_ENCODER.name to videoEncoder.get(),
|
||||
AUDIO_ENCODER.name to audioEncoder.get(),
|
||||
CAMERA_ID.name to cameraId.get(),
|
||||
CAMERA_SIZE.name to cameraSize.get(),
|
||||
CAMERA_AR.name to cameraAr.get(),
|
||||
CAMERA_FPS.name to cameraFps.get(),
|
||||
LOG_LEVEL.name to logLevel.get(),
|
||||
VIDEO_CODEC.name to videoCodec.get(),
|
||||
AUDIO_CODEC.name to audioCodec.get(),
|
||||
VIDEO_SOURCE.name to videoSource.get(),
|
||||
AUDIO_SOURCE.name to audioSource.get(),
|
||||
RECORD_FORMAT.name to recordFormat.get(),
|
||||
CAMERA_FACING.name to cameraFacing.get(),
|
||||
MAX_SIZE.name to maxSize.get(),
|
||||
VIDEO_BIT_RATE.name to videoBitRate.get(),
|
||||
AUDIO_BIT_RATE.name to audioBitRate.get(),
|
||||
MAX_FPS.name to maxFps.get(),
|
||||
ANGLE.name to angle.get(),
|
||||
CAPTURE_ORIENTATION.name to captureOrientation.get(),
|
||||
CAPTURE_ORIENTATION_LOCK.name to captureOrientationLock.get(),
|
||||
DISPLAY_ORIENTATION.name to displayOrientation.get(),
|
||||
RECORD_ORIENTATION.name to recordOrientation.get(),
|
||||
DISPLAY_IME_POLICY.name to displayImePolicy.get(),
|
||||
DISPLAY_ID.name to displayId.get(),
|
||||
SCREEN_OFF_TIMEOUT.name to screenOffTimeout.get(),
|
||||
SHOW_TOUCHES.name to showTouches.get(),
|
||||
FULLSCREEN.name to fullscreen.get(),
|
||||
CONTROL.name to control.get(),
|
||||
VIDEO_PLAYBACK.name to videoPlayback.get(),
|
||||
AUDIO_PLAYBACK.name to audioPlayback.get(),
|
||||
TURN_SCREEN_OFF.name to turnScreenOff.get(),
|
||||
STAY_AWAKE.name to stayAwake.get(),
|
||||
DISABLE_SCREENSAVER.name to disableScreensaver.get(),
|
||||
POWER_OFF_ON_CLOSE.name to powerOffOnClose.get(),
|
||||
CLEANUP.name to cleanup.get(),
|
||||
POWER_ON.name to powerOn.get(),
|
||||
VIDEO.name to video.get(),
|
||||
AUDIO.name to audio.get(),
|
||||
REQUIRE_AUDIO.name to requireAudio.get(),
|
||||
KILL_ADB_ON_CLOSE.name to killAdbOnClose.get(),
|
||||
CAMERA_HIGH_SPEED.name to cameraHighSpeed.get(),
|
||||
LIST.name to list.get(),
|
||||
AUDIO_DUP.name to audioDup.get(),
|
||||
NEW_DISPLAY.name to newDisplay.get(),
|
||||
START_APP.name to startApp.get(),
|
||||
VD_DESTROY_CONTENT.name to vdDestroyContent.get(),
|
||||
VD_SYSTEM_DECORATIONS.name to vdSystemDecorations.get()
|
||||
)
|
||||
}
|
||||
|
||||
override fun validate(): Boolean = runBlocking {
|
||||
runCatching {
|
||||
toClientOptions().validate()
|
||||
true
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
|
||||
// TODO: 处理空值
|
||||
suspend fun toClientOptions(): ClientOptions {
|
||||
return ClientOptions(
|
||||
crop = crop.get(),
|
||||
recordFilename = recordFilename.get(),
|
||||
videoCodecOptions = videoCodecOptions.get(),
|
||||
audioCodecOptions = audioCodecOptions.get(),
|
||||
videoEncoder = videoEncoder.get(),
|
||||
audioEncoder = audioEncoder.get(),
|
||||
cameraId = cameraId.get(),
|
||||
cameraSize = cameraSize.get(),
|
||||
cameraAr = cameraAr.get(),
|
||||
cameraFps = cameraFps.get().toUShort(),
|
||||
logLevel = LogLevel.valueOf(logLevel.get().uppercase()),
|
||||
videoCodec = Codec.valueOf(videoCodec.get().uppercase()),
|
||||
audioCodec = Codec.valueOf(audioCodec.get().uppercase()),
|
||||
videoSource = VideoSource.valueOf(videoSource.get().uppercase()),
|
||||
audioSource = AudioSource.valueOf(audioSource.get().uppercase()),
|
||||
recordFormat = ClientOptions.RecordFormat.valueOf(recordFormat.get().uppercase()),
|
||||
cameraFacing = CameraFacing.valueOf(cameraFacing.get().uppercase()),
|
||||
maxSize = maxSize.get().toUShort(),
|
||||
videoBitRate = videoBitRate.get().toUInt(),
|
||||
audioBitRate = audioBitRate.get().toUInt(),
|
||||
maxFps = maxFps.get(),
|
||||
angle = angle.get(),
|
||||
captureOrientation = Orientation.fromInt(captureOrientation.get()),
|
||||
captureOrientationLock = OrientationLock.valueOf(
|
||||
captureOrientationLock.get().uppercase()
|
||||
),
|
||||
displayOrientation = Orientation.fromInt(displayOrientation.get()),
|
||||
recordOrientation = Orientation.fromInt(recordOrientation.get()),
|
||||
displayImePolicy = DisplayImePolicy.valueOf(displayImePolicy.get().uppercase()),
|
||||
displayId = displayId.get().toUInt(),
|
||||
screenOffTimeout = Tick(screenOffTimeout.get()),
|
||||
showTouches = showTouches.get(),
|
||||
fullscreen = fullscreen.get(),
|
||||
control = control.get(),
|
||||
videoPlayback = videoPlayback.get(),
|
||||
audioPlayback = audioPlayback.get(),
|
||||
turnScreenOff = turnScreenOff.get(),
|
||||
stayAwake = stayAwake.get(),
|
||||
disableScreensaver = disableScreensaver.get(),
|
||||
powerOffOnClose = powerOffOnClose.get(),
|
||||
cleanUp = cleanup.get(),
|
||||
powerOn = powerOn.get(),
|
||||
video = video.get(),
|
||||
audio = audio.get(),
|
||||
requireAudio = requireAudio.get(),
|
||||
killAdbOnClose = killAdbOnClose.get(),
|
||||
cameraHighSpeed = cameraHighSpeed.get(),
|
||||
list = ListOptions.valueOf(list.get().uppercase()),
|
||||
audioDup = audioDup.get(),
|
||||
newDisplay = newDisplay.get(),
|
||||
startApp = startApp.get(),
|
||||
vdDestroyContent = vdDestroyContent.get(),
|
||||
vdSystemDecorations = vdSystemDecorations.get()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package io.github.miuzarte.scrcpyforandroid.storage
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
private const val TAG = "Settings"
|
||||
|
||||
abstract class Settings(
|
||||
context: Context,
|
||||
private val name: String,
|
||||
corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? =
|
||||
ReplaceFileCorruptionHandler {
|
||||
Log.e(TAG, "Preferences corrupted, resetting.", it)
|
||||
emptyPreferences()
|
||||
}
|
||||
) {
|
||||
data class Pair<T>(
|
||||
val key: Preferences.Key<T>,
|
||||
val defaultValue: T,
|
||||
) {
|
||||
val name: String get() = key.name
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置项委托类,自动提供 get/set/observe/observeAsState/asMutableState 方法
|
||||
*/
|
||||
inner class SettingProperty<T>(
|
||||
private val pair: Pair<T>
|
||||
) {
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): SettingProperty<T> = this
|
||||
|
||||
suspend fun get(): T = getValue(pair)
|
||||
|
||||
suspend fun set(value: T) = setValue(pair, value)
|
||||
|
||||
fun observe(): Flow<T> = this@Settings.observe(pair)
|
||||
|
||||
@Composable
|
||||
fun asState(): State<T> = this@Settings.asState(pair)
|
||||
|
||||
@Composable
|
||||
fun asMutableState(): MutableState<T> = this@Settings.asMutableState(pair)
|
||||
}
|
||||
|
||||
// 为 Context 添加扩展委托属性,确保 DataStore 单例
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
|
||||
name = this.name,
|
||||
corruptionHandler = corruptionHandler,
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
)
|
||||
|
||||
abstract suspend fun toMap(): Map<String, Any>
|
||||
abstract fun validate(): Boolean
|
||||
|
||||
// 对外暴露的 DataStore 实例
|
||||
protected val dataStore: DataStore<Preferences> = context.dataStore
|
||||
|
||||
protected fun <T> setting(pair: Pair<T>): SettingProperty<T> =
|
||||
SettingProperty(pair)
|
||||
|
||||
protected suspend fun <T> getValue(pair: Pair<T>): T =
|
||||
dataStore.data.first()[pair.key] ?: pair.defaultValue
|
||||
|
||||
protected suspend fun <T> setValue(pair: Pair<T>, value: T) {
|
||||
dataStore.edit { preferences -> preferences[pair.key] = value }
|
||||
}
|
||||
|
||||
protected fun <T> observe(pair: Pair<T>): Flow<T> =
|
||||
dataStore.data.map { preferences -> preferences[pair.key] ?: pair.defaultValue }
|
||||
|
||||
@Composable
|
||||
protected fun <T> asState(pair: Pair<T>): State<T> =
|
||||
observe(pair).collectAsState(initial = pair.defaultValue)
|
||||
|
||||
@Composable
|
||||
protected fun <T> asMutableState(pair: Pair<T>): MutableState<T> {
|
||||
val scope = rememberCoroutineScope()
|
||||
val state = asState(pair)
|
||||
|
||||
return remember(state.value) {
|
||||
object : MutableState<T> {
|
||||
override var value: T
|
||||
get() = state.value
|
||||
set(newValue) {
|
||||
scope.launch {
|
||||
setValue(pair, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
override fun component1(): T = value
|
||||
override fun component2(): (T) -> Unit = { value = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.input.pointer.pointerInteropFilter
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
@@ -70,6 +71,8 @@ import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
||||
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics
|
||||
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
|
||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlide
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -92,12 +95,16 @@ import top.yukonga.miuix.kmp.theme.MiuixTheme.isDynamicColor
|
||||
import top.yukonga.miuix.kmp.utils.PressFeedbackType
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
// TODO: migrate to scrcpy.Shared
|
||||
|
||||
@Deprecated("scrcpy.Shared.Codec")
|
||||
private val VIDEO_CODEC_OPTIONS = listOf(
|
||||
"h264" to "H.264",
|
||||
"h265" to "H.265",
|
||||
"av1" to "AV1",
|
||||
)
|
||||
|
||||
@Deprecated("scrcpy.Shared.Codec")
|
||||
private val AUDIO_CODEC_OPTIONS = listOf(
|
||||
"opus" to "Opus",
|
||||
"aac" to "AAC",
|
||||
@@ -124,8 +131,14 @@ internal fun StatusCard(
|
||||
sessionInfo: ScrcpySessionInfo?,
|
||||
busyLabel: String?,
|
||||
connectedDeviceLabel: String,
|
||||
themeBaseIndex: Int,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val appSettings = remember(context) { AppSettings(context) }
|
||||
val scrcpyOptions = remember(context) { ScrcpyOptions(context) }
|
||||
|
||||
val themeBaseIndex by appSettings.themeBaseIndex.asState()
|
||||
|
||||
val cleanStatusLine = normalizeStatusLine(statusLine)
|
||||
|
||||
// 根据应用主题设置决定是否使用深色模式
|
||||
@@ -261,9 +274,9 @@ internal fun PreviewCard(
|
||||
previewHeightDp: Int,
|
||||
controlsVisible: Boolean,
|
||||
onTapped: () -> Unit,
|
||||
onOpenFullscreenHaptic: (() -> Unit)? = null,
|
||||
onOpenFullscreen: () -> Unit,
|
||||
) {
|
||||
val haptics = rememberAppHaptics()
|
||||
val alpha by animateFloatAsState(if (controlsVisible) 1f else 0f, label = "preview-controls")
|
||||
|
||||
Card {
|
||||
@@ -312,7 +325,7 @@ internal fun PreviewCard(
|
||||
Button(
|
||||
onClick = {
|
||||
if (alpha > 0.1) {
|
||||
onOpenFullscreenHaptic?.invoke()
|
||||
haptics.contextClick()
|
||||
onOpenFullscreen()
|
||||
}
|
||||
},
|
||||
@@ -361,94 +374,99 @@ internal fun VirtualButtonCard(
|
||||
@Composable
|
||||
internal fun ConfigPanel(
|
||||
busy: Boolean,
|
||||
bitRateMbps: Float,
|
||||
onBitRateSliderChange: (Float) -> Unit,
|
||||
onBitRateInputChange: (String) -> Unit,
|
||||
audioBitRateKbps: Int,
|
||||
onAudioBitRateChange: (Int) -> Unit,
|
||||
videoCodec: String,
|
||||
onVideoCodecChange: (String) -> Unit,
|
||||
audioEnabled: Boolean,
|
||||
onAudioEnabledChange: (Boolean) -> Unit,
|
||||
audioForwardingSupported: Boolean,
|
||||
audioCodec: String,
|
||||
onAudioCodecChange: (String) -> Unit,
|
||||
onOpenAdvanced: () -> Unit,
|
||||
onStartStopHaptic: (() -> Unit)? = null,
|
||||
onStart: () -> Unit,
|
||||
onStop: () -> Unit,
|
||||
sessionStarted: Boolean,
|
||||
) {
|
||||
val videoCodecItems = remember { VIDEO_CODEC_OPTIONS.map { it.second } }
|
||||
val videoCodecIndex =
|
||||
VIDEO_CODEC_OPTIONS.indexOfFirst { it.first == videoCodec }.coerceAtLeast(0)
|
||||
val audioCodecItems = remember { AUDIO_CODEC_OPTIONS.map { it.second } }
|
||||
val audioCodecIndex =
|
||||
AUDIO_CODEC_OPTIONS.indexOfFirst { it.first == audioCodec }.coerceAtLeast(0)
|
||||
val audioBitRatePresetIndex =
|
||||
presetIndexFromInput(audioBitRateKbps.toString(), ScrcpyPresets.AudioBitRate)
|
||||
val context = LocalContext.current
|
||||
|
||||
// val appSettings = remember(context) { AppSettings(context) }
|
||||
val scrcpyOptions = remember(context) { ScrcpyOptions(context) }
|
||||
|
||||
SectionSmallTitle("Scrcpy")
|
||||
Card {
|
||||
var audio by scrcpyOptions.audio.asMutableState()
|
||||
SuperSwitch(
|
||||
title = "音频转发",
|
||||
summary = "转发设备音频到本机 (Android 11+)",
|
||||
checked = audioEnabled,
|
||||
onCheckedChange = onAudioEnabledChange,
|
||||
checked = audio,
|
||||
onCheckedChange = { audio = it },
|
||||
enabled = !sessionStarted && audioForwardingSupported,
|
||||
)
|
||||
|
||||
var audioCodec by scrcpyOptions.audioCodec.asMutableState()
|
||||
val audioCodecItems = remember { AUDIO_CODEC_OPTIONS.map { it.second } }
|
||||
val audioCodecIndex = AUDIO_CODEC_OPTIONS.indexOfFirst {
|
||||
it.first == audioCodec
|
||||
}.coerceAtLeast(0)
|
||||
var audioBitRate by scrcpyOptions.audioBitRate.asMutableState()
|
||||
val audioBitRateKbps = audioBitRate * 1_000f
|
||||
val audioBitRatePresetIndex = presetIndexFromInput(
|
||||
audioBitRateKbps.toString(),
|
||||
ScrcpyPresets.AudioBitRate
|
||||
)
|
||||
SuperDropdown(
|
||||
title = "音频编码",
|
||||
summary = "--audio-codec",
|
||||
items = audioCodecItems,
|
||||
selectedIndex = audioCodecIndex,
|
||||
onSelectedIndexChange = { onAudioCodecChange(AUDIO_CODEC_OPTIONS[it].first) },
|
||||
enabled = !sessionStarted && audioEnabled,
|
||||
onSelectedIndexChange = { audioCodec = AUDIO_CODEC_OPTIONS[it].first },
|
||||
enabled = !sessionStarted && audio,
|
||||
)
|
||||
if (audioEnabled && (audioCodec == "opus" || audioCodec == "aac")) {
|
||||
if (audio && (audioCodec == "opus" || audioCodec == "aac")) {
|
||||
SuperSlide(
|
||||
title = "音频码率",
|
||||
summary = "--audio-bit-rate",
|
||||
value = audioBitRatePresetIndex.toFloat(),
|
||||
onValueChange = { value ->
|
||||
val idx = value.roundToInt().coerceIn(0, ScrcpyPresets.AudioBitRate.lastIndex)
|
||||
onAudioBitRateChange(ScrcpyPresets.AudioBitRate[idx])
|
||||
audioBitRate = ScrcpyPresets.AudioBitRate[idx] * 1024
|
||||
},
|
||||
valueRange = 0f..ScrcpyPresets.AudioBitRate.lastIndex.toFloat(),
|
||||
steps = (ScrcpyPresets.AudioBitRate.size - 2).coerceAtLeast(0),
|
||||
enabled = !sessionStarted,
|
||||
unit = "Kbps",
|
||||
displayText = audioBitRateKbps.toString(),
|
||||
inputInitialValue = audioBitRateKbps.toString(),
|
||||
displayText = audioBitRate.toString(),
|
||||
inputInitialValue = audioBitRate.toString(),
|
||||
inputFilter = { it.filter(Char::isDigit) },
|
||||
inputValueRange = 1f..Float.MAX_VALUE,
|
||||
onInputConfirm = { raw ->
|
||||
raw.toIntOrNull()?.takeIf { it > 0 }?.let { onAudioBitRateChange(it) }
|
||||
raw.toIntOrNull()?.takeIf { it > 0 }?.let { audioBitRate = it }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
var videoCodec by scrcpyOptions.videoCodec.asMutableState()
|
||||
val videoCodecItems = remember { VIDEO_CODEC_OPTIONS.map { it.second } }
|
||||
val videoCodecIndex = VIDEO_CODEC_OPTIONS.indexOfFirst {
|
||||
it.first == videoCodec
|
||||
}.coerceAtLeast(0)
|
||||
var videoBitRate by scrcpyOptions.videoBitRate.asMutableState()
|
||||
val videoBitRateMbps = videoBitRate / 1_000_000f
|
||||
SuperDropdown(
|
||||
title = "视频编码",
|
||||
summary = "--video-codec",
|
||||
items = videoCodecItems,
|
||||
selectedIndex = videoCodecIndex,
|
||||
onSelectedIndexChange = { onVideoCodecChange(VIDEO_CODEC_OPTIONS[it].first) },
|
||||
onSelectedIndexChange = { videoCodec = VIDEO_CODEC_OPTIONS[it].first },
|
||||
enabled = !sessionStarted,
|
||||
)
|
||||
SuperSlide(
|
||||
title = "视频码率",
|
||||
summary = "--video-bit-rate",
|
||||
value = bitRateMbps,
|
||||
onValueChange = {
|
||||
onBitRateSliderChange(it)
|
||||
onBitRateInputChange(formatBitRate(it))
|
||||
value = videoBitRateMbps,
|
||||
onValueChange = { mbps ->
|
||||
videoBitRate = (mbps * 1_000_000).toInt()
|
||||
},
|
||||
valueRange = 0.1f..40f,
|
||||
steps = 399,
|
||||
enabled = !sessionStarted,
|
||||
unit = "Mbps",
|
||||
displayFormatter = { formatBitRate(it) },
|
||||
inputInitialValue = formatBitRate(bitRateMbps),
|
||||
inputInitialValue = formatBitRate(videoBitRateMbps),
|
||||
inputFilter = { text ->
|
||||
var dotUsed = false
|
||||
text.filter { ch ->
|
||||
@@ -467,18 +485,19 @@ internal fun ConfigPanel(
|
||||
onInputConfirm = { raw ->
|
||||
raw.toFloatOrNull()?.let { parsed ->
|
||||
if (parsed >= 0.1f) {
|
||||
onBitRateSliderChange(parsed)
|
||||
onBitRateInputChange(formatBitRate(parsed))
|
||||
videoBitRate = (parsed * 1_000_000).toInt()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
SuperArrow(
|
||||
title = "高级参数",
|
||||
summary = "更多 scrcpy 启动参数",
|
||||
onClick = onOpenAdvanced,
|
||||
enabled = !sessionStarted,
|
||||
)
|
||||
|
||||
TextButton(
|
||||
text = if (sessionStarted) "停止" else "启动",
|
||||
onClick = {
|
||||
@@ -1353,7 +1372,6 @@ internal fun DeviceEditorScreen(
|
||||
if (h.isNotBlank()) {
|
||||
onSave(
|
||||
DeviceShortcut(
|
||||
id = "$h:$p",
|
||||
name = name.trim(),
|
||||
host = h,
|
||||
port = p,
|
||||
|
||||
Reference in New Issue
Block a user