improve: implement more scrcpy options
This commit is contained in:
@@ -59,6 +59,7 @@ fun Preset<Int>.indexOfOrNearest(raw: String): Int {
|
||||
object ScrcpyPresets {
|
||||
val MaxSize = Preset(listOf(0, 720, 1080, 1280, 1600, 1920, 2160, 2560, 3200, 3840)) // px
|
||||
val MaxFPS = Preset(listOf(0, 24, 30, 45, 60, 90, 120)) // fps
|
||||
val ScreenOffTimeout = Preset(listOf(0, 15, 30, 60, 120, 300, 600)) // sec
|
||||
val AudioBitRate = Preset(listOf(0, 32, 64, 96, 128, 160, 192, 256, 320, 384, 512)) // Kbps
|
||||
val CameraFps = Preset(listOf(0, 10, 15, 24, 30, 60, 120, 240, 480, 960)) // fps
|
||||
}
|
||||
|
||||
@@ -133,6 +133,41 @@ object NativeAdbService {
|
||||
response
|
||||
}
|
||||
|
||||
suspend fun startApp(
|
||||
packageName: String,
|
||||
displayId: Int? = null,
|
||||
forceStop: Boolean = false,
|
||||
): String = mutex.withLock {
|
||||
val normalizedPackageName = packageName.trim()
|
||||
require(normalizedPackageName.isNotBlank()) { "package name is blank" }
|
||||
|
||||
if (forceStop) {
|
||||
requireConnection().shell(
|
||||
"am force-stop ${quoteShellArg(normalizedPackageName)}"
|
||||
)
|
||||
}
|
||||
|
||||
val resolveOutput = requireConnection().shell(
|
||||
"cmd package resolve-activity --brief ${quoteShellArg(normalizedPackageName)}"
|
||||
)
|
||||
val componentName = resolveOutput
|
||||
.lineSequence()
|
||||
.map(String::trim)
|
||||
.lastOrNull { '/' in it }
|
||||
?: throw IllegalStateException(
|
||||
"Cannot resolve launch activity for $normalizedPackageName"
|
||||
)
|
||||
|
||||
val displayArg = displayId
|
||||
?.takeIf { it >= 0 }
|
||||
?.let { " --display $it" }
|
||||
.orEmpty()
|
||||
val command = "am start-activity$displayArg -n ${quoteShellArg(componentName)}"
|
||||
val response = requireConnection().shell(command)
|
||||
Log.d(TAG, "startApp(): package=$normalizedPackageName component=$componentName")
|
||||
response
|
||||
}
|
||||
|
||||
suspend fun openShellStream(command: String): AdbSocketStream = mutex.withLock {
|
||||
requireConnection().openStream("shell:$command")
|
||||
}
|
||||
@@ -161,5 +196,9 @@ object NativeAdbService {
|
||||
?: throw IllegalStateException("ADB not connected")
|
||||
}
|
||||
|
||||
private fun quoteShellArg(value: String): String {
|
||||
return "'" + value.replace("'", "'\\''") + "'"
|
||||
}
|
||||
|
||||
private const val TAG = "NativeAdbService"
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcuts
|
||||
import io.github.miuzarte.scrcpyforandroid.nativecore.NativeAdbService
|
||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.VideoSource
|
||||
import io.github.miuzarte.scrcpyforandroid.services.AppWakeLocks
|
||||
import io.github.miuzarte.scrcpyforandroid.services.EventLogger
|
||||
import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent
|
||||
@@ -89,6 +90,13 @@ private const val ADB_TCP_PROBE_TIMEOUT_MS = 500
|
||||
private const val PREVIEW_CARD_ITEM_KEY = "preview_card"
|
||||
private const val PREVIEW_CARD_ITEM_INDEX = 3
|
||||
|
||||
private data class StartAppRequest(
|
||||
val packageName: String,
|
||||
val displayId: Int?,
|
||||
val forceStop: Boolean,
|
||||
val matchedAppLabel: String? = null,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun DeviceTabScreen(
|
||||
scrollBehavior: ScrollBehavior,
|
||||
@@ -517,11 +525,102 @@ fun DeviceTabPage(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun resolveStartAppRequest(
|
||||
scrcpy: Scrcpy,
|
||||
options: io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions,
|
||||
): StartAppRequest? {
|
||||
val raw = options.startApp.trim()
|
||||
if (raw.isBlank()) {
|
||||
return null
|
||||
}
|
||||
|
||||
var query = raw
|
||||
val forceStop = query.startsWith("+")
|
||||
if (forceStop) {
|
||||
query = query.drop(1).trimStart()
|
||||
}
|
||||
require(query.isNotBlank()) { "应用名或包名不能为空" }
|
||||
|
||||
val displayId = when {
|
||||
options.videoSource != VideoSource.DISPLAY -> null
|
||||
options.displayId >= 0 -> options.displayId
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (!query.startsWith("?")) {
|
||||
return StartAppRequest(
|
||||
packageName = query,
|
||||
displayId = displayId,
|
||||
forceStop = forceStop,
|
||||
)
|
||||
}
|
||||
|
||||
val searchName = query.drop(1).trim()
|
||||
require(searchName.isNotBlank()) { "应用名称不能为空" }
|
||||
|
||||
val apps = scrcpy.listings.getApps(forceRefresh = false)
|
||||
val matches = apps.filter { it.label.startsWith(searchName, ignoreCase = true) }
|
||||
|
||||
require(matches.isNotEmpty()) { "未找到应用名以 \"$searchName\" 开头的应用" }
|
||||
require(matches.size == 1) {
|
||||
"按名称匹配到多个应用: " +
|
||||
matches.take(5).joinToString { "${it.label} (${it.packageName})" }
|
||||
}
|
||||
|
||||
return StartAppRequest(
|
||||
packageName = matches[0].packageName,
|
||||
displayId = displayId,
|
||||
forceStop = forceStop,
|
||||
matchedAppLabel = matches[0].label,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun startScrcpySession(openFullscreen: Boolean = false) {
|
||||
val options = scrcpyOptions.toClientOptions(soBundleShared).fix()
|
||||
val session = scrcpy.start(options)
|
||||
pendingScrollToPreview = true
|
||||
if (openFullscreen) withContext(Dispatchers.Main) {
|
||||
val startAppRequest = runCatching {
|
||||
resolveStartAppRequest(scrcpy, options)
|
||||
}.getOrElse { error ->
|
||||
logEvent(
|
||||
"启动应用请求无效: ${
|
||||
error.message?.takeIf { it.isNotBlank() } ?: error.javaClass.simpleName
|
||||
}",
|
||||
Log.WARN,
|
||||
error,
|
||||
)
|
||||
null
|
||||
}
|
||||
startAppRequest?.let { request ->
|
||||
if (options.newDisplay.isNotBlank() && request.displayId == null) {
|
||||
logEvent(
|
||||
"当前实现无法获取 new display 的真实 displayId,应用会启动到默认显示",
|
||||
Log.WARN,
|
||||
)
|
||||
}
|
||||
runCatching {
|
||||
NativeAdbService.startApp(
|
||||
packageName = request.packageName,
|
||||
displayId = request.displayId,
|
||||
forceStop = request.forceStop,
|
||||
)
|
||||
}.onSuccess {
|
||||
val appLabelPart = request.matchedAppLabel?.let { " ($it)" }.orEmpty()
|
||||
logEvent(
|
||||
"已启动应用: ${request.packageName}$appLabelPart" +
|
||||
request.displayId?.let { " @display=$it" }.orEmpty()
|
||||
)
|
||||
}.onFailure { error ->
|
||||
logEvent(
|
||||
"启动应用失败: ${request.packageName} (${
|
||||
error.message?.takeIf { it.isNotBlank() } ?: error.javaClass.simpleName
|
||||
})",
|
||||
Log.WARN,
|
||||
error,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (options.fullscreen || openFullscreen) withContext(Dispatchers.Main) {
|
||||
context.startActivity(StreamActivity.createIntent(context))
|
||||
}
|
||||
if (options.disableScreensaver)
|
||||
@@ -549,6 +648,15 @@ fun DeviceTabPage(
|
||||
snackbar.show("scrcpy 已启动")
|
||||
}
|
||||
|
||||
suspend fun stopScrcpySession() {
|
||||
val options = scrcpyOptions.toClientOptions(soBundleShared).fix()
|
||||
scrcpy.stop()
|
||||
if (options.killAdbOnClose) {
|
||||
// TODO
|
||||
disconnectAdbConnection()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(pendingScrollToPreview, isPreviewCardVisible) {
|
||||
if (!pendingScrollToPreview) return@LaunchedEffect
|
||||
if (isPreviewCardVisible) return@LaunchedEffect
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -40,8 +41,13 @@ import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn
|
||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlider
|
||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperTextField
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared
|
||||
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.LogLevel
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Tick
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.VideoSource
|
||||
import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.Settings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions
|
||||
@@ -61,6 +67,9 @@ import top.yukonga.miuix.kmp.basic.SpinnerEntry
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
import top.yukonga.miuix.kmp.basic.TextField
|
||||
import top.yukonga.miuix.kmp.basic.TopAppBar
|
||||
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
||||
import top.yukonga.miuix.kmp.icon.extended.Store
|
||||
import top.yukonga.miuix.kmp.icon.extended.Tune
|
||||
import top.yukonga.miuix.kmp.preference.ArrowPreference
|
||||
import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference
|
||||
import top.yukonga.miuix.kmp.preference.OverlaySpinnerPreference
|
||||
@@ -153,26 +162,50 @@ internal fun ScrcpyAllOptionsPage(
|
||||
.coerceAtLeast(0)
|
||||
}
|
||||
|
||||
val videoSourceItems = rememberSaveable { Shared.VideoSource.entries.map { it.string } }
|
||||
val videoSourceItems = rememberSaveable { VideoSource.entries.map { it.string } }
|
||||
val videoSourceIndex = rememberSaveable(soBundle.videoSource) {
|
||||
Shared.VideoSource.entries
|
||||
VideoSource.entries
|
||||
.indexOfFirst { it.string == soBundle.videoSource }
|
||||
.coerceAtLeast(0)
|
||||
}
|
||||
|
||||
var displayIdInput by rememberSaveable(soBundle.displayId) {
|
||||
mutableStateOf(
|
||||
if (soBundle.displayId == -1) ""
|
||||
else soBundle.displayId.toString()
|
||||
)
|
||||
val displays = scrcpy.listings.displays
|
||||
val displayDropdownItems = rememberSaveable(displays, listRefreshVersion) {
|
||||
listOf("默认") + displays.map { "${it.id} (${it.width}x${it.height})" }
|
||||
}
|
||||
val displayDropdownIndex = rememberSaveable(
|
||||
soBundle.displayId,
|
||||
displays,
|
||||
listRefreshVersion,
|
||||
) {
|
||||
(displays.indexOfFirst { it.id == soBundle.displayId } + 1).coerceAtLeast(0)
|
||||
}
|
||||
|
||||
var cameraIdInput by rememberSaveable(soBundle.cameraId) {
|
||||
mutableStateOf(soBundle.cameraId)
|
||||
val cameras = scrcpy.listings.cameras
|
||||
val cameraDropdownItems = rememberSaveable(cameras, listRefreshVersion) {
|
||||
listOf("默认") + cameras.map { info ->
|
||||
buildString {
|
||||
append(info.id)
|
||||
append(" (")
|
||||
append(info.facing.string)
|
||||
if (info.activeSize.isNotBlank()) {
|
||||
append(", ")
|
||||
append(info.activeSize)
|
||||
}
|
||||
append(')')
|
||||
}
|
||||
}
|
||||
}
|
||||
val cameraDropdownIndex = rememberSaveable(
|
||||
soBundle.cameraId,
|
||||
cameras,
|
||||
listRefreshVersion,
|
||||
) {
|
||||
(cameras.indexOfFirst { it.id == soBundle.cameraId } + 1).coerceAtLeast(0)
|
||||
}
|
||||
|
||||
val cameraFacingItems = rememberSaveable {
|
||||
listOf("默认") + Shared.CameraFacing.entries
|
||||
listOf("默认") + CameraFacing.entries
|
||||
.drop(1)
|
||||
.map { it.string }
|
||||
}
|
||||
@@ -180,7 +213,7 @@ internal fun ScrcpyAllOptionsPage(
|
||||
if (soBundle.cameraFacing.isEmpty()) {
|
||||
0
|
||||
} else {
|
||||
val idx = Shared.CameraFacing.entries
|
||||
val idx = CameraFacing.entries
|
||||
.indexOfFirst { it.string == soBundle.cameraFacing }
|
||||
if (idx > 0) idx else 0
|
||||
}
|
||||
@@ -192,24 +225,23 @@ internal fun ScrcpyAllOptionsPage(
|
||||
|
||||
val cameraSizes = scrcpy.listings.cameraSizes
|
||||
val cameraSizeDropdownItems = rememberSaveable(cameraSizes, listRefreshVersion) {
|
||||
listOf("自动", "自定义") + cameraSizes
|
||||
listOf("默认", "自定义") + cameraSizes
|
||||
}
|
||||
var cameraSizeDropdownIndex by rememberSaveable(
|
||||
val cameraSizeDropdownIndex = rememberSaveable(
|
||||
soBundle.cameraSize,
|
||||
soBundle.cameraSizeCustom,
|
||||
soBundle.cameraSizeUseCustom,
|
||||
cameraSizes,
|
||||
listRefreshVersion,
|
||||
) {
|
||||
mutableIntStateOf(
|
||||
when {
|
||||
soBundle.cameraSizeUseCustom -> 1 // "自定义"
|
||||
soBundle.cameraSize.isEmpty() -> 0 // "自动"
|
||||
soBundle.cameraSize in cameraSizes ->
|
||||
cameraSizes.indexOf(soBundle.cameraSize) + 2
|
||||
when {
|
||||
soBundle.cameraSizeUseCustom -> 1
|
||||
soBundle.cameraSize.isEmpty() -> 0
|
||||
soBundle.cameraSize in cameraSizes ->
|
||||
cameraSizes.indexOf(soBundle.cameraSize) + 2
|
||||
|
||||
else -> 0 // 默认自动
|
||||
}
|
||||
)
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
var cameraArInput by rememberSaveable(soBundle.cameraAr) {
|
||||
@@ -220,28 +252,19 @@ internal fun ScrcpyAllOptionsPage(
|
||||
ScrcpyPresets.CameraFps.indexOfOrNearest(soBundle.cameraFps)
|
||||
}
|
||||
|
||||
val audioSourceItems = rememberSaveable {
|
||||
Shared.AudioSource.entries.map { it.string }
|
||||
}
|
||||
val audioSourceIndex = rememberSaveable(soBundle.audioSource) {
|
||||
Shared.AudioSource.entries
|
||||
.indexOfFirst { it.string == soBundle.audioSource }
|
||||
.coerceAtLeast(0)
|
||||
val screenOffTimeoutPresetIndex = rememberSaveable(soBundle.screenOffTimeout) {
|
||||
ScrcpyPresets.ScreenOffTimeout.indexOfOrNearest(
|
||||
Tick(soBundle.screenOffTimeout).sec.toInt().coerceAtLeast(0)
|
||||
)
|
||||
}
|
||||
|
||||
val videoEncoderInfos = scrcpy.listings.videoEncoders
|
||||
val audioEncoderInfos = scrcpy.listings.audioEncoders
|
||||
val videoEncoders = remember(videoEncoderInfos) {
|
||||
videoEncoderInfos.map { it.id }
|
||||
val audioSourceItems = rememberSaveable {
|
||||
AudioSource.entries.map { it.string }
|
||||
}
|
||||
val audioEncoders = remember(audioEncoderInfos) {
|
||||
audioEncoderInfos.map { it.id }
|
||||
}
|
||||
val videoEncoderTypes = remember(videoEncoderInfos) {
|
||||
videoEncoderInfos.associate { it.id to it.type.s }
|
||||
}
|
||||
val audioEncoderTypes = remember(audioEncoderInfos) {
|
||||
audioEncoderInfos.associate { it.id to it.type.s }
|
||||
val audioSourceIndex = rememberSaveable(soBundle.audioSource) {
|
||||
AudioSource.entries
|
||||
.indexOfFirst { it.string == soBundle.audioSource }
|
||||
.coerceAtLeast(0)
|
||||
}
|
||||
|
||||
val maxSizePresetIndex = rememberSaveable(soBundle.maxSize) {
|
||||
@@ -260,46 +283,118 @@ internal fun ScrcpyAllOptionsPage(
|
||||
mutableStateOf(soBundle.audioCodecOptions)
|
||||
}
|
||||
|
||||
val videoEncoderDropdownItems = rememberSaveable(videoEncoders, listRefreshVersion) {
|
||||
listOf("") + videoEncoders
|
||||
}
|
||||
val videoEncoderIndex =
|
||||
rememberSaveable(soBundle.videoEncoder, videoEncoders, listRefreshVersion) {
|
||||
(videoEncoders.indexOf(soBundle.videoEncoder) + 1).coerceAtLeast(0)
|
||||
val videoEncoders = scrcpy.listings.videoEncoders
|
||||
val videoEncoderItems by remember(videoEncoders, listRefreshVersion) {
|
||||
derivedStateOf {
|
||||
buildList {
|
||||
add(SpinnerEntry(title = "自动"))
|
||||
videoEncoders.forEach { info ->
|
||||
add(
|
||||
SpinnerEntry(
|
||||
title = info.id,
|
||||
summary = info.type.s,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val videoEncoderIndex = rememberSaveable(
|
||||
soBundle.videoEncoder,
|
||||
videoEncoders,
|
||||
listRefreshVersion,
|
||||
) {
|
||||
(videoEncoders.indexOfFirst { it.id == soBundle.videoEncoder } + 1)
|
||||
.coerceAtLeast(0)
|
||||
}
|
||||
|
||||
val audioEncoderDropdownItems = rememberSaveable(audioEncoders, listRefreshVersion) {
|
||||
listOf("") + audioEncoders
|
||||
val audioEncoders = scrcpy.listings.audioEncoders
|
||||
val audioEncoderItems by remember(audioEncoders, listRefreshVersion) {
|
||||
derivedStateOf {
|
||||
buildList {
|
||||
add(SpinnerEntry(title = "自动"))
|
||||
audioEncoders.forEach { info ->
|
||||
add(
|
||||
SpinnerEntry(
|
||||
title = info.id,
|
||||
summary = info.type.s,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val audioEncoderIndex = rememberSaveable(
|
||||
soBundle.audioEncoder,
|
||||
audioEncoders,
|
||||
listRefreshVersion
|
||||
listRefreshVersion,
|
||||
) {
|
||||
(audioEncoders.indexOf(soBundle.audioEncoder) + 1).coerceAtLeast(0)
|
||||
(audioEncoders.indexOfFirst { it.id == soBundle.audioEncoder } + 1)
|
||||
.coerceAtLeast(0)
|
||||
}
|
||||
|
||||
val videoEncoderEntries = videoEncoderDropdownItems.map { encoderName ->
|
||||
if (encoderName == "") {
|
||||
SpinnerEntry(title = "自动")
|
||||
val displayImePolicyItems = rememberSaveable {
|
||||
listOf("默认") + DisplayImePolicy.entries
|
||||
.drop(1)
|
||||
.map { it.string }
|
||||
}
|
||||
val displayImePolicyIndex = rememberSaveable(soBundle.displayImePolicy) {
|
||||
if (soBundle.displayImePolicy.isEmpty()) {
|
||||
0
|
||||
} else {
|
||||
SpinnerEntry(
|
||||
title = encoderName,
|
||||
summary = videoEncoderTypes[encoderName],
|
||||
)
|
||||
val idx = DisplayImePolicy.entries
|
||||
.indexOfFirst { it.string == soBundle.displayImePolicy }
|
||||
if (idx > 0) idx else 0
|
||||
}
|
||||
}
|
||||
|
||||
val audioEncoderEntries = audioEncoderDropdownItems.map { encoderName ->
|
||||
if (encoderName == "") {
|
||||
SpinnerEntry(title = "自动")
|
||||
} else {
|
||||
SpinnerEntry(
|
||||
title = encoderName,
|
||||
summary = audioEncoderTypes[encoderName],
|
||||
)
|
||||
val apps = remember(scrcpy.listings.apps, listRefreshVersion) {
|
||||
scrcpy.listings.apps.sortedBy { it.packageName }
|
||||
}
|
||||
val appDropdownItems by remember(apps, listRefreshVersion) {
|
||||
derivedStateOf {
|
||||
buildList {
|
||||
add(SpinnerEntry(title = "无"))
|
||||
add(SpinnerEntry(title = "自定义"))
|
||||
apps.forEach { app ->
|
||||
add(
|
||||
SpinnerEntry(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (app.system) MiuixIcons.Tune
|
||||
else MiuixIcons.Store,
|
||||
contentDescription = app.label,
|
||||
modifier = Modifier.padding(end = UiSpacing.ContentVertical),
|
||||
)
|
||||
},
|
||||
title = app.label,
|
||||
summary = app.packageName,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val appDropdownIndex = rememberSaveable(
|
||||
soBundle.startApp,
|
||||
soBundle.startAppCustom,
|
||||
soBundle.startAppUseCustom,
|
||||
apps,
|
||||
listRefreshVersion,
|
||||
) {
|
||||
when {
|
||||
soBundle.startAppUseCustom -> 1
|
||||
soBundle.startApp.isEmpty() -> 0
|
||||
apps.any { it.packageName == soBundle.startApp } ->
|
||||
apps.indexOfFirst { it.packageName == soBundle.startApp } + 2
|
||||
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
var startAppCustomInput by rememberSaveable(soBundle.startAppCustom) {
|
||||
mutableStateOf(soBundle.startAppCustom)
|
||||
}
|
||||
|
||||
// [<width>x<height>][/<dpi>]
|
||||
val (ndWidth, ndHeight, ndDpi) = remember(soBundle.newDisplay) {
|
||||
@@ -332,6 +427,13 @@ internal fun ScrcpyAllOptionsPage(
|
||||
mutableStateOf(cY?.toString() ?: "")
|
||||
}
|
||||
|
||||
val logLevelItems = rememberSaveable { LogLevel.entries.map { it.string } }
|
||||
val logLevelIndex = rememberSaveable(soBundle.logLevel) {
|
||||
LogLevel.entries
|
||||
.indexOfFirst { it.string == soBundle.logLevel }
|
||||
.coerceAtLeast(0)
|
||||
}
|
||||
|
||||
var serverParamsPreview by rememberSaveable { mutableStateOf("") }
|
||||
// 监听选项变化, 自动更新预览
|
||||
LaunchedEffect(soBundle) {
|
||||
@@ -351,7 +453,7 @@ internal fun ScrcpyAllOptionsPage(
|
||||
|
||||
serverParamsPreview = clientOptions
|
||||
.toServerParams(0u)
|
||||
.toList(simplify = true)
|
||||
.toList(preview = true)
|
||||
// improve readability using hard line breaks
|
||||
.joinToString("\n")
|
||||
}
|
||||
@@ -374,19 +476,19 @@ internal fun ScrcpyAllOptionsPage(
|
||||
item {
|
||||
Card {
|
||||
SwitchPreference(
|
||||
title = "启动后关闭屏幕",
|
||||
title = "scrcpy 启动后熄灭设备屏幕",
|
||||
summary = "--turn-screen-off",
|
||||
checked = soBundle.turnScreenOff,
|
||||
onCheckedChange = {
|
||||
soBundle = soBundle.copy(
|
||||
turnScreenOff = it
|
||||
)
|
||||
if (it) {
|
||||
if (it) snackbar.show(
|
||||
// github.com/Genymobile/scrcpy/issues/3376
|
||||
// github.com/Genymobile/scrcpy/issues/4587
|
||||
// github.com/Genymobile/scrcpy/issues/5676
|
||||
snackbar.show("注意:大部分设备在关闭屏幕后刷新率会降低/减半")
|
||||
}
|
||||
"注意:大部分设备在关闭屏幕后刷新率会降低/减半"
|
||||
)
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
@@ -424,38 +526,45 @@ internal fun ScrcpyAllOptionsPage(
|
||||
// enabled = control || video,
|
||||
)
|
||||
SuperSlider(
|
||||
title = "投屏过程中受控机的息屏时间",
|
||||
title = "scrcpy 启动后受控机的息屏时间",
|
||||
summary = "--screen-off-timeout",
|
||||
value = soBundle.screenOffTimeout.coerceAtLeast(0).toFloat(),
|
||||
onValueChange = {
|
||||
value = screenOffTimeoutPresetIndex.toFloat(),
|
||||
onValueChange = { value ->
|
||||
val idx = value.roundToInt()
|
||||
.coerceIn(0, ScrcpyPresets.ScreenOffTimeout.lastIndex)
|
||||
soBundle = soBundle.copy(
|
||||
screenOffTimeout = it.roundToInt()
|
||||
.takeIf { value -> value > 0 }
|
||||
?.toLong() ?: -1
|
||||
screenOffTimeout = ScrcpyPresets.ScreenOffTimeout[idx]
|
||||
.takeIf { it > 0 }
|
||||
?.toLong()
|
||||
?.let(Tick::fromSec)
|
||||
?.value
|
||||
?: -1
|
||||
)
|
||||
},
|
||||
valueRange = 0f..600f,
|
||||
steps = 600 - 1,
|
||||
valueRange = 0f..ScrcpyPresets.ScreenOffTimeout.lastIndex.toFloat(),
|
||||
steps = (ScrcpyPresets.ScreenOffTimeout.size - 2).coerceAtLeast(0),
|
||||
unit = "s",
|
||||
zeroStateText = "默认",
|
||||
displayText =
|
||||
if (soBundle.screenOffTimeout <= 0) "默认"
|
||||
else soBundle.screenOffTimeout.toString(),
|
||||
zeroStateText = "不修改",
|
||||
showKeyPoints = true,
|
||||
keyPoints = ScrcpyPresets.ScreenOffTimeout.indices.map { it.toFloat() },
|
||||
displayText = Tick(soBundle.screenOffTimeout).sec.toString(),
|
||||
inputInitialValue =
|
||||
if (soBundle.screenOffTimeout <= 0) ""
|
||||
else soBundle.screenOffTimeout.toString(),
|
||||
else Tick(soBundle.screenOffTimeout).sec.toString(),
|
||||
inputFilter = { it.filter(Char::isDigit) },
|
||||
inputValueRange = 0f..86400f,
|
||||
onInputConfirm = {
|
||||
soBundle = soBundle.copy(
|
||||
screenOffTimeout = it.toLongOrNull()
|
||||
?.takeIf { value -> value > 0 }
|
||||
?.let(Tick::fromSec)
|
||||
?.value
|
||||
?: -1
|
||||
)
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
title = "开始投屏时不唤醒屏幕",
|
||||
title = "scrcpy 启动时不唤醒屏幕",
|
||||
summary = "--no-power-on",
|
||||
checked = !soBundle.powerOn,
|
||||
onCheckedChange = {
|
||||
@@ -465,7 +574,7 @@ internal fun ScrcpyAllOptionsPage(
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
title = "结束投屏时息屏",
|
||||
title = "scrcpy 结束后息屏",
|
||||
summary = "--power-off-on-close",
|
||||
checked = soBundle.powerOffOnClose,
|
||||
onCheckedChange = {
|
||||
@@ -474,6 +583,16 @@ internal fun ScrcpyAllOptionsPage(
|
||||
)
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
title = "scrcpy 启动后保持设备唤醒状态(插入电源)",
|
||||
summary = "--stay-awake",
|
||||
checked = soBundle.stayAwake,
|
||||
onCheckedChange = {
|
||||
soBundle = soBundle.copy(
|
||||
stayAwake = it
|
||||
)
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
title = "显示物理触控",
|
||||
summary = "--show-touches",
|
||||
@@ -485,15 +604,42 @@ internal fun ScrcpyAllOptionsPage(
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
title = "投屏时保持本机屏幕唤醒",
|
||||
title = "启动后进入全屏",
|
||||
summary = "--fullscreen",
|
||||
checked = soBundle.fullscreen,
|
||||
onCheckedChange = {
|
||||
soBundle = soBundle.copy(
|
||||
fullscreen = it
|
||||
)
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
title = "scrcpy 启动后保持本机屏幕唤醒",
|
||||
summary = "--disable-screensaver",
|
||||
checked = soBundle.disableScreensaver,
|
||||
onCheckedChange = {
|
||||
soBundle = soBundle.copy(
|
||||
disableScreensaver = it
|
||||
)
|
||||
if (it) snackbar.show(
|
||||
"不保证可用"
|
||||
)
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
title = "scrcpy 结束时断开 adb",
|
||||
summary = "--kill-adb-on-close",
|
||||
checked = soBundle.killAdbOnClose,
|
||||
onCheckedChange = {
|
||||
soBundle = soBundle.copy(
|
||||
killAdbOnClose = it
|
||||
)
|
||||
if (it) snackbar.show(
|
||||
"未实现"
|
||||
)
|
||||
},
|
||||
enabled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,26 +756,46 @@ internal fun ScrcpyAllOptionsPage(
|
||||
selectedIndex = videoSourceIndex,
|
||||
onSelectedIndexChange = {
|
||||
soBundle = soBundle.copy(
|
||||
videoSource = Shared.VideoSource.entries[it].string
|
||||
videoSource = VideoSource.entries[it].string
|
||||
)
|
||||
},
|
||||
)
|
||||
AnimatedVisibility(soBundle.videoSource == "display") {
|
||||
Column {
|
||||
SuperTextField(
|
||||
value = displayIdInput,
|
||||
onValueChange = { displayIdInput = it },
|
||||
onFocusLost = {
|
||||
ArrowPreference(
|
||||
title = "获取 Displays",
|
||||
summary = "--list-displays",
|
||||
onClick = {
|
||||
if (refreshBusy) return@ArrowPreference
|
||||
scope.launch {
|
||||
refreshBusy = true
|
||||
snackbar.show("获取中")
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
scrcpy.listings.getDisplays(forceRefresh = true)
|
||||
}
|
||||
listRefreshVersion += 1
|
||||
snackbar.show("获取成功")
|
||||
} catch (e: Exception) {
|
||||
snackbar.show("刷新失败: ${e.message}")
|
||||
} finally {
|
||||
refreshBusy = false
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
OverlayDropdownPreference(
|
||||
title = "监视器 ID",
|
||||
summary = "--display-id",
|
||||
items = displayDropdownItems,
|
||||
selectedIndex = displayDropdownIndex,
|
||||
onSelectedIndexChange = {
|
||||
soBundle = soBundle.copy(
|
||||
displayId = displayIdInput.toIntOrNull() ?: -1
|
||||
displayId =
|
||||
if (it == 0) -1
|
||||
else displays[it - 1].id
|
||||
)
|
||||
},
|
||||
label = "--display-id",
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(all = UiSpacing.Large),
|
||||
)
|
||||
SuperSlider(
|
||||
title = "最大分辨率",
|
||||
@@ -694,19 +860,40 @@ internal fun ScrcpyAllOptionsPage(
|
||||
}
|
||||
AnimatedVisibility(soBundle.videoSource == "camera") {
|
||||
Column {
|
||||
SuperTextField(
|
||||
value = cameraIdInput,
|
||||
onValueChange = { cameraIdInput = it },
|
||||
onFocusLost = {
|
||||
ArrowPreference(
|
||||
title = "获取 Cameras",
|
||||
summary = "--list-cameras",
|
||||
onClick = {
|
||||
if (refreshBusy) return@ArrowPreference
|
||||
scope.launch {
|
||||
refreshBusy = true
|
||||
snackbar.show("获取中")
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
scrcpy.listings.getCameras(forceRefresh = true)
|
||||
}
|
||||
listRefreshVersion += 1
|
||||
snackbar.show("获取成功")
|
||||
} catch (e: Exception) {
|
||||
snackbar.show("刷新失败: ${e.message}")
|
||||
} finally {
|
||||
refreshBusy = false
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
OverlayDropdownPreference(
|
||||
title = "摄像头 ID",
|
||||
summary = "--camera-id",
|
||||
items = cameraDropdownItems,
|
||||
selectedIndex = cameraDropdownIndex,
|
||||
onSelectedIndexChange = {
|
||||
soBundle = soBundle.copy(
|
||||
cameraId = cameraIdInput
|
||||
cameraId =
|
||||
if (it == 0) ""
|
||||
else cameras[it - 1].id
|
||||
)
|
||||
},
|
||||
label = "--camera-id",
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(all = UiSpacing.Large),
|
||||
)
|
||||
OverlayDropdownPreference(
|
||||
title = "摄像头朝向",
|
||||
@@ -717,7 +904,7 @@ internal fun ScrcpyAllOptionsPage(
|
||||
soBundle = soBundle.copy(
|
||||
cameraFacing =
|
||||
if (it == 0) ""
|
||||
else Shared.CameraFacing.entries[it].string
|
||||
else CameraFacing.entries[it].string
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -749,7 +936,6 @@ internal fun ScrcpyAllOptionsPage(
|
||||
items = cameraSizeDropdownItems,
|
||||
selectedIndex = cameraSizeDropdownIndex,
|
||||
onSelectedIndexChange = {
|
||||
cameraSizeDropdownIndex = it
|
||||
when (it) {
|
||||
0 -> {
|
||||
// "自动"
|
||||
@@ -765,7 +951,7 @@ internal fun ScrcpyAllOptionsPage(
|
||||
soBundle = soBundle.copy(
|
||||
cameraSizeUseCustom = true
|
||||
)
|
||||
cameraSizeCustomInput = soBundle.cameraSize
|
||||
cameraSizeCustomInput = ""
|
||||
}
|
||||
|
||||
else -> {
|
||||
@@ -785,15 +971,14 @@ internal fun ScrcpyAllOptionsPage(
|
||||
value = cameraSizeCustomInput,
|
||||
onValueChange = { cameraSizeCustomInput = it },
|
||||
onFocusLost = {
|
||||
if (cameraSizeCustomInput in cameraSizes) {
|
||||
soBundle = if (cameraSizeCustomInput in cameraSizes) {
|
||||
// 输入的值存在于列表中, 取消自定义输入
|
||||
cameraSizeDropdownIndex = cameraSizes
|
||||
.indexOf(cameraSizeCustomInput) + 2
|
||||
soBundle = soBundle.copy(
|
||||
soBundle.copy(
|
||||
cameraSize = cameraSizeCustomInput,
|
||||
cameraSizeUseCustom = false
|
||||
)
|
||||
} else {
|
||||
soBundle = soBundle.copy(
|
||||
soBundle.copy(
|
||||
cameraSizeCustom = cameraSizeCustomInput
|
||||
)
|
||||
}
|
||||
@@ -859,26 +1044,6 @@ internal fun ScrcpyAllOptionsPage(
|
||||
)
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
title = "关闭虚拟显示器时保留内容",
|
||||
summary = "--no-vd-destroy-content",
|
||||
checked = !soBundle.vdDestroyContent,
|
||||
onCheckedChange = {
|
||||
soBundle = soBundle.copy(
|
||||
vdDestroyContent = !it
|
||||
)
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
title = "禁用虚拟显示器系统装饰",
|
||||
summary = "--no-vd-system-decorations",
|
||||
checked = !soBundle.vdSystemDecorations,
|
||||
onCheckedChange = {
|
||||
soBundle = soBundle.copy(
|
||||
vdSystemDecorations = !it
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -894,7 +1059,7 @@ internal fun ScrcpyAllOptionsPage(
|
||||
selectedIndex = audioSourceIndex,
|
||||
onSelectedIndexChange = {
|
||||
soBundle = soBundle.copy(
|
||||
audioSource = Shared.AudioSource.entries[it].string
|
||||
audioSource = AudioSource.entries[it].string
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -943,7 +1108,7 @@ internal fun ScrcpyAllOptionsPage(
|
||||
snackbar.show("获取中")
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
scrcpy.listings.getVideoEncoders(forceRefresh = true)
|
||||
scrcpy.listings.getEncoders(forceRefresh = true)
|
||||
}
|
||||
listRefreshVersion += 1
|
||||
snackbar.show("获取成功")
|
||||
@@ -959,11 +1124,13 @@ internal fun ScrcpyAllOptionsPage(
|
||||
OverlaySpinnerPreference(
|
||||
title = "视频编码器",
|
||||
summary = "--video-encoder",
|
||||
items = videoEncoderEntries,
|
||||
items = videoEncoderItems,
|
||||
selectedIndex = videoEncoderIndex,
|
||||
onSelectedIndexChange = {
|
||||
soBundle = soBundle.copy(
|
||||
videoEncoder = videoEncoderEntries[it].title ?: ""
|
||||
videoEncoder =
|
||||
if (it == 0) ""
|
||||
else videoEncoders[it - 1].id
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -984,11 +1151,13 @@ internal fun ScrcpyAllOptionsPage(
|
||||
OverlaySpinnerPreference(
|
||||
title = "音频编码器",
|
||||
summary = "--audio-encoder",
|
||||
items = audioEncoderEntries,
|
||||
items = audioEncoderItems,
|
||||
selectedIndex = audioEncoderIndex,
|
||||
onSelectedIndexChange = {
|
||||
soBundle = soBundle.copy(
|
||||
audioEncoder = audioEncoderEntries[it].title ?: ""
|
||||
audioEncoder =
|
||||
if (it == 0) ""
|
||||
else audioEncoders[it - 1].id
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -1009,6 +1178,160 @@ internal fun ScrcpyAllOptionsPage(
|
||||
}
|
||||
}
|
||||
|
||||
if (soBundle.videoSource == "display") item {
|
||||
Card {
|
||||
OverlayDropdownPreference(
|
||||
title = "输入法显示策略",
|
||||
summary = "--display-ime-policy",
|
||||
items = displayImePolicyItems,
|
||||
selectedIndex = displayImePolicyIndex,
|
||||
onSelectedIndexChange = {
|
||||
soBundle = soBundle.copy(
|
||||
displayImePolicy =
|
||||
if (it == 0) ""
|
||||
else DisplayImePolicy.entries[it].string
|
||||
)
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
title = "关闭虚拟显示器时保留内容",
|
||||
summary = "--no-vd-destroy-content",
|
||||
checked = !soBundle.vdDestroyContent,
|
||||
onCheckedChange = {
|
||||
soBundle = soBundle.copy(
|
||||
vdDestroyContent = !it
|
||||
)
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
title = "禁用虚拟显示器系统装饰",
|
||||
summary = "--no-vd-system-decorations",
|
||||
checked = !soBundle.vdSystemDecorations,
|
||||
onCheckedChange = {
|
||||
soBundle = soBundle.copy(
|
||||
vdSystemDecorations = !it
|
||||
)
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
title = "禁用结束后清理",
|
||||
summary = "--no-downsize-on-error",
|
||||
checked = !soBundle.downsizeOnError,
|
||||
onCheckedChange = {
|
||||
soBundle = soBundle.copy(
|
||||
downsizeOnError = !it
|
||||
)
|
||||
if (it) snackbar.show(
|
||||
"默认情况下,在 MediaCodec 出错时," +
|
||||
"scrcpy 会自动尝试使用更低的分辨率重新开始。" +
|
||||
"\n此选项将禁用此行为。"
|
||||
)
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
title = "禁用结束后清理",
|
||||
summary = "--no-cleanup",
|
||||
checked = !soBundle.cleanup,
|
||||
onCheckedChange = {
|
||||
soBundle = soBundle.copy(
|
||||
cleanup = !it
|
||||
)
|
||||
if (it) snackbar.show(
|
||||
"默认情况下,scrcpy 会从设备中移除服务器二进制文件," +
|
||||
"并在退出时恢复设备状态(显示触摸、保持唤醒和电源模式)" +
|
||||
"\n此选项将禁用此清理操作"
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (soBundle.videoSource == "display") item {
|
||||
Card {
|
||||
ArrowPreference(
|
||||
title = "获取应用列表",
|
||||
summary = "--list-apps",
|
||||
onClick = {
|
||||
if (refreshBusy) return@ArrowPreference
|
||||
scope.launch {
|
||||
refreshBusy = true
|
||||
snackbar.show("获取中")
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
scrcpy.listings.getApps(forceRefresh = true)
|
||||
}
|
||||
listRefreshVersion += 1
|
||||
snackbar.show("获取成功")
|
||||
} catch (e: Exception) {
|
||||
snackbar.show("刷新失败: ${e.message}")
|
||||
} finally {
|
||||
refreshBusy = false
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
OverlaySpinnerPreference(
|
||||
title = "scrcpy 启动后打开应用",
|
||||
summary = "--start-app\nTODO: 未实现虚拟屏配合",
|
||||
items = appDropdownItems,
|
||||
selectedIndex = appDropdownIndex,
|
||||
onSelectedIndexChange = {
|
||||
when (it) {
|
||||
0 -> {
|
||||
soBundle = soBundle.copy(
|
||||
startApp = "",
|
||||
startAppUseCustom = false,
|
||||
)
|
||||
startAppCustomInput = ""
|
||||
}
|
||||
|
||||
1 -> {
|
||||
soBundle = soBundle.copy(
|
||||
startAppUseCustom = true
|
||||
)
|
||||
startAppCustomInput = ""
|
||||
}
|
||||
|
||||
else -> {
|
||||
val selectedApp = apps[it - 2]
|
||||
soBundle = soBundle.copy(
|
||||
startApp = selectedApp.packageName,
|
||||
startAppUseCustom = false,
|
||||
)
|
||||
startAppCustomInput = ""
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
AnimatedVisibility(soBundle.startAppUseCustom) {
|
||||
SuperTextField(
|
||||
value = startAppCustomInput,
|
||||
onValueChange = { startAppCustomInput = it },
|
||||
onFocusLost = {
|
||||
val matchedAppIndex = apps
|
||||
.indexOfFirst { it.packageName == startAppCustomInput }
|
||||
soBundle = if (matchedAppIndex >= 0) {
|
||||
soBundle.copy(
|
||||
startApp = apps[matchedAppIndex].packageName,
|
||||
startAppUseCustom = false,
|
||||
)
|
||||
} else {
|
||||
soBundle.copy(
|
||||
startAppCustom = startAppCustomInput
|
||||
)
|
||||
}
|
||||
},
|
||||
label = "--start-app",
|
||||
useLabelAsPlaceholder = true,
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(all = UiSpacing.Large),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (soBundle.videoSource == "display") item {
|
||||
Card {
|
||||
Column(
|
||||
@@ -1243,6 +1566,22 @@ internal fun ScrcpyAllOptionsPage(
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Card {
|
||||
OverlayDropdownPreference(
|
||||
title = "日志等级",
|
||||
summary = "--verbosity",
|
||||
items = logLevelItems,
|
||||
selectedIndex = logLevelIndex,
|
||||
onSelectedIndexChange = {
|
||||
soBundle = soBundle.copy(
|
||||
logLevel = LogLevel.entries[it].string
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item { Spacer(Modifier.height(UiSpacing.PageBottom)) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ data class ClientOptions(
|
||||
// var clipboardAutosync: Boolean = true, // to server
|
||||
|
||||
// --no-downsize-on-error
|
||||
// var downsizeOnError: Boolean = true, // to server
|
||||
var downsizeOnError: Boolean = true, // to server
|
||||
|
||||
// var tcpip: Boolean, // to server
|
||||
// var tcpipDst: String = "", // to server
|
||||
@@ -172,7 +172,7 @@ data class ClientOptions(
|
||||
// var selectTcpip: Boolean, // to server
|
||||
|
||||
// --no-cleanup
|
||||
var cleanUp: Boolean = true, // to server
|
||||
var cleanup: Boolean = true, // to server
|
||||
|
||||
// var startFpsCounter: Boolean,
|
||||
|
||||
@@ -226,6 +226,12 @@ data class ClientOptions(
|
||||
M4A, MKA, OPUS, AAC, FLAC, WAV -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String) =
|
||||
entries.find { it.string.equals(value, ignoreCase = true) }
|
||||
?: AUTO
|
||||
}
|
||||
}
|
||||
|
||||
fun fix(): ClientOptions {
|
||||
@@ -552,7 +558,9 @@ data class ClientOptions(
|
||||
|
||||
powerOffOnClose = powerOffOnClose,
|
||||
|
||||
cleanUp = cleanUp,
|
||||
downsizeOnError = downsizeOnError,
|
||||
|
||||
cleanUp = cleanup,
|
||||
powerOn = powerOn,
|
||||
|
||||
// killAdbOnClose == killAdbOnClose, // client side
|
||||
|
||||
@@ -344,7 +344,7 @@ class Scrcpy(
|
||||
return getEncoders(forceRefresh).second
|
||||
}
|
||||
|
||||
private suspend fun getEncoders(forceRefresh: Boolean = false)
|
||||
suspend fun getEncoders(forceRefresh: Boolean = false)
|
||||
: Pair<List<EncoderInfo>, List<EncoderInfo>> {
|
||||
if (!forceRefresh && cachedVideoEncoders != null && cachedAudioEncoders != null)
|
||||
return cachedVideoEncoders.orEmpty() to cachedAudioEncoders.orEmpty()
|
||||
@@ -455,7 +455,7 @@ class Scrcpy(
|
||||
video = false,
|
||||
audio = false,
|
||||
control = false,
|
||||
cleanUp = false,
|
||||
cleanup = false,
|
||||
list = list,
|
||||
)
|
||||
val serverParams = options.toServerParams(scid)
|
||||
@@ -1065,6 +1065,7 @@ class Scrcpy(
|
||||
}
|
||||
serverLogBuffer.addLast(line)
|
||||
}
|
||||
logEvent(line)
|
||||
Log.i(TAG, "[server:$socketName] $line")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ data class ServerParams(
|
||||
|
||||
var powerOffOnClose: Boolean,
|
||||
|
||||
// var downsizeOnError: Boolean,
|
||||
var downsizeOnError: Boolean,
|
||||
// var tcpip: Boolean,
|
||||
// var tcpipDst: String,
|
||||
// var selectUsb: Boolean,
|
||||
@@ -107,13 +107,12 @@ data class ServerParams(
|
||||
return (extraArgs.toList() + this.toList()).joinToString(SEPARATOR)
|
||||
}
|
||||
|
||||
fun toList(simplify: Boolean = false): MutableList<String> {
|
||||
fun toList(preview: Boolean = false): MutableList<String> {
|
||||
val cmd = mutableListOf<String>()
|
||||
|
||||
if (!simplify) {
|
||||
cmd.add("scid=${scid.toString(16)}")
|
||||
cmd.add("log_level=${logLevel.string}")
|
||||
}
|
||||
if (!preview) cmd.add("scid=${scid.toString(16)}")
|
||||
|
||||
cmd.add("log_level=${logLevel.string}")
|
||||
|
||||
if (!video) {
|
||||
cmd.add("video=false")
|
||||
@@ -216,7 +215,7 @@ data class ServerParams(
|
||||
require(screenOffTimeout >= 0) {
|
||||
"screen_off_timeout must be >= 0"
|
||||
}
|
||||
cmd.add("screen_off_timeout=${screenOffTimeout.toMs()}")
|
||||
cmd.add("screen_off_timeout=${screenOffTimeout.ms}")
|
||||
}
|
||||
if (videoCodecOptions.isNotBlank()) {
|
||||
validate(videoCodecOptions)
|
||||
@@ -242,8 +241,6 @@ data class ServerParams(
|
||||
if (!clipBoardAutosync) {
|
||||
cmd.add("clipboard_autosync=false")
|
||||
}
|
||||
// not implemented
|
||||
val downsizeOnError = false
|
||||
if (!downsizeOnError) {
|
||||
cmd.add("downsize_on_error=false")
|
||||
}
|
||||
@@ -285,4 +282,4 @@ data class ServerParams(
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@ class Shared {
|
||||
fun toMs(): Long = value / 1000
|
||||
fun toSec(): Long = value / 1_000_000
|
||||
fun toSecDouble(): Double = value / 1_000_000.0
|
||||
val ns: Long get() = toNs()
|
||||
val us: Long get() = toUs()
|
||||
val ms: Long get() = toMs()
|
||||
val sec: Long get() = toSec()
|
||||
val secDouble: Double get() = toSecDouble()
|
||||
|
||||
operator fun plus(other: Tick): Tick = Tick(value + other.value)
|
||||
operator fun minus(other: Tick): Tick = Tick(value - other.value)
|
||||
@@ -38,17 +43,23 @@ class Shared {
|
||||
fun Duration.toTick(): Tick = Tick(this.inWholeMicroseconds)
|
||||
}
|
||||
|
||||
enum class ListOptions(val value: Int) {
|
||||
NULL(0x0),
|
||||
ENCODERS(0x1), // --list-encoders
|
||||
DISPLAYS(0x2), // --list-displays
|
||||
CAMERAS(0x4), // --list-cameras
|
||||
CAMERA_SIZES(0x8), // --list-camera-sizes
|
||||
APPS(0x10); // --list-apps
|
||||
enum class ListOptions(val string: String, val value: Int) {
|
||||
NULL("null", 0x0),
|
||||
ENCODERS("encoders", 0x1), // --list-encoders
|
||||
DISPLAYS("displays", 0x2), // --list-displays
|
||||
CAMERAS("cameras", 0x4), // --list-cameras
|
||||
CAMERA_SIZES("camera_sizes", 0x8), // --list-camera-sizes
|
||||
APPS("apps", 0x10); // --list-apps
|
||||
|
||||
infix fun or(other: ListOptions) = value or other.value
|
||||
infix fun and(other: ListOptions) = value and other.value
|
||||
infix fun has(other: ListOptions) = this and other != 0
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String) =
|
||||
entries.find { it.string.equals(value, ignoreCase = true) }
|
||||
?: NULL
|
||||
}
|
||||
}
|
||||
|
||||
enum class EncoderType(val s: String) {
|
||||
@@ -62,6 +73,12 @@ class Shared {
|
||||
INFO("info"),
|
||||
WARN("warn"),
|
||||
ERROR("error");
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String) =
|
||||
entries.find { it.string.equals(value, ignoreCase = true) }
|
||||
?: VERBOSE
|
||||
}
|
||||
}
|
||||
|
||||
enum class Codec(
|
||||
@@ -177,6 +194,12 @@ class Shared {
|
||||
UNLOCKED("unlocked"), // ignore
|
||||
LOCKED_VALUE("locked_value"), // "@${orientation.string}"
|
||||
LOCKED_INITIAL("locked_initial"); // "@"
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String) =
|
||||
entries.find { it.string.equals(value, ignoreCase = true) }
|
||||
?: UNLOCKED
|
||||
}
|
||||
}
|
||||
|
||||
enum class DisplayImePolicy(val string: String) {
|
||||
@@ -184,5 +207,11 @@ class Shared {
|
||||
LOCAL("local"),
|
||||
FALLBACK("fallback"),
|
||||
HIDE("hide");
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String) =
|
||||
entries.find { it.string.equals(value, ignoreCase = true) }
|
||||
?: UNDEFINED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ 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.ClientOptions.RecordFormat
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.AudioSource
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.CameraFacing
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec
|
||||
@@ -74,11 +75,11 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
||||
)
|
||||
val LOG_LEVEL = Pair(
|
||||
stringPreferencesKey("log_level"),
|
||||
"info",
|
||||
LogLevel.INFO.string,
|
||||
)
|
||||
val VIDEO_CODEC = Pair(
|
||||
stringPreferencesKey("video_codec"),
|
||||
"h264",
|
||||
Codec.H264.string,
|
||||
)
|
||||
val AUDIO_CODEC = Pair(
|
||||
stringPreferencesKey("audio_codec"),
|
||||
@@ -138,7 +139,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
||||
)
|
||||
val DISPLAY_IME_POLICY = Pair(
|
||||
stringPreferencesKey("display_ime_policy"),
|
||||
"undefined",
|
||||
DisplayImePolicy.UNDEFINED.string,
|
||||
)
|
||||
val DISPLAY_ID = Pair(
|
||||
intPreferencesKey("display_id"),
|
||||
@@ -184,6 +185,10 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
||||
booleanPreferencesKey("power_off_on_close"),
|
||||
false,
|
||||
)
|
||||
val DOWNSIZE_ON_ERROR = Pair(
|
||||
booleanPreferencesKey("downsize_on_error"),
|
||||
true,
|
||||
)
|
||||
val CLEANUP = Pair(
|
||||
booleanPreferencesKey("cleanup"),
|
||||
true,
|
||||
@@ -228,6 +233,14 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
||||
stringPreferencesKey("start_app"),
|
||||
"",
|
||||
)
|
||||
val START_APP_CUSTOM = Pair(
|
||||
stringPreferencesKey("start_app_custom"),
|
||||
"",
|
||||
)
|
||||
val START_APP_USE_CUSTOM = Pair(
|
||||
booleanPreferencesKey("start_app_use_custom"),
|
||||
false,
|
||||
)
|
||||
val VD_DESTROY_CONTENT = Pair(
|
||||
booleanPreferencesKey("vd_destroy_content"),
|
||||
true,
|
||||
@@ -334,6 +347,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
||||
val stayAwake: Boolean,
|
||||
val disableScreensaver: Boolean,
|
||||
val powerOffOnClose: Boolean,
|
||||
val downsizeOnError: Boolean,
|
||||
val cleanup: Boolean,
|
||||
val powerOn: Boolean,
|
||||
val video: Boolean,
|
||||
@@ -345,6 +359,8 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
||||
val audioDup: Boolean,
|
||||
val newDisplay: String,
|
||||
val startApp: String,
|
||||
val startAppCustom: String,
|
||||
val startAppUseCustom: Boolean,
|
||||
val vdDestroyContent: Boolean,
|
||||
val vdSystemDecorations: Boolean
|
||||
) : Parcelable {
|
||||
@@ -449,6 +465,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
||||
stayAwake = preferences.read(STAY_AWAKE),
|
||||
disableScreensaver = preferences.read(DISABLE_SCREENSAVER),
|
||||
powerOffOnClose = preferences.read(POWER_OFF_ON_CLOSE),
|
||||
downsizeOnError = preferences.read(DOWNSIZE_ON_ERROR),
|
||||
cleanup = preferences.read(CLEANUP),
|
||||
powerOn = preferences.read(POWER_ON),
|
||||
video = preferences.read(VIDEO),
|
||||
@@ -460,6 +477,8 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
||||
audioDup = preferences.read(AUDIO_DUP),
|
||||
newDisplay = preferences.read(NEW_DISPLAY),
|
||||
startApp = preferences.read(START_APP),
|
||||
startAppCustom = preferences.read(START_APP_CUSTOM),
|
||||
startAppUseCustom = preferences.read(START_APP_USE_CUSTOM),
|
||||
vdDestroyContent = preferences.read(VD_DESTROY_CONTENT),
|
||||
vdSystemDecorations = preferences.read(VD_SYSTEM_DECORATIONS),
|
||||
)
|
||||
@@ -493,12 +512,12 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
||||
cameraSize = if (!bundle.cameraSizeUseCustom) bundle.cameraSize else bundle.cameraSizeCustom,
|
||||
cameraAr = bundle.cameraAr,
|
||||
cameraFps = bundle.cameraFps.toUShort(),
|
||||
logLevel = LogLevel.valueOf(bundle.logLevel.uppercase()),
|
||||
logLevel = LogLevel.fromString(bundle.logLevel),
|
||||
videoCodec = Codec.fromString(bundle.videoCodec),
|
||||
audioCodec = Codec.fromString(bundle.audioCodec),
|
||||
videoSource = VideoSource.fromString(bundle.videoSource),
|
||||
audioSource = AudioSource.fromString(bundle.audioSource),
|
||||
recordFormat = ClientOptions.RecordFormat.valueOf(bundle.recordFormat.uppercase()),
|
||||
recordFormat = RecordFormat.fromString(bundle.recordFormat),
|
||||
cameraFacing = CameraFacing.fromString(bundle.cameraFacing),
|
||||
maxSize = bundle.maxSize.toUShort(),
|
||||
videoBitRate = bundle.videoBitRate,
|
||||
@@ -506,12 +525,10 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
||||
maxFps = bundle.maxFps,
|
||||
angle = bundle.angle,
|
||||
captureOrientation = Orientation.fromInt(bundle.captureOrientation),
|
||||
captureOrientationLock = OrientationLock.valueOf(
|
||||
bundle.captureOrientationLock.uppercase()
|
||||
),
|
||||
captureOrientationLock = OrientationLock.fromString(bundle.captureOrientationLock),
|
||||
displayOrientation = Orientation.fromInt(bundle.displayOrientation),
|
||||
recordOrientation = Orientation.fromInt(bundle.recordOrientation),
|
||||
displayImePolicy = DisplayImePolicy.valueOf(bundle.displayImePolicy.uppercase()),
|
||||
displayImePolicy = DisplayImePolicy.fromString(bundle.displayImePolicy),
|
||||
displayId = bundle.displayId,
|
||||
screenOffTimeout = Tick(bundle.screenOffTimeout),
|
||||
showTouches = bundle.showTouches,
|
||||
@@ -523,17 +540,18 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
||||
stayAwake = bundle.stayAwake,
|
||||
disableScreensaver = bundle.disableScreensaver,
|
||||
powerOffOnClose = bundle.powerOffOnClose,
|
||||
cleanUp = bundle.cleanup,
|
||||
downsizeOnError = bundle.downsizeOnError,
|
||||
cleanup = bundle.cleanup,
|
||||
powerOn = bundle.powerOn,
|
||||
video = bundle.video,
|
||||
audio = bundle.audio,
|
||||
requireAudio = bundle.requireAudio,
|
||||
killAdbOnClose = bundle.killAdbOnClose,
|
||||
cameraHighSpeed = bundle.cameraHighSpeed,
|
||||
list = ListOptions.valueOf(bundle.list.uppercase()),
|
||||
list = ListOptions.fromString(bundle.list),
|
||||
audioDup = bundle.audioDup,
|
||||
newDisplay = bundle.newDisplay,
|
||||
startApp = bundle.startApp,
|
||||
startApp = if (!bundle.startAppUseCustom) bundle.startApp else bundle.startAppCustom,
|
||||
vdDestroyContent = bundle.vdDestroyContent,
|
||||
vdSystemDecorations = bundle.vdSystemDecorations
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user