refactor: organize codes; add README.md

This commit is contained in:
Miuzarte
2026-03-19 12:25:53 +08:00
parent c0c4ae1d23
commit 71e942b23e
25 changed files with 1696 additions and 1015 deletions

45
README.md Normal file
View File

@@ -0,0 +1,45 @@
# Scrcpy For Android
[Scrcpy](https://github.com/Genymobile/scrcpy) android client
## 截图
<!-- markdownlint-disable MD033 -->
<p align="center">
<img src="https://github.com/user-attachments/assets/64e24f71-0326-407a-a527-070586bbec9a" height="320" alt="Screenshot 1" />
<img src="https://github.com/user-attachments/assets/74170ada-6dee-4ec7-ab24-c5ef2a231a47" height="320" alt="Screenshot 2" />
<img src="https://github.com/user-attachments/assets/6301f2fb-624b-4209-b548-6f37b9bcedc8" height="320" alt="Screenshot 3" />
</p>
## 已知问题
- ADB 配对流程未实现
- 多指触控抬起后滞留
- 快速离开再进入全屏会导致视频流关键帧丢失
## 构建
- JDK 17+
- Android SDK`compileSdk 36` / `buildTools 36.0.0`
- Android NDK `28.2.13676358`
```bash
./gradlew assembleDebug
```
Windows
```powershell
.\gradlew.bat assembleDebug
```
## Credits
- [Genymobile/scrcpy](https://github.com/Genymobile/scrcpy)
- [YuKongA/miuix](https://github.com/compose-miuix-ui/miuix)
- [tiann/KernelSU/manager](https://github.com/tiann/KernelSU/tree/main/manager)
## License
[Apache License 2.0](LICENSE)

View File

@@ -28,6 +28,7 @@ android {
ndk { ndk {
abiFilters.clear() abiFilters.clear()
//noinspection ChromeOsAbiSupport
abiFilters += configuredAbiList abiFilters += configuredAbiList
} }

View File

@@ -15,8 +15,8 @@ import java.io.File
import java.time.LocalTime import java.time.LocalTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.ArrayDeque import java.util.ArrayDeque
import java.util.concurrent.CopyOnWriteArraySet
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArraySet
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
import java.util.concurrent.Executors import java.util.concurrent.Executors
@@ -34,7 +34,8 @@ class NativeCoreFacade(private val appContext: Context) {
private val bootstrapLock = Any() private val bootstrapLock = Any()
private val bootstrapPackets = ArrayDeque<CachedPacket>() private val bootstrapPackets = ArrayDeque<CachedPacket>()
private var packetCount: Long = 0 private var packetCount: Long = 0
@Volatile private var audioPlayer: ScrcpyAudioPlayer? = null @Volatile
private var audioPlayer: ScrcpyAudioPlayer? = null
@Volatile @Volatile
private var currentSessionInfo: ScrcpySessionInfo? = null private var currentSessionInfo: ScrcpySessionInfo? = null
@@ -72,7 +73,10 @@ class NativeCoreFacade(private val appContext: Context) {
val currentId = surfaceIdentityMap[tag] val currentId = surfaceIdentityMap[tag]
val requestId = surface?.let { System.identityHashCode(it) } val requestId = surface?.let { System.identityHashCode(it) }
if (requestId != null && currentId != null && requestId != currentId) { if (requestId != null && currentId != null && requestId != currentId) {
Log.i(TAG, "unregisterVideoSurface(): skip stale request tag=$tag requestSurfaceId=$requestId currentSurfaceId=$currentId") Log.i(
TAG,
"unregisterVideoSurface(): skip stale request tag=$tag requestSurfaceId=$requestId currentSurfaceId=$currentId"
)
return return
} }
Log.i(TAG, "unregisterVideoSurface(): tag=$tag surfaceId=$requestId") Log.i(TAG, "unregisterVideoSurface(): tag=$tag surfaceId=$requestId")
@@ -178,7 +182,12 @@ class NativeCoreFacade(private val appContext: Context) {
audioPlayer?.release() audioPlayer?.release()
audioPlayer = null audioPlayer = null
if (info.audioCodecId != 0 && !request.noAudioPlayback) { if (info.audioCodecId != 0 && !request.noAudioPlayback) {
Log.i(TAG, "scrcpyStart(): create audio player codecId=0x${info.audioCodecId.toUInt().toString(16)}") Log.i(
TAG,
"scrcpyStart(): create audio player codecId=0x${
info.audioCodecId.toUInt().toString(16)
}"
)
val player = ScrcpyAudioPlayer(info.audioCodecId) val player = ScrcpyAudioPlayer(info.audioCodecId)
audioPlayer = player audioPlayer = player
sessionManager.attachAudioConsumer { packet -> sessionManager.attachAudioConsumer { packet ->
@@ -208,7 +217,11 @@ class NativeCoreFacade(private val appContext: Context) {
return true return true
} }
fun scrcpyListEncoders(customServerUri: String?, remotePath: String, serverVersion: String = "3.3.4"): ScrcpyEncoderLists { fun scrcpyListEncoders(
customServerUri: String?,
remotePath: String,
serverVersion: String = "3.3.4"
): ScrcpyEncoderLists {
return ioCall { return ioCall {
val serverJar = if (customServerUri.isNullOrBlank()) { val serverJar = if (customServerUri.isNullOrBlank()) {
extractAssetToCache(DEFAULT_SERVER_ASSET) extractAssetToCache(DEFAULT_SERVER_ASSET)
@@ -232,7 +245,11 @@ class NativeCoreFacade(private val appContext: Context) {
} }
} }
fun scrcpyListCameraSizes(customServerUri: String?, remotePath: String, serverVersion: String = "3.3.4"): ScrcpyCameraSizeLists { fun scrcpyListCameraSizes(
customServerUri: String?,
remotePath: String,
serverVersion: String = "3.3.4"
): ScrcpyCameraSizeLists {
return ioCall { return ioCall {
val serverJar = if (customServerUri.isNullOrBlank()) { val serverJar = if (customServerUri.isNullOrBlank()) {
extractAssetToCache(DEFAULT_SERVER_ASSET) extractAssetToCache(DEFAULT_SERVER_ASSET)
@@ -307,7 +324,17 @@ class NativeCoreFacade(private val appContext: Context) {
buttons: Int = 0, buttons: Int = 0,
) { ) {
ioExecute { ioExecute {
runCatching { sessionManager.injectScroll(x, y, screenWidth, screenHeight, hScroll, vScroll, buttons) } runCatching {
sessionManager.injectScroll(
x,
y,
screenWidth,
screenHeight,
hScroll,
vScroll,
buttons
)
}
} }
} }
@@ -475,10 +502,13 @@ class NativeCoreFacade(private val appContext: Context) {
val mime = when (session.codec.lowercase()) { val mime = when (session.codec.lowercase()) {
"h264" -> "video/avc" "h264" -> "video/avc"
"h265" -> "video/hevc" "h265" -> "video/hevc"
"av1" -> "video/av01" "av1" -> "video/av01"
else -> "video/avc" else -> "video/avc"
} }
Log.i(TAG, "createOrReplaceDecoder(): tag=$tag codec=$mime size=${session.width}x${session.height}") Log.i(
TAG,
"createOrReplaceDecoder(): tag=$tag codec=$mime size=${session.width}x${session.height}"
)
val decoder = AnnexBDecoder( val decoder = AnnexBDecoder(
width = session.width, width = session.width,
height = session.height, height = session.height,
@@ -489,7 +519,10 @@ class NativeCoreFacade(private val appContext: Context) {
if (current == null || (current.width == width && current.height == height)) { if (current == null || (current.width == width && current.height == height)) {
return@AnnexBDecoder return@AnnexBDecoder
} }
Log.i(TAG, "videoSizeChanged(): ${current.width}x${current.height} -> ${width}x${height}") Log.i(
TAG,
"videoSizeChanged(): ${current.width}x${current.height} -> ${width}x${height}"
)
currentSessionInfo = current.copy(width = width, height = height) currentSessionInfo = current.copy(width = width, height = height)
mainHandler.post { mainHandler.post {
videoSizeListeners.forEach { listener -> videoSizeListeners.forEach { listener ->
@@ -542,14 +575,22 @@ class NativeCoreFacade(private val appContext: Context) {
cacheBootstrapPacket(packet) cacheBootstrapPacket(packet)
packetCount += 1 packetCount += 1
if (packetCount == 1L || packetCount % 120L == 0L) { if (packetCount == 1L || packetCount % 120L == 0L) {
Log.i(TAG, "videoFeed(): packets=$packetCount key=${packet.isKeyFrame} cfg=${packet.isConfig} decoders=${decoderMap.size}") Log.i(
TAG,
"videoFeed(): packets=$packetCount key=${packet.isKeyFrame} cfg=${packet.isConfig} decoders=${decoderMap.size}"
)
} }
decoderMap.forEach { (tag, decoder) -> decoderMap.forEach { (tag, decoder) ->
if (!surfaceIdentityMap.containsKey(tag)) { if (!surfaceIdentityMap.containsKey(tag)) {
return@forEach return@forEach
} }
runCatching { runCatching {
decoder.feedAnnexB(packet.data, packet.ptsUs, packet.isKeyFrame, packet.isConfig) decoder.feedAnnexB(
packet.data,
packet.ptsUs,
packet.isKeyFrame,
packet.isConfig
)
} }
} }
} }
@@ -634,21 +675,3 @@ data class ScrcpySessionInfo(
val codec: String, val codec: String,
val controlEnabled: Boolean, val controlEnabled: Boolean,
) )
object AndroidKeycodes {
const val HOME = 3
const val BACK = 4
const val POWER = 26
const val APP_SWITCH = 187
const val VOLUME_UP = 24
const val VOLUME_DOWN = 25
}
object MotionActions {
const val DOWN = 0
const val UP = 1
const val MOVE = 2
const val CANCEL = 3
const val POINTER_DOWN = 5
const val POINTER_UP = 6
}

View File

@@ -1,75 +1,79 @@
package io.github.miuzarte.scrcpyforandroid.constants package io.github.miuzarte.scrcpyforandroid.constants
object AppDefaults { object AppDefaults {
const val MaxEventLogLines = 512 const val EVENT_LOG_LINES = 512
const val DefaultAdbPort = 5555 const val ADB_PORT = 5555
// Devices // Devices
const val DefaultQuickConnectInput = "" const val QUICK_CONNECT_INPUT = ""
const val DefaultPairHost = "" const val PAIR_HOST = ""
const val DefaultServerRemotePathInput = "" const val PAIR_PORT = ""
const val DefaultAdbKeyNameInput = "" const val PAIR_CODE = ""
const val DefaultPairPort = ""
const val DefaultPairCode = ""
const val DefaultAudioEnabled = true const val AUDIO_ENABLED = true
const val DefaultAudioCodec = "opus" const val AUDIO_CODEC = "opus"
const val DefaultAudioBitRateKbps = 128 const val AUDIO_BIT_RATE_KBPS = 128
const val DefaultVideoCodec = "h264" const val AUDIO_BIT_RATE_INPUT = "128"
const val DefaultBitRateMbps = 8f const val VIDEO_CODEC = "h264"
const val DefaultBitRateInput = "8.0" const val VIDEO_BIT_RATE_MBPS = 8f
const val VIDEO_BIT_RATE_INPUT = "8.0"
const val DefaultTurnScreenOff = false const val TURN_SCREEN_OFF = false
const val DefaultNoControl = false const val NO_CONTROL = false
const val DefaultNoVideo = false const val NO_VIDEO = false
const val DefaultVideoSourcePreset = "display" const val VIDEO_SOURCE_PRESET = "display"
const val DefaultDisplayIdInput = "" const val DISPLAY_ID = ""
const val DefaultCameraIdInput = "" const val CAMERA_ID = ""
const val DefaultCameraFacingPreset = "" const val CAMERA_FACING_PRESET = ""
const val DefaultCameraSizePreset = "" const val CAMERA_SIZE_PRESET = ""
const val DefaultCameraSizeCustom = "" const val CAMERA_SIZE_CUSTOM = ""
const val DefaultCameraArInput = "" const val CAMERA_AR = ""
const val DefaultCameraFpsInput = "" const val CAMERA_FPS = ""
const val DefaultCameraHighSpeed = false const val CAMERA_HIGH_SPEED = false
const val DefaultAudioSourcePreset = "output" const val AUDIO_SOURCE_PRESET = "output"
const val DefaultAudioSourceCustom = "" const val AUDIO_SOURCE_CUSTOM = ""
const val DefaultAudioDup = false const val AUDIO_DUP = false
const val DefaultNoAudioPlayback = false const val NO_AUDIO_PLAYBACK = false
const val DefaultRequireAudio = false const val REQUIRE_AUDIO = false
const val DefaultMaxSizeInput = "" const val MAX_SIZE_INPUT = ""
const val DefaultMaxFpsInput = "" const val MAX_FPS_INPUT = ""
const val DefaultVideoEncoder = "" const val VIDEO_ENCODER = ""
const val DefaultVideoCodecOptions = "" const val VIDEO_CODEC_OPTION = ""
const val DefaultAudioEncoder = "" const val AUDIO_ENCODER = ""
const val DefaultAudioCodecOptions = "" const val AUDIO_CODEC_OPTION = ""
const val DefaultNewDisplayWidth = "" const val NEW_DISPLAY_WIDTH = ""
const val DefaultNewDisplayHeight = "" const val NEW_DISPLAY_HEIGHT = ""
const val DefaultNewDisplayDpi = "" const val NEW_DISPLAY_DPI = ""
const val DefaultCropWidth = "" const val CROP_WIDTH = ""
const val DefaultCropHeight = "" const val CROP_HEIGHT = ""
const val DefaultCropX = "" const val CROP_X = ""
const val DefaultCropY = "" const val CROP_Y = ""
// Settings // Settings
const val DefaultThemeBaseIndex = 0 const val THEME_BASE_INDEX = 0
const val DefaultMonetEnabled = false const val MONET = false
const val DefaultFullscreenDebugInfoEnabled = false const val FULLSCREEN_DEBUG_INFO = false
const val DefaultShowFullscreenVirtualButtons = true const val SHOW_FULLSCREEN_VIRTUAL_BUTTONS = true
const val DefaultDevicePreviewCardHeightDp = 320 const val KEEP_SCREEN_ON_WHEN_STREAMING = false
const val DefaultKeepScreenOnWhenStreamingEnabled = false const val DEVICE_PREVIEW_CARD_HEIGHT_DP = 320
const val DefaultVirtualButtonsOutside = "more,home,back" const val VIRTUAL_BUTTONS_OUTSIDE = "more,home,back"
const val DefaultVirtualButtonsInMore = "app_switch,menu,notification,volume_up,volume_down,volume_mute,power,screenshot" const val VIRTUAL_BUTTONS_IN_MORE =
"app_switch,menu,notification,volume_up,volume_down,volume_mute,power,screenshot"
const val DefaultServerRemotePath = "/data/local/tmp/scrcpy-server.jar" const val CUSTOM_SERVER_URI = ""
const val DefaultAdbKeyName = "scrcpy" const val SERVER_REMOTE_PATH = "/data/local/tmp/scrcpy-server.jar"
const val SERVER_REMOTE_PATH_INPUT = ""
const val ADB_KEY_NAME = "scrcpy"
const val ADB_KEY_NAME_INPUT = ""
} }

View File

@@ -1,59 +1,78 @@
package io.github.miuzarte.scrcpyforandroid.constants package io.github.miuzarte.scrcpyforandroid.constants
object AppPreferenceKeys { object AppPreferenceKeys {
const val PrefsName = "scrcpy_app_prefs" const val PREFS_NAME = "scrcpy_app_prefs"
const val NativeAdbKeyPrefsName = "nativecore_adb_rsa" const val NATIVE_ADB_KEY_PREFS_NAME = "nativecore_adb_rsa"
const val NativeAdbPrivateKey = "priv" const val NATIVE_ADB_PRIVATE_KEY = "priv"
const val ThemeBaseIndex = "theme_base_index"
const val MonetEnabled = "monet_enabled" // Devices
const val FullscreenDebugInfoEnabled = "fullscreen_debug_info_enabled" const val QUICK_DEVICES = "quick_devices"
const val ShowFullscreenVirtualButtons = "show_fullscreen_virtual_buttons" const val QUICK_CONNECT_INPUT = "quick_connect_input"
const val DevicePreviewCardHeightDp = "device_preview_card_height_dp"
const val KeepScreenOnWhenStreamingEnabled = "keep_screen_on_when_streaming_enabled" const val PAIR_HOST = "pair_host"
const val VirtualButtonsOutside = "virtual_buttons_outside" const val PAIR_PORT = "pair_port"
const val VirtualButtonsInMore = "virtual_buttons_in_more" const val PAIR_CODE = "pair_code"
const val CustomServerUri = "custom_server_uri"
const val ServerRemotePath = "server_remote_path" const val AUDIO_ENABLED = "audio_enabled"
const val VideoCodec = "video_codec" const val AUDIO_CODEC = "audio_codec"
const val AudioEnabled = "audio_enabled" const val AUDIO_BIT_RATE_INPUT = "audio_bit_rate_input"
const val AudioCodec = "audio_codec" const val AUDIO_BIT_RATE_KBPS = "audio_bit_rate_kbps"
const val PairHost = "pair_host" const val VIDEO_CODEC = "video_codec"
const val PairPort = "pair_port" const val VIDEO_BIT_RATE_MBPS = "video_bit_rate_mbps"
const val PairCode = "pair_code" const val VIDEO_BIT_RATE_INPUT = "video_bit_rate_input"
const val QuickConnectInput = "quick_connect_input"
const val BitRateMbps = "bit_rate_mbps" const val TURN_SCREEN_OFF = "turn_screen_off"
const val BitRateInput = "bit_rate_input" const val NO_CONTROL = "no_control"
const val AudioBitRateKbps = "audio_bit_rate_kbps" const val NO_VIDEO = "no_video"
const val MaxSizeInput = "max_size_input"
const val MaxFpsInput = "max_fps_input" const val VIDEO_SOURCE_PRESET = "video_source_preset"
const val NoControl = "no_control" const val DISPLAY_ID = "display_id"
const val VideoEncoder = "video_encoder"
const val VideoCodecOptions = "video_codec_options" const val CAMERA_ID = "camera_id"
const val AudioEncoder = "audio_encoder" const val CAMERA_FACING_PRESET = "camera_facing_preset"
const val AudioCodecOptions = "audio_codec_options" const val CAMERA_SIZE_PRESET = "camera_size_preset"
const val AudioDup = "audio_dup" const val CAMERA_SIZE_CUSTOM = "camera_size_custom"
const val AudioSourcePreset = "audio_source_preset" const val CAMERA_AR = "camera_ar"
const val AudioSourceCustom = "audio_source_custom" const val CAMERA_FPS = "camera_fps"
const val VideoSourcePreset = "video_source_preset" const val CAMERA_HIGH_SPEED = "camera_high_speed"
const val CameraIdInput = "camera_id_input"
const val CameraFacingPreset = "camera_facing_preset" const val AUDIO_SOURCE_PRESET = "audio_source_preset"
const val CameraSizePreset = "camera_size_preset" const val AUDIO_SOURCE_CUSTOM = "audio_source_custom"
const val CameraSizeCustom = "camera_size_custom" const val AUDIO_DUP = "audio_dup"
const val CameraArInput = "camera_ar_input" const val NO_AUDIO_PLAYBACK = "no_audio_playback"
const val CameraFpsInput = "camera_fps_input" const val REQUIRE_AUDIO = "require_audio"
const val CameraHighSpeed = "camera_high_speed"
const val NoAudioPlayback = "no_audio_playback" const val MAX_SIZE_INPUT = "max_size_input"
const val NoVideo = "no_video" const val MAX_FPS_INPUT = "max_fps_input"
const val RequireAudio = "require_audio"
const val TurnScreenOff = "turn_screen_off" const val VIDEO_ENCODER = "video_encoder"
const val NewDisplayWidth = "new_display_width" const val VIDEO_CODEC_OPTION = "video_codec_options"
const val NewDisplayHeight = "new_display_height" const val AUDIO_ENCODER = "audio_encoder"
const val NewDisplayDpi = "new_display_dpi" const val AUDIO_CODEC_OPTION = "audio_codec_options"
const val DisplayIdInput = "display_id_input"
const val CropWidth = "crop_width" const val NEW_DISPLAY_WIDTH = "new_display_width"
const val CropHeight = "crop_height" const val NEW_DISPLAY_HEIGHT = "new_display_height"
const val CropX = "crop_x" const val NEW_DISPLAY_DPI = "new_display_dpi"
const val CropY = "crop_y"
const val AdbKeyName = "adb_key_name" const val CROP_WIDTH = "crop_width"
const val QuickDevices = "quick_devices" const val CROP_HEIGHT = "crop_height"
const val CROP_X = "crop_x"
const val CROP_Y = "crop_y"
// Settings
const val THEME_BASE_INDEX = "theme_base_index"
const val MONET = "monet"
const val FULLSCREEN_DEBUG_INFO = "fullscreen_debug_info"
const val SHOW_FULLSCREEN_VIRTUAL_BUTTONS = "show_fullscreen_virtual_buttons"
const val KEEP_SCREEN_ON_WHEN_STREAMING = "keep_screen_on_when_streaming"
const val DEVICE_PREVIEW_CARD_HEIGHT_DP = "device_preview_card_height_dp"
const val VIRTUAL_BUTTONS_OUTSIDE = "virtual_buttons_outside"
const val VIRTUAL_BUTTONS_IN_MORE = "virtual_buttons_in_more"
const val CUSTOM_SERVER_URI = "custom_server_uri"
const val SERVER_REMOTE_PATH = "server_remote_path"
const val ADB_KEY_NAME = "adb_key_name"
} }

View File

@@ -1,6 +1,6 @@
package io.github.miuzarte.scrcpyforandroid.constants package io.github.miuzarte.scrcpyforandroid.constants
object UiMotion { object UiMotion {
const val PageSwitchDampingRatio = 0.9f const val PAGE_SWITCH_DAMPING_RATIO = 0.9f
const val PageSwitchStiffness = 700f const val PAGE_SWITCH_STIFFNESS = 700f
} }

View File

@@ -24,6 +24,7 @@ class AnnexBDecoder(
private var outCount = 0L private var outCount = 0L
private var fpsWindowStartNs = System.nanoTime() private var fpsWindowStartNs = System.nanoTime()
private var fpsWindowFrameCount = 0 private var fpsWindowFrameCount = 0
@Volatile @Volatile
private var released = false private var released = false
@@ -65,16 +66,26 @@ class AnnexBDecoder(
codec.queueInputBuffer(inputIndex, 0, data.size, ptsUs, flags) codec.queueInputBuffer(inputIndex, 0, data.size, ptsUs, flags)
inCount += 1 inCount += 1
if (inCount == 1L || inCount % 180L == 0L || isConfig) { if (inCount == 1L || inCount % 180L == 0L || isConfig) {
Log.i(TAG, "feed(): mime=$decoderMime in=$inCount size=${data.size} key=$isKeyFrame cfg=$isConfig pts=$ptsUs") Log.i(
TAG,
"feed(): mime=$decoderMime in=$inCount size=${data.size} key=$isKeyFrame cfg=$isConfig pts=$ptsUs"
)
} }
} else { } else {
if (isConfig || isKeyFrame) { if (isConfig || isKeyFrame) {
Log.w(TAG, "drop critical packet: mime=$decoderMime size=${data.size} key=$isKeyFrame cfg=$isConfig") Log.w(
TAG,
"drop critical packet: mime=$decoderMime size=${data.size} key=$isKeyFrame cfg=$isConfig"
)
} }
} }
drainOutput() drainOutput()
}.onFailure { }.onFailure {
Log.w(TAG, "feed failed: mime=$decoderMime size=${data.size} key=$isKeyFrame cfg=$isConfig", it) Log.w(
TAG,
"feed failed: mime=$decoderMime size=${data.size} key=$isKeyFrame cfg=$isConfig",
it
)
} }
} }
@@ -108,10 +119,12 @@ class AnnexBDecoder(
Log.i(TAG, "drain(): mime=$decoderMime out=$outCount") Log.i(TAG, "drain(): mime=$decoderMime out=$outCount")
} }
} }
outIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> { outIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
notifyOutputSize(codec.outputFormat) notifyOutputSize(codec.outputFormat)
continue continue
} }
else -> return else -> return
} }
} }

View File

@@ -41,23 +41,33 @@ internal class DirectAdbTransport(private val context: Context) {
val privateKey: PrivateKey get() = keys.first val privateKey: PrivateKey get() = keys.first
val publicKeyX509: ByteArray get() = keys.second val publicKeyX509: ByteArray get() = keys.second
@Volatile var keyName: String = AppDefaults.DefaultAdbKeyName @Volatile
var keyName: String = AppDefaults.ADB_KEY_NAME
fun connect(host: String, port: Int): DirectAdbConnection { fun connect(host: String, port: Int): DirectAdbConnection {
Log.i(TAG, "connect(): opening direct adbd transport to $host:$port") Log.i(TAG, "connect(): opening direct adbd transport to $host:$port")
val conn = DirectAdbConnection(host, port, privateKey, publicKeyX509, keyName.ifBlank { AppDefaults.DefaultAdbKeyName }) val conn = DirectAdbConnection(
host,
port,
privateKey,
publicKeyX509,
keyName.ifBlank { AppDefaults.ADB_KEY_NAME })
conn.handshake() conn.handshake()
Log.i(TAG, "connect(): handshake success for $host:$port") Log.i(TAG, "connect(): handshake success for $host:$port")
return conn return conn
} }
private fun loadOrCreate(): Pair<PrivateKey, ByteArray> { private fun loadOrCreate(): Pair<PrivateKey, ByteArray> {
val prefs = context.getSharedPreferences(AppPreferenceKeys.NativeAdbKeyPrefsName, Context.MODE_PRIVATE) val prefs = context.getSharedPreferences(
val privB64 = prefs.getString(AppPreferenceKeys.NativeAdbPrivateKey, null) AppPreferenceKeys.NATIVE_ADB_KEY_PREFS_NAME,
Context.MODE_PRIVATE
)
val privB64 = prefs.getString(AppPreferenceKeys.NATIVE_ADB_PRIVATE_KEY, null)
if (privB64 != null) { if (privB64 != null) {
try { try {
val kf = KeyFactory.getInstance("RSA") val kf = KeyFactory.getInstance("RSA")
val priv = kf.generatePrivate(PKCS8EncodedKeySpec(Base64.decode(privB64, Base64.DEFAULT))) val priv =
kf.generatePrivate(PKCS8EncodedKeySpec(Base64.decode(privB64, Base64.DEFAULT)))
val pub = derivePublicX509(priv) val pub = derivePublicX509(priv)
Log.i(TAG, "loadOrCreate(): loaded persisted RSA key pair, fp=${fingerprint(pub)}") Log.i(TAG, "loadOrCreate(): loaded persisted RSA key pair, fp=${fingerprint(pub)}")
return Pair(priv, pub) return Pair(priv, pub)
@@ -68,8 +78,16 @@ internal class DirectAdbTransport(private val context: Context) {
val kpg = KeyPairGenerator.getInstance("RSA") val kpg = KeyPairGenerator.getInstance("RSA")
kpg.initialize(2048) kpg.initialize(2048)
val kp = kpg.generateKeyPair() val kp = kpg.generateKeyPair()
prefs.edit { putString(AppPreferenceKeys.NativeAdbPrivateKey, Base64.encodeToString(kp.private.encoded, Base64.NO_WRAP)) } prefs.edit {
Log.i(TAG, "loadOrCreate(): generated new RSA key pair, fp=${fingerprint(kp.public.encoded)}") putString(
AppPreferenceKeys.NATIVE_ADB_PRIVATE_KEY,
Base64.encodeToString(kp.private.encoded, Base64.NO_WRAP)
)
}
Log.i(
TAG,
"loadOrCreate(): generated new RSA key pair, fp=${fingerprint(kp.public.encoded)}"
)
return Pair(kp.private, kp.public.encoded) return Pair(kp.private, kp.public.encoded)
} }
@@ -96,7 +114,7 @@ internal class DirectAdbConnection(
val port: Int, val port: Int,
private val privateKey: PrivateKey, private val privateKey: PrivateKey,
private val publicKeyX509: ByteArray, private val publicKeyX509: ByteArray,
private val keyName: String = AppDefaults.DefaultAdbKeyName, private val keyName: String = AppDefaults.ADB_KEY_NAME,
) : AutoCloseable { ) : AutoCloseable {
private val sha1DigestInfoPrefix = byteArrayOf( private val sha1DigestInfoPrefix = byteArrayOf(
@@ -122,7 +140,9 @@ internal class DirectAdbConnection(
private lateinit var rawOut: OutputStream private lateinit var rawOut: OutputStream
private val nextLocalId = AtomicInteger(1) private val nextLocalId = AtomicInteger(1)
private val streams = ConcurrentHashMap<Int, AdbSocketStream>() private val streams = ConcurrentHashMap<Int, AdbSocketStream>()
@Volatile private var closed = false
@Volatile
private var closed = false
private var readerThread: Thread? = null private var readerThread: Thread? = null
companion object { companion object {
@@ -171,9 +191,17 @@ internal class DirectAdbConnection(
throw IOException("ADB: connection rejected. Please accept the authorisation dialog on the target device.") throw IOException("ADB: connection rejected. Please accept the authorisation dialog on the target device.")
} }
} }
else -> throw IOException("ADB: unexpected message 0x${afterSign.command.toString(16)} after AUTH_SIGNATURE")
else -> throw IOException(
"ADB: unexpected message 0x${
afterSign.command.toString(
16
)
} after AUTH_SIGNATURE"
)
} }
} }
else -> throw IOException("ADB: unexpected initial message 0x${first.command.toString(16)}") else -> throw IOException("ADB: unexpected initial message 0x${first.command.toString(16)}")
} }
@@ -262,6 +290,7 @@ internal class DirectAdbConnection(
sendMsg(A_CLSE, 0, msg.arg0) sendMsg(A_CLSE, 0, msg.arg0)
} }
} }
A_CLSE -> streams.remove(msg.arg1)?.forceClose() A_CLSE -> streams.remove(msg.arg1)?.forceClose()
A_OPEN -> sendMsg(A_CLSE, 0, msg.arg0) A_OPEN -> sendMsg(A_CLSE, 0, msg.arg0)
} }
@@ -275,7 +304,12 @@ internal class DirectAdbConnection(
} }
@Synchronized @Synchronized
private fun sendMsg(command: Int, arg0: Int = 0, arg1: Int = 0, data: ByteArray = ByteArray(0)) { private fun sendMsg(
command: Int,
arg0: Int = 0,
arg1: Int = 0,
data: ByteArray = ByteArray(0)
) {
val crc = data.fold(0L) { acc, b -> acc + (b.toLong() and 0xFF) }.toInt() val crc = data.fold(0L) { acc, b -> acc + (b.toLong() and 0xFF) }.toInt()
val header = ByteBuffer.allocate(24).order(ByteOrder.LITTLE_ENDIAN) val header = ByteBuffer.allocate(24).order(ByteOrder.LITTLE_ENDIAN)
.putInt(command).putInt(arg0).putInt(arg1) .putInt(command).putInt(arg0).putInt(arg1)
@@ -296,7 +330,8 @@ internal class DirectAdbConnection(
val dataLen = buf.int val dataLen = buf.int
buf.int buf.int
buf.int buf.int
val data = if (dataLen > 0) ByteArray(dataLen).also { rawIn.readExact(it) } else ByteArray(0) val data =
if (dataLen > 0) ByteArray(dataLen).also { rawIn.readExact(it) } else ByteArray(0)
return AdbMsg(command, arg0, arg1, data) return AdbMsg(command, arg0, arg1, data)
} }
@@ -391,12 +426,16 @@ internal class AdbSocketStream(
private const val A_CLSE = 0x45534c43 private const val A_CLSE = 0x45534c43
} }
@Volatile var remoteId: Int = 0 @Volatile
@Volatile var closed: Boolean = false var remoteId: Int = 0
@Volatile
var closed: Boolean = false
private val latch = CountDownLatch(1) private val latch = CountDownLatch(1)
private val latchOk = AtomicBoolean(false) private val latchOk = AtomicBoolean(false)
private val queue = LinkedBlockingQueue<Any>() private val queue = LinkedBlockingQueue<Any>()
private object EndOfStreamMarker private object EndOfStreamMarker
val inputStream: InputStream = InStream() val inputStream: InputStream = InStream()
@@ -476,6 +515,7 @@ internal class AdbSocketStream(
if (len == 0) return if (len == 0) return
sender(A_WRTE, localId, remoteId, b.copyOfRange(off, off + len)) sender(A_WRTE, localId, remoteId, b.copyOfRange(off, off + len))
} }
override fun flush() {} override fun flush() {}
} }
} }

View File

@@ -7,13 +7,18 @@ import java.nio.file.Path
class NativeAdbService(appContext: Context) { class NativeAdbService(appContext: Context) {
private val transport = DirectAdbTransport(appContext) private val transport = DirectAdbTransport(appContext)
@Volatile private var connection: DirectAdbConnection? = null @Volatile
@Volatile private var connectedHost: String? = null private var connection: DirectAdbConnection? = null
@Volatile private var connectedPort: Int? = null @Volatile
private var connectedHost: String? = null
@Volatile
private var connectedPort: Int? = null
var keyName: String var keyName: String
get() = transport.keyName get() = transport.keyName
set(value) { transport.keyName = value } set(value) {
transport.keyName = value
}
@Synchronized @Synchronized
fun pair(host: String, port: Int, pairingCode: String): Boolean { fun pair(host: String, port: Int, pairingCode: String): Boolean {

View File

@@ -4,7 +4,6 @@ import android.media.AudioAttributes
import android.media.AudioFormat import android.media.AudioFormat
import android.media.AudioManager import android.media.AudioManager
import android.media.AudioTrack import android.media.AudioTrack
import android.os.Build
import android.media.MediaCodec import android.media.MediaCodec
import android.media.MediaFormat import android.media.MediaFormat
import android.util.Log import android.util.Log
@@ -23,15 +22,22 @@ class ScrcpyAudioPlayer(private val codecId: Int) {
private var audioTrack: AudioTrack? = null private var audioTrack: AudioTrack? = null
private val bufferInfo = MediaCodec.BufferInfo() private val bufferInfo = MediaCodec.BufferInfo()
@Volatile private var prepared = false @Volatile
@Volatile private var released = false private var prepared = false
@Volatile
private var released = false
private var packetCount = 0L private var packetCount = 0L
fun feedPacket(data: ByteArray, ptsUs: Long, isConfig: Boolean) { fun feedPacket(data: ByteArray, ptsUs: Long, isConfig: Boolean) {
if (released) return if (released) return
if (isConfig) { if (isConfig) {
Log.i(TAG, "feedPacket(): config packet size=${data.size} codec=0x${codecId.toUInt().toString(16)}") Log.i(
TAG,
"feedPacket(): config packet size=${data.size} codec=0x${
codecId.toUInt().toString(16)
}"
)
when (codecId) { when (codecId) {
AUDIO_CODEC_OPUS -> prepareOpus(data) AUDIO_CODEC_OPUS -> prepareOpus(data)
AUDIO_CODEC_AAC -> prepareAac(data) AUDIO_CODEC_AAC -> prepareAac(data)
@@ -47,13 +53,7 @@ class ScrcpyAudioPlayer(private val codecId: Int) {
} }
if (codecId == AUDIO_CODEC_RAW) { if (codecId == AUDIO_CODEC_RAW) {
ensureRawAudioTrack()?.let { track -> ensureRawAudioTrack()?.write(data, 0, data.size, AudioTrack.WRITE_NON_BLOCKING)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
track.write(data, 0, data.size, AudioTrack.WRITE_NON_BLOCKING)
} else {
track.write(data, 0, data.size)
}
}
return return
} }
@@ -74,11 +74,16 @@ class ScrcpyAudioPlayer(private val codecId: Int) {
private fun prepareOpus(opusHead: ByteArray) { private fun prepareOpus(opusHead: ByteArray) {
if (prepared || released) return if (prepared || released) return
runCatching { runCatching {
val format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_OPUS, SAMPLE_RATE, CHANNELS) val format = MediaFormat.createAudioFormat(
MediaFormat.MIMETYPE_AUDIO_OPUS,
SAMPLE_RATE,
CHANNELS
)
format.setByteBuffer("csd-0", ByteBuffer.wrap(opusHead)) format.setByteBuffer("csd-0", ByteBuffer.wrap(opusHead))
// pre-skip field: 2 bytes LE at offset 10 of the OpusHead // pre-skip field: 2 bytes LE at offset 10 of the OpusHead
if (opusHead.size >= 12) { if (opusHead.size >= 12) {
val preSkip = ((opusHead[11].toInt() and 0xFF) shl 8) or (opusHead[10].toInt() and 0xFF) val preSkip =
((opusHead[11].toInt() and 0xFF) shl 8) or (opusHead[10].toInt() and 0xFF)
val codecDelayNs = preSkip.toLong() * 1_000_000_000L / SAMPLE_RATE val codecDelayNs = preSkip.toLong() * 1_000_000_000L / SAMPLE_RATE
format.setByteBuffer("csd-1", longBuffer(codecDelayNs)) format.setByteBuffer("csd-1", longBuffer(codecDelayNs))
format.setByteBuffer("csd-2", longBuffer(OPUS_SEEK_PREROLL_NS)) format.setByteBuffer("csd-2", longBuffer(OPUS_SEEK_PREROLL_NS))
@@ -90,7 +95,8 @@ class ScrcpyAudioPlayer(private val codecId: Int) {
private fun prepareAac(aacConfig: ByteArray) { private fun prepareAac(aacConfig: ByteArray) {
if (prepared || released) return if (prepared || released) return
runCatching { runCatching {
val format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, SAMPLE_RATE, CHANNELS) val format =
MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, SAMPLE_RATE, CHANNELS)
format.setByteBuffer("csd-0", ByteBuffer.wrap(aacConfig)) format.setByteBuffer("csd-0", ByteBuffer.wrap(aacConfig))
startCodecAndTrack(format) startCodecAndTrack(format)
}.onFailure { Log.w(TAG, "prepareAac failed", it) } }.onFailure { Log.w(TAG, "prepareAac failed", it) }
@@ -99,7 +105,11 @@ class ScrcpyAudioPlayer(private val codecId: Int) {
private fun prepareFlac(flacConfig: ByteArray) { private fun prepareFlac(flacConfig: ByteArray) {
if (prepared || released) return if (prepared || released) return
runCatching { runCatching {
val format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_FLAC, SAMPLE_RATE, CHANNELS) val format = MediaFormat.createAudioFormat(
MediaFormat.MIMETYPE_AUDIO_FLAC,
SAMPLE_RATE,
CHANNELS
)
if (flacConfig.isNotEmpty()) { if (flacConfig.isNotEmpty()) {
format.setByteBuffer("csd-0", ByteBuffer.wrap(flacConfig)) format.setByteBuffer("csd-0", ByteBuffer.wrap(flacConfig))
} }
@@ -161,11 +171,7 @@ class ScrcpyAudioPlayer(private val codecId: Int) {
val pcm = ByteArray(size) val pcm = ByteArray(size)
outBuf.position(bufferInfo.offset) outBuf.position(bufferInfo.offset)
outBuf.get(pcm) outBuf.get(pcm)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { track.write(pcm, 0, size, AudioTrack.WRITE_NON_BLOCKING)
track.write(pcm, 0, size, AudioTrack.WRITE_NON_BLOCKING)
} else {
track.write(pcm, 0, size)
}
} }
codec.releaseOutputBuffer(idx, false) codec.releaseOutputBuffer(idx, false)
idx = codec.dequeueOutputBuffer(bufferInfo, 0L) idx = codec.dequeueOutputBuffer(bufferInfo, 0L)
@@ -190,9 +196,9 @@ class ScrcpyAudioPlayer(private val codecId: Int) {
companion object { companion object {
private const val TAG = "ScrcpyAudioPlayer" private const val TAG = "ScrcpyAudioPlayer"
const val AUDIO_CODEC_OPUS = 0x6f707573 const val AUDIO_CODEC_OPUS = 0x6f707573
const val AUDIO_CODEC_AAC = 0x00616163 const val AUDIO_CODEC_AAC = 0x00616163
const val AUDIO_CODEC_FLAC = 0x666c6163 const val AUDIO_CODEC_FLAC = 0x666c6163
const val AUDIO_CODEC_RAW = 0x00726177 const val AUDIO_CODEC_RAW = 0x00726177
private const val SAMPLE_RATE = 48000 private const val SAMPLE_RATE = 48000
private const val CHANNELS = 2 private const val CHANNELS = 2
private const val CODEC_TIMEOUT_US = 10_000L private const val CODEC_TIMEOUT_US = 10_000L

View File

@@ -11,21 +11,25 @@ import java.io.InputStreamReader
import java.nio.file.Path import java.nio.file.Path
import java.security.SecureRandom import java.security.SecureRandom
import java.util.ArrayDeque import java.util.ArrayDeque
import java.util.LinkedHashSet
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.math.roundToInt import kotlin.math.roundToInt
class ScrcpySessionManager(private val adbService: NativeAdbService) { class ScrcpySessionManager(private val adbService: NativeAdbService) {
@Volatile @Volatile
private var activeSession: ActiveSession? = null private var activeSession: ActiveSession? = null
@Volatile @Volatile
private var videoConsumer: ((VideoPacket) -> Unit)? = null private var videoConsumer: ((VideoPacket) -> Unit)? = null
@Volatile @Volatile
private var videoReaderThread: Thread? = null private var videoReaderThread: Thread? = null
@Volatile @Volatile
private var audioConsumer: ((AudioPacket) -> Unit)? = null private var audioConsumer: ((AudioPacket) -> Unit)? = null
@Volatile @Volatile
private var audioReaderThread: Thread? = null private var audioReaderThread: Thread? = null
@Volatile @Volatile
private var lastServerCommand: String? = null private var lastServerCommand: String? = null
private val serverLogBuffer = ArrayDeque<String>() private val serverLogBuffer = ArrayDeque<String>()
@@ -44,7 +48,10 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
adbService.push(serverJarPath, targetPath) adbService.push(serverJarPath, targetPath)
val cmd = buildServerCommand(targetPath, scid, options) val cmd = buildServerCommand(targetPath, scid, options)
lastServerCommand = cmd lastServerCommand = cmd
Log.i(TAG, "start(): socket=$socketName codec=${options.videoCodec} audio=${options.audio} audioCodec=${options.audioCodec}") Log.i(
TAG,
"start(): socket=$socketName codec=${options.videoCodec} audio=${options.audio} audioCodec=${options.audioCodec}"
)
val serverStream = adbService.openShellStream(cmd) val serverStream = adbService.openShellStream(cmd)
val serverLogThread = startServerLogThread(serverStream, socketName) val serverLogThread = startServerLogThread(serverStream, socketName)
Thread.sleep(SERVER_BOOT_DELAY_MS) Thread.sleep(SERVER_BOOT_DELAY_MS)
@@ -64,13 +71,16 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
videoStream = firstStream videoStream = firstStream
videoInput = firstInput videoInput = firstInput
} }
options.audio -> { options.audio -> {
audioStream = firstStream audioStream = firstStream
audioInput = firstInput audioInput = firstInput
} }
options.control -> { options.control -> {
controlStream = firstStream controlStream = firstStream
} }
else -> { else -> {
throw IllegalArgumentException("At least one of video/audio/control must be enabled") throw IllegalArgumentException("At least one of video/audio/control must be enabled")
} }
@@ -128,11 +138,15 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
audioStream = audioStream, audioStream = audioStream,
audioInput = audioInput, audioInput = audioInput,
controlStream = controlStream, controlStream = controlStream,
controlWriter = controlStream?.outputStream?.let { ScrcpyControlWriter(DataOutputStream(it)) }, controlWriter = controlStream?.outputStream?.let {
ScrcpyControlWriter(
DataOutputStream(it)
)
},
) )
return sessionInfo return sessionInfo
} catch (t: Throwable) { } catch (t: Throwable) {
val tail = snapshotServerLogs(maxLines = 120) val tail = snapshotServerLogs()
val detail = if (tail.isBlank()) "" else " | server_log_tail=\n$tail" val detail = if (tail.isBlank()) "" else " | server_log_tail=\n$tail"
throw IllegalStateException("scrcpy start failed: ${t.message}$detail", t) throw IllegalStateException("scrcpy start failed: ${t.message}$detail", t)
} }
@@ -173,7 +187,7 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
) )
} catch (_: EOFException) { } catch (_: EOFException) {
break break
} catch (e: InterruptedException) { } catch (_: InterruptedException) {
// Ignore transient interrupts while session remains active. // Ignore transient interrupts while session remains active.
if (activeSession !== session || vStream.closed) { if (activeSession !== session || vStream.closed) {
break break
@@ -212,10 +226,12 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
Log.w(TAG, "audio disabled by server") Log.w(TAG, "audio disabled by server")
return@thread return@thread
} }
AUDIO_ERROR -> { AUDIO_ERROR -> {
Log.e(TAG, "audio stream configuration error from server") Log.e(TAG, "audio stream configuration error from server")
return@thread return@thread
} }
else -> { else -> {
Log.i(TAG, "audio stream codec=0x${streamCodecId.toUInt().toString(16)}") Log.i(TAG, "audio stream codec=0x${streamCodecId.toUInt().toString(16)}")
} }
@@ -223,7 +239,9 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
if (session.info.audioCodecId != 0 && streamCodecId != session.info.audioCodecId) { if (session.info.audioCodecId != 0 && streamCodecId != session.info.audioCodecId) {
Log.w( Log.w(
TAG, TAG,
"audio codec mismatch: requested=0x${session.info.audioCodecId.toUInt().toString(16)} stream=0x${streamCodecId.toUInt().toString(16)}", "audio codec mismatch: requested=0x${
session.info.audioCodecId.toUInt().toString(16)
} stream=0x${streamCodecId.toUInt().toString(16)}",
) )
} }
@@ -238,10 +256,16 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
val isConfig = (ptsAndFlags and PACKET_FLAG_CONFIG) != 0L val isConfig = (ptsAndFlags and PACKET_FLAG_CONFIG) != 0L
val ptsUs = ptsAndFlags and PACKET_PTS_MASK val ptsUs = ptsAndFlags and PACKET_PTS_MASK
audioConsumer?.invoke(AudioPacket(data = payload, ptsUs = ptsUs, isConfig = isConfig)) audioConsumer?.invoke(
AudioPacket(
data = payload,
ptsUs = ptsUs,
isConfig = isConfig
)
)
} catch (_: EOFException) { } catch (_: EOFException) {
break break
} catch (e: InterruptedException) { } catch (_: InterruptedException) {
if (activeSession !== session || aStream.closed) break if (activeSession !== session || aStream.closed) break
Thread.interrupted() Thread.interrupted()
} catch (e: Exception) { } catch (e: Exception) {
@@ -279,12 +303,38 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
actionButton: Int, actionButton: Int,
buttons: Int, buttons: Int,
) { ) {
requireControlWriter().injectTouch(action, pointerId, x, y, screenWidth, screenHeight, pressure, actionButton, buttons) requireControlWriter().injectTouch(
action,
pointerId,
x,
y,
screenWidth,
screenHeight,
pressure,
actionButton,
buttons
)
} }
@Synchronized @Synchronized
fun injectScroll(x: Int, y: Int, screenWidth: Int, screenHeight: Int, hScroll: Float, vScroll: Float, buttons: Int) { fun injectScroll(
requireControlWriter().injectScroll(x, y, screenWidth, screenHeight, hScroll, vScroll, buttons) x: Int,
y: Int,
screenWidth: Int,
screenHeight: Int,
hScroll: Float,
vScroll: Float,
buttons: Int
) {
requireControlWriter().injectScroll(
x,
y,
screenWidth,
screenHeight,
hScroll,
vScroll,
buttons
)
} }
@Synchronized @Synchronized
@@ -384,10 +434,15 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
} }
private fun requireControlWriter(): ScrcpyControlWriter { private fun requireControlWriter(): ScrcpyControlWriter {
return activeSession?.controlWriter ?: throw IllegalStateException("scrcpy control channel not available") return activeSession?.controlWriter
?: throw IllegalStateException("scrcpy control channel not available")
} }
private fun buildServerCommand(targetPath: String, scid: Int, options: ScrcpyStartOptions): String { private fun buildServerCommand(
targetPath: String,
scid: Int,
options: ScrcpyStartOptions
): String {
val serverArgs = buildServerArgs(scid, options) val serverArgs = buildServerArgs(scid, options)
return "CLASSPATH=$targetPath app_process / com.genymobile.scrcpy.Server ${options.serverVersion} $serverArgs" return "CLASSPATH=$targetPath app_process / com.genymobile.scrcpy.Server ${options.serverVersion} $serverArgs"
} }
@@ -474,7 +529,7 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
ServerArg( ServerArg(
type = ServerArgType.NUMBER, type = ServerArgType.NUMBER,
key = "video_bit_rate", key = "video_bit_rate",
value = options.videoBitRate, zeroValueBehavior = ZeroValueBehavior.KEEP, value = options.videoBitRate,
), ),
ServerArg( ServerArg(
type = ServerArgType.NUMBER, type = ServerArgType.NUMBER,
@@ -528,7 +583,6 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
key = "audio_bit_rate", key = "audio_bit_rate",
value = options.audioBitRate, value = options.audioBitRate,
includeWhen = options.audio, includeWhen = options.audio,
zeroValueBehavior = ZeroValueBehavior.KEEP,
), ),
ServerArg( ServerArg(
type = ServerArgType.STRING, type = ServerArgType.STRING,
@@ -539,7 +593,8 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
ServerArg( ServerArg(
type = ServerArgType.BOOLEAN, type = ServerArgType.BOOLEAN,
key = "audio_dup", key = "audio_dup",
value = options.audioDup, includeWhen = options.audioDup, value = options.audioDup,
includeWhen = options.audioDup,
), ),
ServerArg( ServerArg(
type = ServerArgType.STRING, type = ServerArgType.STRING,
@@ -579,15 +634,22 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
ServerArg( ServerArg(
type = ServerArgType.BOOLEAN, type = ServerArgType.BOOLEAN,
key = "list_encoders", key = "list_encoders",
value = options.listEncoders, includeWhen = options.listEncoders, value = options.listEncoders,
includeWhen = options.listEncoders,
), ),
ServerArg( ServerArg(
type = ServerArgType.BOOLEAN, type = ServerArgType.BOOLEAN,
key = "list_camera_sizes", key = "list_camera_sizes",
value = options.listCameraSizes, includeWhen = options.listCameraSizes, value = options.listCameraSizes,
includeWhen = options.listCameraSizes,
), ),
// Reserved for future args that require repeated/list values. // Reserved for future args that require repeated/list values.
// ServerArg(type = ServerArgType.LIST, key = "_unused_list", value = emptyList<String>(), includeWhen = false), // ServerArg(
// type = ServerArgType.LIST,
// key = "_unused_list",
// value = emptyList<String>(),
// includeWhen = false,
// ),
) )
val rendered = args.mapNotNull { it.render() } val rendered = args.mapNotNull { it.render() }
@@ -608,8 +670,8 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
} }
private enum class ZeroValueBehavior { private enum class ZeroValueBehavior {
KEEP,
OMIT, OMIT,
KEEP,
} }
private data class ServerArg<T>( private data class ServerArg<T>(
@@ -658,7 +720,7 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
?.filter { it.isNotBlank() } ?.filter { it.isNotBlank() }
.orEmpty() .orEmpty()
if (items.isEmpty()) return null if (items.isEmpty()) return null
return "$key=${items.joinToString(",")}" return "$key=${items.joinToString(",")}"
} }
} }
@@ -716,7 +778,12 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
private fun startServerLogThread(serverStream: AdbSocketStream, socketName: String): Thread { private fun startServerLogThread(serverStream: AdbSocketStream, socketName: String): Thread {
return thread(start = true, name = "scrcpy-server-log") { return thread(start = true, name = "scrcpy-server-log") {
try { try {
BufferedReader(InputStreamReader(serverStream.inputStream, Charsets.UTF_8)).use { reader -> BufferedReader(
InputStreamReader(
serverStream.inputStream,
Charsets.UTF_8
)
).use { reader ->
while (true) { while (true) {
val line = reader.readLine() ?: break val line = reader.readLine() ?: break
appendServerLog(line) appendServerLog(line)
@@ -740,7 +807,7 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
} }
@Synchronized @Synchronized
private fun snapshotServerLogs(maxLines: Int): String { private fun snapshotServerLogs(maxLines: Int = 120): String {
if (serverLogBuffer.isEmpty()) { if (serverLogBuffer.isEmpty()) {
return "" return ""
} }
@@ -748,7 +815,10 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
return serverLogBuffer.toList().takeLast(take).joinToString("\n") return serverLogBuffer.toList().takeLast(take).joinToString("\n")
} }
private fun openAbstractSocketWithRetry(socketName: String, expectDummyByte: Boolean): AdbSocketStream { private fun openAbstractSocketWithRetry(
socketName: String,
expectDummyByte: Boolean
): AdbSocketStream {
var lastEx: Exception? = null var lastEx: Exception? = null
repeat(CONNECT_RETRY_COUNT) { attempt -> repeat(CONNECT_RETRY_COUNT) { attempt ->
try { try {
@@ -963,7 +1033,15 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
} }
@Synchronized @Synchronized
fun injectScroll(x: Int, y: Int, screenWidth: Int, screenHeight: Int, hScroll: Float, vScroll: Float, buttons: Int) { fun injectScroll(
x: Int,
y: Int,
screenWidth: Int,
screenHeight: Int,
hScroll: Float,
vScroll: Float,
buttons: Int
) {
output.writeByte(TYPE_INJECT_SCROLL_EVENT) output.writeByte(TYPE_INJECT_SCROLL_EVENT)
writePosition(x, y, screenWidth, screenHeight) writePosition(x, y, screenWidth, screenHeight)
output.writeShort(encodeSignedFixedPoint16(hScroll / 16f)) output.writeShort(encodeSignedFixedPoint16(hScroll / 16f))
@@ -1033,6 +1111,7 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
private const val AUDIO_CODEC_AAC = 0x00616163 private const val AUDIO_CODEC_AAC = 0x00616163
private const val AUDIO_CODEC_FLAC = 0x666c6163 private const val AUDIO_CODEC_FLAC = 0x666c6163
private const val AUDIO_CODEC_RAW = 0x00726177 private const val AUDIO_CODEC_RAW = 0x00726177
// Audio stream disable codes from server (writeDisableStream) // Audio stream disable codes from server (writeDisableStream)
private const val AUDIO_DISABLED = 0 private const val AUDIO_DISABLED = 0
private const val AUDIO_ERROR = 1 private const val AUDIO_ERROR = 1
@@ -1044,10 +1123,12 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
private const val TYPE_SET_DISPLAY_POWER = 10 private const val TYPE_SET_DISPLAY_POWER = 10
private val VIDEO_ENCODER_REGEX = Regex("--video-encoder=([\\w.\\-]+)") private val VIDEO_ENCODER_REGEX = Regex("--video-encoder=([\\w.\\-]+)")
private val AUDIO_ENCODER_REGEX = Regex("--audio-encoder=([\\w.\\-]+)") private val AUDIO_ENCODER_REGEX = Regex("--audio-encoder=([\\w.\\-]+)")
private val VIDEO_ENCODER_FALLBACK_REGEX = Regex("""--video-encoder=['\"]?([^'\"\s]+)""") private val VIDEO_ENCODER_FALLBACK_REGEX = Regex("""--video-encoder=['"]?([^'"\s]+)""")
private val AUDIO_ENCODER_FALLBACK_REGEX = Regex("""--audio-encoder=['\"]?([^'\"\s]+)""") private val AUDIO_ENCODER_FALLBACK_REGEX = Regex("""--audio-encoder=['"]?([^'"\s]+)""")
private val VIDEO_ENCODER_TYPE_REGEX = Regex("""--video-codec=[^\s]+\s+--video-encoder=([^\s]+).*?\((hw|sw)\)""") private val VIDEO_ENCODER_TYPE_REGEX =
private val AUDIO_ENCODER_TYPE_REGEX = Regex("""--audio-codec=[^\s]+\s+--audio-encoder=([^\s]+).*?\((hw|sw)\)""") Regex("""--video-codec=\S+\s+--video-encoder=(\S+).*?\((hw|sw)\)""")
private val AUDIO_ENCODER_TYPE_REGEX =
Regex("""--audio-codec=\S+\s+--audio-encoder=(\S+).*?\((hw|sw)\)""")
private val CAMERA_SIZE_REGEX = Regex("--camera-size=([0-9]+x[0-9]+)") private val CAMERA_SIZE_REGEX = Regex("--camera-size=([0-9]+x[0-9]+)")
private val CAMERA_SIZE_FALLBACK_REGEX = Regex("\\b([1-9][0-9]{1,4}x[1-9][0-9]{1,4})\\b") private val CAMERA_SIZE_FALLBACK_REGEX = Regex("\\b([1-9][0-9]{1,4}x[1-9][0-9]{1,4})\\b")

View File

@@ -1,7 +1,6 @@
package io.github.miuzarte.scrcpyforandroid.pages package io.github.miuzarte.scrcpyforandroid.pages
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -12,6 +11,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
@@ -22,11 +22,10 @@ import io.github.miuzarte.scrcpyforandroid.constants.ScrcpyPresets
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlide import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlide
import kotlin.math.roundToInt
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.SnackbarHostState
import top.yukonga.miuix.kmp.basic.ScrollBehavior import top.yukonga.miuix.kmp.basic.ScrollBehavior
import top.yukonga.miuix.kmp.basic.SnackbarHostState
import top.yukonga.miuix.kmp.basic.SpinnerEntry import top.yukonga.miuix.kmp.basic.SpinnerEntry
import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextField import top.yukonga.miuix.kmp.basic.TextField
@@ -34,6 +33,7 @@ import top.yukonga.miuix.kmp.extra.SuperArrow
import top.yukonga.miuix.kmp.extra.SuperDropdown import top.yukonga.miuix.kmp.extra.SuperDropdown
import top.yukonga.miuix.kmp.extra.SuperSpinner import top.yukonga.miuix.kmp.extra.SuperSpinner
import top.yukonga.miuix.kmp.extra.SuperSwitch import top.yukonga.miuix.kmp.extra.SuperSwitch
import kotlin.math.roundToInt
private val AUDIO_SOURCE_OPTIONS = listOf( private val AUDIO_SOURCE_OPTIONS = listOf(
"output" to "output", "output" to "output",
@@ -142,15 +142,20 @@ internal fun AdvancedConfigPage(
) { ) {
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val maxSizePresetIndex = presetIndexFromInputForAdvancedPage(maxSizeInput, ScrcpyPresets.MaxSize) val maxSizePresetIndex =
presetIndexFromInputForAdvancedPage(maxSizeInput, ScrcpyPresets.MaxSize)
val maxFpsPresetIndex = presetIndexFromInputForAdvancedPage(maxFpsInput, ScrcpyPresets.MaxFPS) val maxFpsPresetIndex = presetIndexFromInputForAdvancedPage(maxFpsInput, ScrcpyPresets.MaxFPS)
val audioSourceItems = AUDIO_SOURCE_OPTIONS.map { it.second } val audioSourceItems = AUDIO_SOURCE_OPTIONS.map { it.second }
val audioSourceIndex = AUDIO_SOURCE_OPTIONS.indexOfFirst { it.first == audioSourcePreset }.let { if (it >= 0) it else 0 } val audioSourceIndex = AUDIO_SOURCE_OPTIONS.indexOfFirst { it.first == audioSourcePreset }
.let { if (it >= 0) it else 0 }
val videoSourceItems = VIDEO_SOURCE_OPTIONS.map { it.second } val videoSourceItems = VIDEO_SOURCE_OPTIONS.map { it.second }
val videoSourceIndex = VIDEO_SOURCE_OPTIONS.indexOfFirst { it.first == videoSourcePreset }.let { if (it >= 0) it else 0 } val videoSourceIndex = VIDEO_SOURCE_OPTIONS.indexOfFirst { it.first == videoSourcePreset }
.let { if (it >= 0) it else 0 }
val cameraFacingItems = CAMERA_FACING_OPTIONS.map { it.second } val cameraFacingItems = CAMERA_FACING_OPTIONS.map { it.second }
val cameraFacingIndex = CAMERA_FACING_OPTIONS.indexOfFirst { it.first == cameraFacingPreset }.let { if (it >= 0) it else 0 } val cameraFacingIndex = CAMERA_FACING_OPTIONS.indexOfFirst { it.first == cameraFacingPreset }
val cameraFpsPresetIndex = presetIndexFromInputForAdvancedPage(cameraFpsInput, CAMERA_FPS_PRESETS) .let { if (it >= 0) it else 0 }
val cameraFpsPresetIndex =
presetIndexFromInputForAdvancedPage(cameraFpsInput, CAMERA_FPS_PRESETS)
val videoEncoderEntries = videoEncoderDropdownItems.map { encoderName -> val videoEncoderEntries = videoEncoderDropdownItems.map { encoderName ->
if (encoderName == "默认") { if (encoderName == "默认") {
SpinnerEntry(title = encoderName) SpinnerEntry(title = encoderName)
@@ -269,7 +274,10 @@ internal fun AdvancedConfigPage(
title = "摄像头分辨率", title = "摄像头分辨率",
summary = "--camera-size", summary = "--camera-size",
items = cameraSizeDropdownItems, items = cameraSizeDropdownItems,
selectedIndex = cameraSizeIndex.coerceIn(0, (cameraSizeDropdownItems.size - 1).coerceAtLeast(0)), selectedIndex = cameraSizeIndex.coerceIn(
0,
(cameraSizeDropdownItems.size - 1).coerceAtLeast(0)
),
onSelectedIndexChange = { index -> onSelectedIndexChange = { index ->
onCameraSizePresetChange( onCameraSizePresetChange(
when (index) { when (index) {

View File

@@ -5,56 +5,48 @@ import android.app.Activity
import android.util.Log import android.util.Log
import android.view.WindowManager import android.view.WindowManager
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.sp
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DragIndicator
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade
import io.github.miuzarte.scrcpyforandroid.ScrcpySessionInfo import io.github.miuzarte.scrcpyforandroid.ScrcpySessionInfo
import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics
import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn
import io.github.miuzarte.scrcpyforandroid.services.fetchConnectedDeviceInfo
import io.github.miuzarte.scrcpyforandroid.services.DevicePageSettings import io.github.miuzarte.scrcpyforandroid.services.DevicePageSettings
import io.github.miuzarte.scrcpyforandroid.services.loadQuickDevices import io.github.miuzarte.scrcpyforandroid.services.fetchConnectedDeviceInfo
import io.github.miuzarte.scrcpyforandroid.services.loadDevicePageSettings import io.github.miuzarte.scrcpyforandroid.services.loadDevicePageSettings
import io.github.miuzarte.scrcpyforandroid.services.loadQuickDevices
import io.github.miuzarte.scrcpyforandroid.services.parseQuickTarget import io.github.miuzarte.scrcpyforandroid.services.parseQuickTarget
import io.github.miuzarte.scrcpyforandroid.services.saveQuickDevices
import io.github.miuzarte.scrcpyforandroid.services.saveDevicePageSettings 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.updateQuickDeviceNameIfEmpty
import io.github.miuzarte.scrcpyforandroid.services.upsertQuickDevice import io.github.miuzarte.scrcpyforandroid.services.upsertQuickDevice
import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn
import io.github.miuzarte.scrcpyforandroid.widgets.ConfigPanel import io.github.miuzarte.scrcpyforandroid.widgets.ConfigPanel
import io.github.miuzarte.scrcpyforandroid.widgets.PairingCard
import io.github.miuzarte.scrcpyforandroid.widgets.DeviceEditorScreen import io.github.miuzarte.scrcpyforandroid.widgets.DeviceEditorScreen
import io.github.miuzarte.scrcpyforandroid.widgets.DeviceTile import io.github.miuzarte.scrcpyforandroid.widgets.DeviceTile
import io.github.miuzarte.scrcpyforandroid.widgets.LogsPanel import io.github.miuzarte.scrcpyforandroid.widgets.LogsPanel
import io.github.miuzarte.scrcpyforandroid.widgets.PairingCard
import io.github.miuzarte.scrcpyforandroid.widgets.PreviewCard import io.github.miuzarte.scrcpyforandroid.widgets.PreviewCard
import io.github.miuzarte.scrcpyforandroid.widgets.QuickConnectCard import io.github.miuzarte.scrcpyforandroid.widgets.QuickConnectCard
import io.github.miuzarte.scrcpyforandroid.widgets.SectionSmallTitle import io.github.miuzarte.scrcpyforandroid.widgets.SectionSmallTitle
@@ -66,9 +58,6 @@ import io.github.miuzarte.scrcpyforandroid.widgets.StatusCard
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonCard import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonCard
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -77,11 +66,10 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import top.yukonga.miuix.kmp.basic.ScrollBehavior import top.yukonga.miuix.kmp.basic.ScrollBehavior
import top.yukonga.miuix.kmp.basic.SnackbarHostState import top.yukonga.miuix.kmp.basic.SnackbarHostState
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.extra.SuperBottomSheet import top.yukonga.miuix.kmp.extra.SuperBottomSheet
import top.yukonga.miuix.kmp.theme.MiuixTheme import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.math.roundToInt import kotlin.math.roundToInt
private const val ADB_CONNECT_TIMEOUT_MS = 3_000L private const val ADB_CONNECT_TIMEOUT_MS = 3_000L
@@ -90,38 +78,40 @@ private const val ADB_KEEPALIVE_TIMEOUT_MS = 2_000L
private const val DEVICE_SHORTCUT_SEPARATOR = "\u001F" private const val DEVICE_SHORTCUT_SEPARATOR = "\u001F"
private const val LOG_TAG = "DevicePage" private const val LOG_TAG = "DevicePage"
private val DeviceShortcutStateListSaver = listSaver<androidx.compose.runtime.snapshots.SnapshotStateList<DeviceShortcut>, String>( private val DeviceShortcutStateListSaver =
save = { list -> listSaver<androidx.compose.runtime.snapshots.SnapshotStateList<DeviceShortcut>, String>(
list.map { item -> save = { list ->
listOf( list.map { item ->
item.id, listOf(
item.name, item.id,
item.host, item.name,
item.port.toString(), item.host,
if (item.online) "1" else "0", item.port.toString(),
).joinToString(DEVICE_SHORTCUT_SEPARATOR) if (item.online) "1" else "0",
} ).joinToString(DEVICE_SHORTCUT_SEPARATOR)
}, }
restore = { saved -> },
saved.mapNotNull { line -> restore = { saved ->
val parts = line.split(DEVICE_SHORTCUT_SEPARATOR) saved.mapNotNull { line ->
if (parts.size != 5) return@mapNotNull null val parts = line.split(DEVICE_SHORTCUT_SEPARATOR)
val port = parts[3].toIntOrNull() ?: return@mapNotNull null if (parts.size != 5) return@mapNotNull null
DeviceShortcut( val port = parts[3].toIntOrNull() ?: return@mapNotNull null
id = parts[0], DeviceShortcut(
name = parts[1], id = parts[0],
host = parts[2], name = parts[1],
port = port, host = parts[2],
online = parts[4] == "1", port = port,
) online = parts[4] == "1",
}.toMutableStateList() )
}, }.toMutableStateList()
) },
)
private val StringStateListSaver = listSaver<androidx.compose.runtime.snapshots.SnapshotStateList<String>, String>( private val StringStateListSaver =
save = { it.toList() }, listSaver<androidx.compose.runtime.snapshots.SnapshotStateList<String>, String>(
restore = { it.toMutableStateList() }, save = { it.toList() },
) restore = { it.toMutableStateList() },
)
@Composable @Composable
fun DeviceTabScreen( fun DeviceTabScreen(
@@ -231,7 +221,7 @@ fun DeviceTabScreen(
var statusLine by rememberSaveable { mutableStateOf("未连接") } var statusLine by rememberSaveable { mutableStateOf("未连接") }
var adbConnected by rememberSaveable { mutableStateOf(false) } var adbConnected by rememberSaveable { mutableStateOf(false) }
var currentTargetHost by rememberSaveable { mutableStateOf("") } var currentTargetHost by rememberSaveable { mutableStateOf("") }
var currentTargetPort by rememberSaveable { mutableIntStateOf(AppDefaults.DefaultAdbPort) } var currentTargetPort by rememberSaveable { mutableIntStateOf(AppDefaults.ADB_PORT) }
var connectedDeviceLabel by rememberSaveable { mutableStateOf("未连接") } var connectedDeviceLabel by rememberSaveable { mutableStateOf("未连接") }
var sessionInfoWidth by rememberSaveable { mutableIntStateOf(0) } var sessionInfoWidth by rememberSaveable { mutableIntStateOf(0) }
var sessionInfoHeight by rememberSaveable { mutableIntStateOf(0) } var sessionInfoHeight by rememberSaveable { mutableIntStateOf(0) }
@@ -241,7 +231,13 @@ fun DeviceTabScreen(
var sessionInfo by remember { var sessionInfo by remember {
mutableStateOf<ScrcpySessionInfo?>(null) mutableStateOf<ScrcpySessionInfo?>(null)
} }
LaunchedEffect(sessionInfoWidth, sessionInfoHeight, sessionInfoDeviceName, sessionInfoCodec, sessionInfoControlEnabled) { LaunchedEffect(
sessionInfoWidth,
sessionInfoHeight,
sessionInfoDeviceName,
sessionInfoCodec,
sessionInfoControlEnabled
) {
sessionInfo = if (sessionInfoDeviceName.isNotBlank()) { sessionInfo = if (sessionInfoDeviceName.isNotBlank()) {
ScrcpySessionInfo( ScrcpySessionInfo(
width = sessionInfoWidth, width = sessionInfoWidth,
@@ -261,18 +257,22 @@ fun DeviceTabScreen(
var adbConnecting by rememberSaveable { mutableStateOf(false) } var adbConnecting by rememberSaveable { mutableStateOf(false) }
var connectHost by rememberSaveable { mutableStateOf("") } var connectHost by rememberSaveable { mutableStateOf("") }
var connectPort by rememberSaveable { mutableStateOf(AppDefaults.DefaultAdbPort.toString()) } var connectPort by rememberSaveable { mutableStateOf(AppDefaults.ADB_PORT.toString()) }
var quickConnectInput by rememberSaveable { mutableStateOf(initialSettings.quickConnectInput) } var quickConnectInput by rememberSaveable { mutableStateOf(initialSettings.quickConnectInput) }
var audioForwardingSupported by rememberSaveable { mutableStateOf(true) } var audioForwardingSupported by rememberSaveable { mutableStateOf(true) }
var cameraMirroringSupported by rememberSaveable { mutableStateOf(true) } var cameraMirroringSupported by rememberSaveable { mutableStateOf(true) }
var bitRateMbps by rememberSaveable { mutableFloatStateOf(initialSettings.bitRateMbps) } var bitRateMbps by rememberSaveable { mutableFloatStateOf(initialSettings.videoBitRateMbps) }
var bitRateInput by rememberSaveable { mutableStateOf(initialSettings.bitRateInput) } var bitRateInput by rememberSaveable { mutableStateOf(initialSettings.videoBitRateInput) }
var audioBitRateKbps by rememberSaveable { mutableIntStateOf(initialSettings.audioBitRateKbps) } var audioBitRateKbps by rememberSaveable { mutableIntStateOf(initialSettings.audioBitRateKbps) }
val currentTarget = if (currentTargetHost.isNotBlank()) ConnectionTarget(currentTargetHost, currentTargetPort) else null val currentTarget = if (currentTargetHost.isNotBlank()) ConnectionTarget(
currentTargetHost,
currentTargetPort
) else null
val eventLog = rememberSaveable(saver = StringStateListSaver) { mutableStateListOf() } val eventLog = rememberSaveable(saver = StringStateListSaver) { mutableStateListOf() }
val quickDevices = rememberSaveable(saver = DeviceShortcutStateListSaver) { mutableStateListOf() } val quickDevices =
rememberSaveable(saver = DeviceShortcutStateListSaver) { mutableStateListOf() }
LaunchedEffect(eventLog.size) { LaunchedEffect(eventLog.size) {
onCanClearLogsChange(eventLog.isNotEmpty()) onCanClearLogsChange(eventLog.isNotEmpty())
@@ -282,13 +282,25 @@ fun DeviceTabScreen(
val timestamp = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date()) val timestamp = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())
val line = "[$timestamp] $message" val line = "[$timestamp] $message"
eventLog.add(0, line) eventLog.add(0, line)
if (eventLog.size > AppDefaults.MaxEventLogLines) { if (eventLog.size > AppDefaults.EVENT_LOG_LINES) {
eventLog.removeRange(AppDefaults.MaxEventLogLines, eventLog.size) eventLog.removeRange(AppDefaults.EVENT_LOG_LINES, eventLog.size)
} }
when (level) { when (level) {
Log.ERROR -> if (error != null) Log.e(LOG_TAG, message, error) else Log.e(LOG_TAG, message) Log.ERROR -> if (error != null) Log.e(LOG_TAG, message, error) else Log.e(
Log.WARN -> if (error != null) Log.w(LOG_TAG, message, error) else Log.w(LOG_TAG, message) LOG_TAG,
Log.DEBUG -> if (error != null) Log.d(LOG_TAG, message, error) else Log.d(LOG_TAG, message) message
)
Log.WARN -> if (error != null) Log.w(LOG_TAG, message, error) else Log.w(
LOG_TAG,
message
)
Log.DEBUG -> if (error != null) Log.d(LOG_TAG, message, error) else Log.d(
LOG_TAG,
message
)
else -> if (error != null) Log.i(LOG_TAG, message, error) else Log.i(LOG_TAG, message) else -> if (error != null) Log.i(LOG_TAG, message, error) else Log.i(LOG_TAG, message)
} }
} }
@@ -298,13 +310,19 @@ fun DeviceTabScreen(
audioForwardingSupported = audioSupported audioForwardingSupported = audioSupported
if (!audioSupported && audioEnabled) { if (!audioSupported && audioEnabled) {
onAudioEnabledChange(false) onAudioEnabledChange(false)
logEvent("设备 Android ${release.ifBlank { "?" }} (SDK $sdkInt) 不支持音频转发,已自动关闭", Log.WARN) logEvent(
"设备 Android ${release.ifBlank { "?" }} (SDK $sdkInt) 不支持音频转发,已自动关闭",
Log.WARN
)
} }
val cameraSupported = sdkInt !in 0..<31 val cameraSupported = sdkInt !in 0..<31
cameraMirroringSupported = cameraSupported cameraMirroringSupported = cameraSupported
if (!cameraSupported && videoSourcePreset == "camera") { if (!cameraSupported && videoSourcePreset == "camera") {
onVideoSourcePresetChange("display") onVideoSourcePresetChange("display")
logEvent("设备 Android ${release.ifBlank { "?" }} (SDK $sdkInt) 不支持 camera mirroring已切换为 display", Log.WARN) logEvent(
"设备 Android ${release.ifBlank { "?" }} (SDK $sdkInt) 不支持 camera mirroring已切换为 display",
Log.WARN
)
} }
} }
@@ -377,7 +395,7 @@ fun DeviceTabScreen(
fun refreshEncoderLists() { fun refreshEncoderLists() {
if (!adbConnected) return if (!adbConnected) return
val remotePath = serverRemotePath.trim().ifBlank { AppDefaults.DefaultServerRemotePath } val remotePath = serverRemotePath.trim().ifBlank { AppDefaults.SERVER_REMOTE_PATH }
runCatching { runCatching {
nativeCore.scrcpyListEncoders( nativeCore.scrcpyListEncoders(
customServerUri = customServerUri, customServerUri = customServerUri,
@@ -413,7 +431,7 @@ fun DeviceTabScreen(
fun refreshCameraSizeLists() { fun refreshCameraSizeLists() {
if (!adbConnected) return if (!adbConnected) return
val remotePath = serverRemotePath.trim().ifBlank { AppDefaults.DefaultServerRemotePath } val remotePath = serverRemotePath.trim().ifBlank { AppDefaults.SERVER_REMOTE_PATH }
runCatching { runCatching {
nativeCore.scrcpyListCameraSizes( nativeCore.scrcpyListCameraSizes(
customServerUri = customServerUri, customServerUri = customServerUri,
@@ -470,20 +488,14 @@ fun DeviceTabScreen(
LaunchedEffect( LaunchedEffect(
quickConnectInput, quickConnectInput,
audioBitRateKbps,
bitRateMbps, bitRateMbps,
bitRateInput, bitRateInput,
audioBitRateKbps, turnScreenOff,
maxSizeInput,
maxFpsInput,
noControl, noControl,
videoEncoder, noVideo,
videoCodecOptions,
audioEncoder,
audioCodecOptions,
audioDup,
audioSourcePreset,
audioSourceCustom,
videoSourcePreset, videoSourcePreset,
displayIdInput,
cameraIdInput, cameraIdInput,
cameraFacingPreset, cameraFacingPreset,
cameraSizePreset, cameraSizePreset,
@@ -491,14 +503,20 @@ fun DeviceTabScreen(
cameraArInput, cameraArInput,
cameraFpsInput, cameraFpsInput,
cameraHighSpeed, cameraHighSpeed,
audioSourcePreset,
audioSourceCustom,
audioDup,
noAudioPlayback, noAudioPlayback,
noVideo,
requireAudio, requireAudio,
turnScreenOff, maxSizeInput,
maxFpsInput,
videoEncoder,
videoCodecOptions,
audioEncoder,
audioCodecOptions,
newDisplayWidth, newDisplayWidth,
newDisplayHeight, newDisplayHeight,
newDisplayDpi, newDisplayDpi,
displayIdInput,
cropWidth, cropWidth,
cropHeight, cropHeight,
cropX, cropX,
@@ -508,35 +526,36 @@ fun DeviceTabScreen(
context, context,
DevicePageSettings( DevicePageSettings(
quickConnectInput = quickConnectInput, quickConnectInput = quickConnectInput,
bitRateMbps = bitRateMbps,
bitRateInput = bitRateInput,
audioBitRateKbps = audioBitRateKbps, audioBitRateKbps = audioBitRateKbps,
maxSizeInput = maxSizeInput, audioBitRateInput = audioBitRateKbps.toString(),
maxFpsInput = maxFpsInput, videoBitRateMbps = bitRateMbps,
videoBitRateInput = bitRateInput,
turnScreenOff = turnScreenOff,
noControl = noControl, noControl = noControl,
videoEncoder = videoEncoder, noVideo = noVideo,
videoCodecOptions = videoCodecOptions,
audioEncoder = audioEncoder,
audioCodecOptions = audioCodecOptions,
audioDup = audioDup,
audioSourcePreset = audioSourcePreset,
audioSourceCustom = audioSourceCustom,
videoSourcePreset = videoSourcePreset, videoSourcePreset = videoSourcePreset,
displayIdInput = displayIdInput,
cameraIdInput = cameraIdInput, cameraIdInput = cameraIdInput,
cameraFacingPreset = cameraFacingPreset, cameraFacingPreset = cameraFacingPreset,
cameraSizePreset = cameraSizePreset, cameraSizePreset = cameraSizePreset,
cameraSizeCustom = cameraSizeCustom, cameraSizeCustom = cameraSizeCustom,
cameraArInput = cameraArInput, cameraAr = cameraArInput,
cameraFpsInput = cameraFpsInput, cameraFps = cameraFpsInput,
cameraHighSpeed = cameraHighSpeed, cameraHighSpeed = cameraHighSpeed,
audioSourcePreset = audioSourcePreset,
audioSourceCustom = audioSourceCustom,
audioDup = audioDup,
noAudioPlayback = noAudioPlayback, noAudioPlayback = noAudioPlayback,
noVideo = noVideo,
requireAudio = requireAudio, requireAudio = requireAudio,
turnScreenOff = turnScreenOff, maxSizeInput = maxSizeInput,
maxFpsInput = maxFpsInput,
videoEncoder = videoEncoder,
videoCodecOptions = videoCodecOptions,
audioEncoder = audioEncoder,
audioCodecOptions = audioCodecOptions,
newDisplayWidth = newDisplayWidth, newDisplayWidth = newDisplayWidth,
newDisplayHeight = newDisplayHeight, newDisplayHeight = newDisplayHeight,
newDisplayDpi = newDisplayDpi, newDisplayDpi = newDisplayDpi,
displayIdInput = displayIdInput,
cropWidth = cropWidth, cropWidth = cropWidth,
cropHeight = cropHeight, cropHeight = cropHeight,
cropX = cropX, cropX = cropX,
@@ -747,7 +766,8 @@ fun DeviceTabScreen(
itemsIndexed(quickDevices, key = { _, device -> device.id }) { _, device -> itemsIndexed(quickDevices, key = { _, device -> device.id }) { _, device ->
val host = device.host val host = device.host
val port = device.port val port = device.port
val isConnectedTarget = adbConnected && currentTarget?.host == host && currentTarget.port == port val isConnectedTarget =
adbConnected && currentTarget?.host == host && currentTarget.port == port
DeviceTile( DeviceTile(
device = device, device = device,
@@ -768,7 +788,7 @@ fun DeviceTabScreen(
nativeCore.adbDisconnect() nativeCore.adbDisconnect()
adbConnected = false adbConnected = false
currentTargetHost = "" currentTargetHost = ""
currentTargetPort = AppDefaults.DefaultAdbPort currentTargetPort = AppDefaults.ADB_PORT
audioForwardingSupported = true audioForwardingSupported = true
cameraMirroringSupported = true cameraMirroringSupported = true
sessionInfo = null sessionInfo = null
@@ -809,7 +829,13 @@ fun DeviceTabScreen(
enabled = !adbConnecting, enabled = !adbConnecting,
onAddDevice = { onAddDevice = {
val target = parseQuickTarget(quickConnectInput) ?: return@QuickConnectCard val target = parseQuickTarget(quickConnectInput) ?: return@QuickConnectCard
upsertQuickDevice(context, quickDevices, target.host, target.port, online = false) upsertQuickDevice(
context,
quickDevices,
target.host,
target.port,
online = false
)
scope.launch { scope.launch {
snack.showSnackbar("已添加设备: ${target.host}:${target.port}") snack.showSnackbar("已添加设备: ${target.host}:${target.port}")
} }
@@ -840,10 +866,13 @@ fun DeviceTabScreen(
runBusy("执行配对") { runBusy("执行配对") {
val ok = nativeCore.adbPair( val ok = nativeCore.adbPair(
host.trim(), host.trim(),
port.toIntOrNull() ?: AppDefaults.DefaultAdbPort, port.toIntOrNull() ?: AppDefaults.ADB_PORT,
code.trim(), code.trim(),
) )
logEvent(if (ok) "配对成功" else "配对失败", if (ok) Log.INFO else Log.ERROR) logEvent(
if (ok) "配对成功" else "配对失败",
if (ok) Log.INFO else Log.ERROR
)
scope.launch { scope.launch {
snack.showSnackbar(if (ok) "配对成功" else "配对失败") snack.showSnackbar(if (ok) "配对成功" else "配对失败")
} }

View File

@@ -2,8 +2,8 @@ package io.github.miuzarte.scrcpyforandroid.pages
import android.app.Activity import android.app.Activity
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -21,8 +21,8 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade
import io.github.miuzarte.scrcpyforandroid.ScrcpySessionInfo import io.github.miuzarte.scrcpyforandroid.ScrcpySessionInfo
import io.github.miuzarte.scrcpyforandroid.widgets.FullscreenControlScreen
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics
import io.github.miuzarte.scrcpyforandroid.widgets.FullscreenControlScreen
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonBar import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonBar
import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.Scaffold
@@ -56,7 +56,9 @@ fun FullscreenControlPage(
moreActions = virtualButtonLayout.second, moreActions = virtualButtonLayout.second,
) )
} }
val initialOrientation = remember(activity) { activity?.requestedOrientation ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } val initialOrientation = remember(activity) {
activity?.requestedOrientation ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
var session by remember(launch) { var session by remember(launch) {
mutableStateOf( mutableStateOf(
ScrcpySessionInfo( ScrcpySessionInfo(
@@ -88,12 +90,15 @@ fun FullscreenControlPage(
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
val controller = WindowInsetsControllerCompat(window, window.decorView) val controller = WindowInsetsControllerCompat(window, window.decorView)
controller.hide(WindowInsetsCompat.Type.systemBars()) controller.hide(WindowInsetsCompat.Type.systemBars())
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE controller.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
} }
onDispose { onDispose {
val restoreWindow = activity?.window val restoreWindow = activity?.window
if (restoreWindow != null) { if (restoreWindow != null) {
WindowInsetsControllerCompat(restoreWindow, restoreWindow.decorView).show(WindowInsetsCompat.Type.systemBars()) WindowInsetsControllerCompat(restoreWindow, restoreWindow.decorView).show(
WindowInsetsCompat.Type.systemBars()
)
WindowCompat.setDecorFitsSystemWindows(restoreWindow, true) WindowCompat.setDecorFitsSystemWindows(restoreWindow, true)
} }
} }

View File

@@ -6,8 +6,8 @@ import androidx.activity.compose.PredictiveBackHandler
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.spring import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
@@ -34,7 +34,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberDecoratedNavEntries import androidx.navigation3.runtime.rememberDecoratedNavEntries
@@ -92,14 +91,18 @@ fun MainPage() {
val initialDeviceSettings = remember(context) { loadDevicePageSettings(context) } val initialDeviceSettings = remember(context) { loadDevicePageSettings(context) }
val snackHostState = remember { SnackbarHostState() } val snackHostState = remember { SnackbarHostState() }
val tabs = remember { MainTabDestination.entries } val tabs = remember { MainTabDestination.entries }
val pagerState = rememberPagerState(initialPage = MainTabDestination.Device.ordinal, pageCount = { tabs.size }) val pagerState = rememberPagerState(
initialPage = MainTabDestination.Device.ordinal,
pageCount = { tabs.size })
val currentTab = tabs[pagerState.currentPage] val currentTab = tabs[pagerState.currentPage]
val saveableStateHolder = rememberSaveableStateHolder() val saveableStateHolder = rememberSaveableStateHolder()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val rootBackStack = remember { mutableStateListOf<NavKey>(RootScreen.Home) } val rootBackStack = remember { mutableStateListOf<NavKey>(RootScreen.Home) }
val currentRootScreen = rootBackStack.lastOrNull() as? RootScreen ?: RootScreen.Home val currentRootScreen = rootBackStack.lastOrNull() as? RootScreen ?: RootScreen.Home
val deviceScrollBehavior = MiuixScrollBehavior(canScroll = { currentTab == MainTabDestination.Device }) val deviceScrollBehavior =
val settingsScrollBehavior = MiuixScrollBehavior(canScroll = { currentTab == MainTabDestination.Settings }) MiuixScrollBehavior(canScroll = { currentTab == MainTabDestination.Device })
val settingsScrollBehavior =
MiuixScrollBehavior(canScroll = { currentTab == MainTabDestination.Settings })
val advancedScrollBehavior = MiuixScrollBehavior( val advancedScrollBehavior = MiuixScrollBehavior(
canScroll = { canScroll = {
currentRootScreen is RootScreen.Advanced || currentRootScreen is RootScreen.VirtualButtonOrder currentRootScreen is RootScreen.Advanced || currentRootScreen is RootScreen.VirtualButtonOrder
@@ -110,27 +113,23 @@ fun MainPage() {
restore = { restored -> restored.toList() }, 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 themeBaseIndex by rememberSaveable { mutableIntStateOf(initialSettings.themeBaseIndex) }
var monetEnabled by rememberSaveable { mutableStateOf(initialSettings.monetEnabled) } var monetEnabled by rememberSaveable { mutableStateOf(initialSettings.monetEnabled) }
var fullscreenDebugInfoEnabled by rememberSaveable { mutableStateOf(initialSettings.fullscreenDebugInfoEnabled) } var fullscreenDebugInfoEnabled by rememberSaveable { mutableStateOf(initialSettings.fullscreenDebugInfoEnabled) }
var showFullscreenVirtualButtons by rememberSaveable { var showFullscreenVirtualButtons by rememberSaveable { mutableStateOf(initialSettings.showFullscreenVirtualButtons) }
mutableStateOf(initialSettings.showFullscreenVirtualButtons) var keepScreenOnWhenStreamingEnabled by rememberSaveable { mutableStateOf(initialSettings.keepScreenOnWhenStreamingEnabled) }
} var devicePreviewCardHeightDp by rememberSaveable { mutableIntStateOf(initialSettings.devicePreviewCardHeightDp) }
var keepScreenOnWhenStreamingEnabled by rememberSaveable {
mutableStateOf(initialSettings.keepScreenOnWhenStreamingEnabled)
}
var virtualButtonsOutside by rememberSaveable(stateSaver = stringListSaver) { var virtualButtonsOutside by rememberSaveable(stateSaver = stringListSaver) {
mutableStateOf(VirtualButtonActions.parseStoredIds(initialSettings.virtualButtonsOutside)) mutableStateOf(VirtualButtonActions.parseStoredIds(initialSettings.virtualButtonsOutside))
} }
var virtualButtonsInMore by rememberSaveable(stateSaver = stringListSaver) { var virtualButtonsInMore by rememberSaveable(stateSaver = stringListSaver) {
mutableStateOf(VirtualButtonActions.parseStoredIds(initialSettings.virtualButtonsInMore)) mutableStateOf(VirtualButtonActions.parseStoredIds(initialSettings.virtualButtonsInMore))
} }
var devicePreviewCardHeightDp by rememberSaveable { mutableIntStateOf(initialSettings.devicePreviewCardHeightDp) }
var customServerUri by rememberSaveable { mutableStateOf(initialSettings.customServerUri) } var customServerUri by rememberSaveable { mutableStateOf(initialSettings.customServerUri) }
var serverRemotePath by rememberSaveable { mutableStateOf(initialSettings.serverRemotePath) } var serverRemotePath by rememberSaveable { mutableStateOf(initialSettings.serverRemotePath) }
var videoCodec by rememberSaveable { mutableStateOf(initialSettings.videoCodec) }
var audioEnabled by rememberSaveable { mutableStateOf(initialSettings.audioEnabled) }
var audioCodec by rememberSaveable { mutableStateOf(initialSettings.audioCodec) }
var adbKeyName by rememberSaveable { mutableStateOf(initialSettings.adbKeyName) } var adbKeyName by rememberSaveable { mutableStateOf(initialSettings.adbKeyName) }
var noControl by rememberSaveable { mutableStateOf(initialDeviceSettings.noControl) } var noControl by rememberSaveable { mutableStateOf(initialDeviceSettings.noControl) }
var videoEncoder by rememberSaveable { mutableStateOf(initialDeviceSettings.videoEncoder) } var videoEncoder by rememberSaveable { mutableStateOf(initialDeviceSettings.videoEncoder) }
@@ -145,8 +144,8 @@ fun MainPage() {
var cameraFacingPreset by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraFacingPreset) } var cameraFacingPreset by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraFacingPreset) }
var cameraSizePreset by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraSizePreset) } var cameraSizePreset by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraSizePreset) }
var cameraSizeCustom by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraSizeCustom) } var cameraSizeCustom by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraSizeCustom) }
var cameraArInput by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraArInput) } var cameraArInput by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraAr) }
var cameraFpsInput by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraFpsInput) } var cameraFpsInput by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraFps) }
var cameraHighSpeed by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraHighSpeed) } var cameraHighSpeed by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraHighSpeed) }
var noAudioPlayback by rememberSaveable { mutableStateOf(initialDeviceSettings.noAudioPlayback) } var noAudioPlayback by rememberSaveable { mutableStateOf(initialDeviceSettings.noAudioPlayback) }
var noVideo by rememberSaveable { mutableStateOf(initialDeviceSettings.noVideo) } var noVideo by rememberSaveable { mutableStateOf(initialDeviceSettings.noVideo) }
@@ -178,47 +177,48 @@ fun MainPage() {
val themeController = remember(themeMode) { ThemeController(colorSchemeMode = themeMode) } val themeController = remember(themeMode) { ThemeController(colorSchemeMode = themeMode) }
LaunchedEffect( LaunchedEffect(
audioEnabled,
audioCodec,
videoCodec,
themeBaseIndex, themeBaseIndex,
monetEnabled, monetEnabled,
fullscreenDebugInfoEnabled, fullscreenDebugInfoEnabled,
showFullscreenVirtualButtons, showFullscreenVirtualButtons,
keepScreenOnWhenStreamingEnabled, keepScreenOnWhenStreamingEnabled,
devicePreviewCardHeightDp,
virtualButtonsOutside, virtualButtonsOutside,
virtualButtonsInMore, virtualButtonsInMore,
devicePreviewCardHeightDp,
customServerUri, customServerUri,
serverRemotePath, serverRemotePath,
videoCodec,
audioEnabled,
audioCodec,
adbKeyName, adbKeyName,
) { ) {
saveMainSettings( saveMainSettings(
context, context,
MainSettings( MainSettings(
audioEnabled = audioEnabled,
audioCodec = audioCodec,
videoCodec = videoCodec,
themeBaseIndex = themeBaseIndex, themeBaseIndex = themeBaseIndex,
monetEnabled = monetEnabled, monetEnabled = monetEnabled,
fullscreenDebugInfoEnabled = fullscreenDebugInfoEnabled, fullscreenDebugInfoEnabled = fullscreenDebugInfoEnabled,
showFullscreenVirtualButtons = showFullscreenVirtualButtons, showFullscreenVirtualButtons = showFullscreenVirtualButtons,
keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled, keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled,
devicePreviewCardHeightDp = devicePreviewCardHeightDp,
virtualButtonsOutside = VirtualButtonActions.encodeStoredIds(virtualButtonsOutside), virtualButtonsOutside = VirtualButtonActions.encodeStoredIds(virtualButtonsOutside),
virtualButtonsInMore = VirtualButtonActions.encodeStoredIds(virtualButtonsInMore), virtualButtonsInMore = VirtualButtonActions.encodeStoredIds(virtualButtonsInMore),
devicePreviewCardHeightDp = devicePreviewCardHeightDp,
customServerUri = customServerUri, customServerUri = customServerUri,
serverRemotePath = serverRemotePath, serverRemotePath = serverRemotePath,
videoCodec = videoCodec,
audioEnabled = audioEnabled,
audioCodec = audioCodec,
adbKeyName = adbKeyName, adbKeyName = adbKeyName,
), ),
) )
} }
LaunchedEffect(adbKeyName) { LaunchedEffect(adbKeyName) {
nativeCore.setAdbKeyName(adbKeyName.ifBlank { AppDefaults.DefaultAdbKeyName }) nativeCore.setAdbKeyName(adbKeyName.ifBlank { AppDefaults.ADB_KEY_NAME })
} }
val canNavigateBack = rootBackStack.size > 1 || pagerState.currentPage != MainTabDestination.Device.ordinal val canNavigateBack =
rootBackStack.size > 1 || pagerState.currentPage != MainTabDestination.Device.ordinal
fun popRoot() { fun popRoot() {
if (rootBackStack.size > 1) { if (rootBackStack.size > 1) {
@@ -234,8 +234,8 @@ fun MainPage() {
pagerState.animateScrollToPage( pagerState.animateScrollToPage(
page = MainTabDestination.Device.ordinal, page = MainTabDestination.Device.ordinal,
animationSpec = spring( animationSpec = spring(
dampingRatio = UiMotion.PageSwitchDampingRatio, dampingRatio = UiMotion.PAGE_SWITCH_DAMPING_RATIO,
stiffness = UiMotion.PageSwitchStiffness, stiffness = UiMotion.PAGE_SWITCH_STIFFNESS,
), ),
) )
} }
@@ -251,403 +251,424 @@ fun MainPage() {
} }
} }
val picker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? -> val picker =
if (uri == null) return@rememberLauncherForActivityResult rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? ->
runCatching { if (uri == null) return@rememberLauncherForActivityResult
context.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) runCatching {
context.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
}
customServerUri = uri.toString()
} }
customServerUri = uri.toString()
}
val rootEntryProvider = entryProvider<NavKey> { val rootEntryProvider = entryProvider<NavKey> {
entry(RootScreen.Home) { entry(RootScreen.Home) {
Scaffold( Scaffold(
bottomBar = { bottomBar = {
NavigationBar { NavigationBar {
tabs.forEach { tab -> tabs.forEach { tab ->
NavigationBarItem( NavigationBarItem(
selected = currentTab == tab, selected = currentTab == tab,
onClick = { onClick = {
scope.launch { scope.launch {
pagerState.animateScrollToPage( pagerState.animateScrollToPage(
page = tab.ordinal, page = tab.ordinal,
animationSpec = spring( animationSpec = spring(
dampingRatio = UiMotion.PageSwitchDampingRatio, dampingRatio = UiMotion.PAGE_SWITCH_DAMPING_RATIO,
stiffness = UiMotion.PageSwitchStiffness, stiffness = UiMotion.PAGE_SWITCH_STIFFNESS,
), ),
)
}
},
icon = tab.icon,
label = tab.label,
)
}
}
},
snackbarHost = { SnackbarHost(snackHostState) },
) { contentPadding ->
HorizontalPager(
modifier = Modifier
.fillMaxSize()
.padding(bottom = contentPadding.calculateBottomPadding()),
state = pagerState,
beyondViewportPageCount = 1,
) { page ->
val tab = tabs[page]
saveableStateHolder.SaveableStateProvider(tab.name) {
when (tab) {
MainTabDestination.Device -> Scaffold(
topBar = {
TopAppBar(
title = tab.title,
actions = {
IconButton(
onClick = { showDeviceMenu = true },
holdDownState = showDeviceMenu,
) {
Icon(
Icons.Default.MoreVert,
contentDescription = "更多"
)
}
DeviceMenuPopup(
show = showDeviceMenu,
canClearLogs = canClearLogs,
onDismissRequest = { showDeviceMenu = false },
onReorderDevices = {
openReorderDevicesAction?.invoke()
showDeviceMenu = false
},
onOpenVirtualButtonOrder = {
rootBackStack.add(RootScreen.VirtualButtonOrder)
showDeviceMenu = false
},
onClearLogs = {
clearLogsAction?.invoke()
showDeviceMenu = false
},
) )
},
scrollBehavior = deviceScrollBehavior,
)
},
) { pagePadding ->
DeviceTabScreen(
contentPadding = pagePadding,
nativeCore = nativeCore,
snack = snackHostState,
scrollBehavior = deviceScrollBehavior,
keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled,
virtualButtonsOutside = virtualButtonsOutside,
virtualButtonsInMore = virtualButtonsInMore,
customServerUri = customServerUri,
serverRemotePath = serverRemotePath,
onServerRemotePathChange = { serverRemotePath = it },
videoCodec = videoCodec,
onVideoCodecChange = { videoCodec = it },
audioEnabled = audioEnabled,
onAudioEnabledChange = { audioEnabled = it },
audioCodec = audioCodec,
onAudioCodecChange = { audioCodec = it },
noControl = noControl,
onNoControlChange = {
noControl = it
if (it) {
turnScreenOff = false
} }
}, },
icon = tab.icon, videoEncoder = videoEncoder,
label = tab.label, 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()
videoEncoderOptions.addAll(it)
},
onVideoEncoderTypeMapChange = {
videoEncoderTypeMap.clear()
videoEncoderTypeMap.putAll(it)
},
audioEncoderOptions = audioEncoderOptions,
onAudioEncoderOptionsChange = {
audioEncoderOptions.clear()
audioEncoderOptions.addAll(it)
},
onAudioEncoderTypeMapChange = {
audioEncoderTypeMap.clear()
audioEncoderTypeMap.putAll(it)
},
cameraSizeOptions = cameraSizeOptions,
onCameraSizeOptionsChange = {
cameraSizeOptions.clear()
cameraSizeOptions.addAll(it)
},
onSessionStartedChange = { sessionStarted = it },
onRefreshEncodersActionChange = { refreshEncodersAction = it },
onRefreshCameraSizesActionChange = {
refreshCameraSizesAction = it
},
onClearLogsActionChange = { clearLogsAction = it },
onCanClearLogsChange = { canClearLogs = it },
onOpenReorderDevicesActionChange = {
openReorderDevicesAction = it
},
onOpenAdvancedPage = { rootBackStack.add(RootScreen.Advanced) },
onOpenFullscreenPage = { session ->
rootBackStack.add(
RootScreen.Fullscreen(
launch = FullscreenControlLaunch(
deviceName = session.deviceName,
width = session.width,
height = session.height,
codec = session.codec,
),
),
)
},
previewCardHeightDp = devicePreviewCardHeightDp,
) )
} }
}
},
snackbarHost = { SnackbarHost(snackHostState) },
) { contentPadding ->
HorizontalPager(
modifier = Modifier
.fillMaxSize()
.padding(bottom = contentPadding.calculateBottomPadding()),
state = pagerState,
beyondViewportPageCount = 1,
) { page ->
val tab = tabs[page]
saveableStateHolder.SaveableStateProvider(tab.name) {
when (tab) {
MainTabDestination.Device -> Scaffold(
topBar = {
TopAppBar(
title = tab.title,
actions = {
IconButton(
onClick = { showDeviceMenu = true },
holdDownState = showDeviceMenu,
) {
Icon(Icons.Default.MoreVert, contentDescription = "更多")
}
DeviceMenuPopup(
show = showDeviceMenu,
canClearLogs = canClearLogs,
onDismissRequest = { showDeviceMenu = false },
onReorderDevices = {
openReorderDevicesAction?.invoke()
showDeviceMenu = false
},
onOpenVirtualButtonOrder = {
rootBackStack.add(RootScreen.VirtualButtonOrder)
showDeviceMenu = false
},
onClearLogs = {
clearLogsAction?.invoke()
showDeviceMenu = false
},
)
},
scrollBehavior = deviceScrollBehavior,
)
},
) { pagePadding ->
DeviceTabScreen(
contentPadding = pagePadding,
nativeCore = nativeCore,
snack = snackHostState,
scrollBehavior = deviceScrollBehavior,
keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled,
virtualButtonsOutside = virtualButtonsOutside,
virtualButtonsInMore = virtualButtonsInMore,
customServerUri = customServerUri,
serverRemotePath = serverRemotePath,
onServerRemotePathChange = { serverRemotePath = it },
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()
videoEncoderOptions.addAll(it)
},
onVideoEncoderTypeMapChange = {
videoEncoderTypeMap.clear()
videoEncoderTypeMap.putAll(it)
},
audioEncoderOptions = audioEncoderOptions,
onAudioEncoderOptionsChange = {
audioEncoderOptions.clear()
audioEncoderOptions.addAll(it)
},
onAudioEncoderTypeMapChange = {
audioEncoderTypeMap.clear()
audioEncoderTypeMap.putAll(it)
},
cameraSizeOptions = cameraSizeOptions,
onCameraSizeOptionsChange = {
cameraSizeOptions.clear()
cameraSizeOptions.addAll(it)
},
onSessionStartedChange = { sessionStarted = it },
onRefreshEncodersActionChange = { refreshEncodersAction = it },
onRefreshCameraSizesActionChange = { refreshCameraSizesAction = it },
onClearLogsActionChange = { clearLogsAction = it },
onCanClearLogsChange = { canClearLogs = it },
onOpenReorderDevicesActionChange = { openReorderDevicesAction = it },
onOpenAdvancedPage = { rootBackStack.add(RootScreen.Advanced) },
onOpenFullscreenPage = { session ->
rootBackStack.add(
RootScreen.Fullscreen(
launch = FullscreenControlLaunch(
deviceName = session.deviceName,
width = session.width,
height = session.height,
codec = session.codec,
),
),
)
},
previewCardHeightDp = devicePreviewCardHeightDp,
)
}
MainTabDestination.Settings -> Scaffold( MainTabDestination.Settings -> Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = tab.title, title = tab.title,
scrollBehavior = settingsScrollBehavior,
)
},
) { pagePadding ->
SettingsScreen(
contentPadding = pagePadding,
themeBaseIndex = themeBaseIndex,
onThemeBaseIndexChange = { themeBaseIndex = it },
monetEnabled = monetEnabled,
onMonetEnabledChange = { monetEnabled = it },
fullscreenDebugInfoEnabled = fullscreenDebugInfoEnabled,
onFullscreenDebugInfoEnabledChange = { fullscreenDebugInfoEnabled = it },
showFullscreenVirtualButtons = showFullscreenVirtualButtons,
onShowFullscreenVirtualButtonsChange = { showFullscreenVirtualButtons = it },
keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled,
onKeepScreenOnWhenStreamingEnabledChange = {
keepScreenOnWhenStreamingEnabled = it
},
onOpenReorderDevices = { openReorderDevicesAction?.invoke() },
onOpenVirtualButtonOrder = { rootBackStack.add(RootScreen.VirtualButtonOrder) },
devicePreviewCardHeightDp = devicePreviewCardHeightDp,
onDevicePreviewCardHeightDpChange = {
devicePreviewCardHeightDp = it.coerceAtLeast(120)
},
customServerUri = customServerUri,
onPickServer = {
picker.launch(arrayOf("application/java-archive", "application/octet-stream", "*/*"))
},
onClearServer = { customServerUri = null },
serverRemotePath = serverRemotePath,
onServerRemotePathChange = { serverRemotePath = it },
adbKeyName = adbKeyName,
onAdbKeyNameChange = { adbKeyName = it },
scrollBehavior = settingsScrollBehavior, scrollBehavior = settingsScrollBehavior,
) )
} },
) { 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)
},
customServerUri = customServerUri,
onPickServer = {
picker.launch(
arrayOf(
"application/java-archive",
"application/octet-stream",
"*/*"
)
)
},
onClearServer = { customServerUri = null },
serverRemotePath = serverRemotePath,
onServerRemotePathChange = { serverRemotePath = it },
adbKeyName = adbKeyName,
onAdbKeyNameChange = { adbKeyName = it },
scrollBehavior = settingsScrollBehavior,
)
} }
} }
} }
} }
} }
}
entry(RootScreen.Advanced) { entry(RootScreen.Advanced) {
val videoEncoderDropdownItems = listOf("默认") + videoEncoderOptions val videoEncoderDropdownItems = listOf("默认") + videoEncoderOptions
val audioEncoderDropdownItems = listOf("默认") + audioEncoderOptions val audioEncoderDropdownItems = listOf("默认") + audioEncoderOptions
val videoEncoderIndex = (videoEncoderOptions.indexOf(videoEncoder) + 1).coerceAtLeast(0) val videoEncoderIndex = (videoEncoderOptions.indexOf(videoEncoder) + 1).coerceAtLeast(0)
val audioEncoderIndex = (audioEncoderOptions.indexOf(audioEncoder) + 1).coerceAtLeast(0) val audioEncoderIndex = (audioEncoderOptions.indexOf(audioEncoder) + 1).coerceAtLeast(0)
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = "高级参数", title = "高级参数",
navigationIcon = { navigationIcon = {
IconButton(onClick = { popRoot() }) { IconButton(onClick = { popRoot() }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "返回") Icon(
} Icons.AutoMirrored.Filled.ArrowBack,
}, contentDescription = "返回"
scrollBehavior = advancedScrollBehavior, )
)
},
snackbarHost = { SnackbarHost(snackHostState) },
) { pagePadding ->
AdvancedConfigPage(
contentPadding = pagePadding,
scrollBehavior = advancedScrollBehavior,
sessionStarted = sessionStarted,
snackbarHostState = snackHostState,
audioEnabled = audioEnabled,
noControl = noControl,
onNoControlChange = {
noControl = it
if (it) {
turnScreenOff = false
} }
}, },
audioDup = audioDup,
onAudioDupChange = { audioDup = it },
audioSourcePreset = audioSourcePreset,
onAudioSourcePresetChange = { audioSourcePreset = it },
audioSourceCustom = audioSourceCustom,
onAudioSourceCustomChange = { audioSourceCustom = it },
videoSourcePreset = videoSourcePreset,
onVideoSourcePresetChange = { videoSourcePreset = it },
cameraIdInput = cameraIdInput,
onCameraIdInputChange = { cameraIdInput = it },
cameraFacingPreset = cameraFacingPreset,
onCameraFacingPresetChange = { cameraFacingPreset = it },
cameraSizePreset = cameraSizePreset,
onCameraSizePresetChange = { cameraSizePreset = it },
cameraSizeCustom = cameraSizeCustom,
onCameraSizeCustomChange = { cameraSizeCustom = it },
cameraSizeDropdownItems = listOf("默认") + cameraSizeOptions + listOf("自定义"),
cameraSizeIndex = when (cameraSizePreset) {
"custom" -> cameraSizeOptions.size + 1
in cameraSizeOptions -> cameraSizeOptions.indexOf(cameraSizePreset) + 1
else -> 0
},
cameraArInput = cameraArInput,
onCameraArInputChange = { cameraArInput = it },
cameraFpsInput = cameraFpsInput,
onCameraFpsInputChange = { cameraFpsInput = it },
cameraHighSpeed = cameraHighSpeed,
onCameraHighSpeedChange = { cameraHighSpeed = it },
noAudioPlayback = noAudioPlayback,
onNoAudioPlaybackChange = { noAudioPlayback = it },
noVideo = noVideo,
onNoVideoChange = { noVideo = it },
requireAudio = requireAudio,
onRequireAudioChange = { requireAudio = it },
turnScreenOff = turnScreenOff,
onTurnScreenOffChange = { turnScreenOff = it },
maxSizeInput = maxSizeInput,
onMaxSizeInputChange = { maxSizeInput = it },
maxFpsInput = maxFpsInput,
onMaxFpsInputChange = { maxFpsInput = it },
videoEncoderDropdownItems = videoEncoderDropdownItems,
videoEncoderTypeMap = videoEncoderTypeMap,
videoEncoderIndex = videoEncoderIndex,
onVideoEncoderChange = { videoEncoder = it },
videoCodecOptions = videoCodecOptions,
onVideoCodecOptionsChange = { videoCodecOptions = it },
audioEncoderDropdownItems = audioEncoderDropdownItems,
audioEncoderTypeMap = audioEncoderTypeMap,
audioEncoderIndex = audioEncoderIndex,
onAudioEncoderChange = { audioEncoder = it },
audioCodecOptions = audioCodecOptions,
onAudioCodecOptionsChange = { audioCodecOptions = it },
onRefreshEncoders = { refreshEncodersAction?.invoke() },
onRefreshCameraSizes = { refreshCameraSizesAction?.invoke() },
newDisplayWidth = newDisplayWidth,
onNewDisplayWidthChange = { newDisplayWidth = it },
newDisplayHeight = newDisplayHeight,
onNewDisplayHeightChange = { newDisplayHeight = it },
newDisplayDpi = newDisplayDpi,
onNewDisplayDpiChange = { newDisplayDpi = it },
displayIdInput = displayIdInput,
onDisplayIdInputChange = { displayIdInput = it },
cropWidth = cropWidth,
onCropWidthChange = { cropWidth = it },
cropHeight = cropHeight,
onCropHeightChange = { cropHeight = it },
cropX = cropX,
onCropXChange = { cropX = it },
cropY = cropY,
onCropYChange = { cropY = it },
)
}
}
entry(RootScreen.VirtualButtonOrder) {
Scaffold(
modifier = Modifier.nestedScroll(advancedScrollBehavior.nestedScrollConnection),
topBar = {
TopAppBar(
title = "虚拟按钮排序",
navigationIcon = {
IconButton(onClick = { popRoot() }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "返回")
}
},
scrollBehavior = advancedScrollBehavior,
)
},
) { pagePadding ->
VirtualButtonOrderPage(
contentPadding = pagePadding,
scrollBehavior = advancedScrollBehavior, scrollBehavior = advancedScrollBehavior,
outsideIds = virtualButtonsOutside,
moreIds = virtualButtonsInMore,
onLayoutChange = { outside, more ->
virtualButtonsOutside = outside
virtualButtonsInMore = more
},
) )
} },
} snackbarHost = { SnackbarHost(snackHostState) },
) { pagePadding ->
entry<RootScreen.Fullscreen> { screen -> AdvancedConfigPage(
FullscreenControlPage( contentPadding = pagePadding,
launch = screen.launch, scrollBehavior = advancedScrollBehavior,
nativeCore = nativeCore, sessionStarted = sessionStarted,
virtualButtonsOutside = virtualButtonsOutside, snackbarHostState = snackHostState,
virtualButtonsInMore = virtualButtonsInMore, audioEnabled = audioEnabled,
showDebugInfo = fullscreenDebugInfoEnabled, noControl = noControl,
showVirtualButtons = showFullscreenVirtualButtons, onNoControlChange = {
onDismiss = { popRoot() }, noControl = it
if (it) {
turnScreenOff = false
}
},
audioDup = audioDup,
onAudioDupChange = { audioDup = it },
audioSourcePreset = audioSourcePreset,
onAudioSourcePresetChange = { audioSourcePreset = it },
audioSourceCustom = audioSourceCustom,
onAudioSourceCustomChange = { audioSourceCustom = it },
videoSourcePreset = videoSourcePreset,
onVideoSourcePresetChange = { videoSourcePreset = it },
cameraIdInput = cameraIdInput,
onCameraIdInputChange = { cameraIdInput = it },
cameraFacingPreset = cameraFacingPreset,
onCameraFacingPresetChange = { cameraFacingPreset = it },
cameraSizePreset = cameraSizePreset,
onCameraSizePresetChange = { cameraSizePreset = it },
cameraSizeCustom = cameraSizeCustom,
onCameraSizeCustomChange = { cameraSizeCustom = it },
cameraSizeDropdownItems = listOf("默认") + cameraSizeOptions + listOf("自定义"),
cameraSizeIndex = when (cameraSizePreset) {
"custom" -> cameraSizeOptions.size + 1
in cameraSizeOptions -> cameraSizeOptions.indexOf(cameraSizePreset) + 1
else -> 0
},
cameraArInput = cameraArInput,
onCameraArInputChange = { cameraArInput = it },
cameraFpsInput = cameraFpsInput,
onCameraFpsInputChange = { cameraFpsInput = it },
cameraHighSpeed = cameraHighSpeed,
onCameraHighSpeedChange = { cameraHighSpeed = it },
noAudioPlayback = noAudioPlayback,
onNoAudioPlaybackChange = { noAudioPlayback = it },
noVideo = noVideo,
onNoVideoChange = { noVideo = it },
requireAudio = requireAudio,
onRequireAudioChange = { requireAudio = it },
turnScreenOff = turnScreenOff,
onTurnScreenOffChange = { turnScreenOff = it },
maxSizeInput = maxSizeInput,
onMaxSizeInputChange = { maxSizeInput = it },
maxFpsInput = maxFpsInput,
onMaxFpsInputChange = { maxFpsInput = it },
videoEncoderDropdownItems = videoEncoderDropdownItems,
videoEncoderTypeMap = videoEncoderTypeMap,
videoEncoderIndex = videoEncoderIndex,
onVideoEncoderChange = { videoEncoder = it },
videoCodecOptions = videoCodecOptions,
onVideoCodecOptionsChange = { videoCodecOptions = it },
audioEncoderDropdownItems = audioEncoderDropdownItems,
audioEncoderTypeMap = audioEncoderTypeMap,
audioEncoderIndex = audioEncoderIndex,
onAudioEncoderChange = { audioEncoder = it },
audioCodecOptions = audioCodecOptions,
onAudioCodecOptionsChange = { audioCodecOptions = it },
onRefreshEncoders = { refreshEncodersAction?.invoke() },
onRefreshCameraSizes = { refreshCameraSizesAction?.invoke() },
newDisplayWidth = newDisplayWidth,
onNewDisplayWidthChange = { newDisplayWidth = it },
newDisplayHeight = newDisplayHeight,
onNewDisplayHeightChange = { newDisplayHeight = it },
newDisplayDpi = newDisplayDpi,
onNewDisplayDpiChange = { newDisplayDpi = it },
displayIdInput = displayIdInput,
onDisplayIdInputChange = { displayIdInput = it },
cropWidth = cropWidth,
onCropWidthChange = { cropWidth = it },
cropHeight = cropHeight,
onCropHeightChange = { cropHeight = it },
cropX = cropX,
onCropXChange = { cropX = it },
cropY = cropY,
onCropYChange = { cropY = it },
) )
} }
}
entry(RootScreen.VirtualButtonOrder) {
Scaffold(
modifier = Modifier.nestedScroll(advancedScrollBehavior.nestedScrollConnection),
topBar = {
TopAppBar(
title = "虚拟按钮排序",
navigationIcon = {
IconButton(onClick = { popRoot() }) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "返回"
)
}
},
scrollBehavior = advancedScrollBehavior,
)
},
) { pagePadding ->
VirtualButtonOrderPage(
contentPadding = pagePadding,
scrollBehavior = advancedScrollBehavior,
outsideIds = virtualButtonsOutside,
moreIds = virtualButtonsInMore,
onLayoutChange = { outside, more ->
virtualButtonsOutside = outside
virtualButtonsInMore = more
},
)
}
}
entry<RootScreen.Fullscreen> { screen ->
FullscreenControlPage(
launch = screen.launch,
nativeCore = nativeCore,
virtualButtonsOutside = virtualButtonsOutside,
virtualButtonsInMore = virtualButtonsInMore,
showDebugInfo = fullscreenDebugInfoEnabled,
showVirtualButtons = showFullscreenVirtualButtons,
onDismiss = { popRoot() },
)
}
} }
val rootEntries = rememberDecoratedNavEntries( val rootEntries = rememberDecoratedNavEntries(
@@ -724,7 +745,8 @@ private fun DeviceMenuPopupItem(
} }
val additionalTopPadding = if (index == 0) UiSpacing.PopupHorizontal else UiSpacing.PageItem val additionalTopPadding = if (index == 0) UiSpacing.PopupHorizontal else UiSpacing.PageItem
val additionalBottomPadding = if (index == optionSize - 1) UiSpacing.PopupHorizontal else UiSpacing.PageItem val additionalBottomPadding =
if (index == optionSize - 1) UiSpacing.PopupHorizontal else UiSpacing.PageItem
Text( Text(
text = text, text = text,
fontSize = MiuixTheme.textStyles.body1.fontSize, fontSize = MiuixTheme.textStyles.body1.fontSize,

View File

@@ -22,7 +22,6 @@ import top.yukonga.miuix.kmp.basic.IconButton
import top.yukonga.miuix.kmp.basic.ScrollBehavior import top.yukonga.miuix.kmp.basic.ScrollBehavior
import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextField import top.yukonga.miuix.kmp.basic.TextField
import top.yukonga.miuix.kmp.extra.SuperArrow
import top.yukonga.miuix.kmp.extra.SuperDropdown import top.yukonga.miuix.kmp.extra.SuperDropdown
import top.yukonga.miuix.kmp.extra.SuperSwitch import top.yukonga.miuix.kmp.extra.SuperSwitch
import top.yukonga.miuix.kmp.theme.ColorSchemeMode import top.yukonga.miuix.kmp.theme.ColorSchemeMode
@@ -61,12 +60,8 @@ fun SettingsScreen(
onMonetEnabledChange: (Boolean) -> Unit, onMonetEnabledChange: (Boolean) -> Unit,
fullscreenDebugInfoEnabled: Boolean, fullscreenDebugInfoEnabled: Boolean,
onFullscreenDebugInfoEnabledChange: (Boolean) -> Unit, onFullscreenDebugInfoEnabledChange: (Boolean) -> Unit,
showFullscreenVirtualButtons: Boolean,
onShowFullscreenVirtualButtonsChange: (Boolean) -> Unit,
keepScreenOnWhenStreamingEnabled: Boolean, keepScreenOnWhenStreamingEnabled: Boolean,
onKeepScreenOnWhenStreamingEnabledChange: (Boolean) -> Unit, onKeepScreenOnWhenStreamingEnabledChange: (Boolean) -> Unit,
onOpenReorderDevices: () -> Unit,
onOpenVirtualButtonOrder: () -> Unit,
devicePreviewCardHeightDp: Int, devicePreviewCardHeightDp: Int,
onDevicePreviewCardHeightDpChange: (Int) -> Unit, onDevicePreviewCardHeightDpChange: (Int) -> Unit,
customServerUri: String?, customServerUri: String?,
@@ -111,12 +106,6 @@ fun SettingsScreen(
checked = fullscreenDebugInfoEnabled, checked = fullscreenDebugInfoEnabled,
onCheckedChange = onFullscreenDebugInfoEnabledChange, onCheckedChange = onFullscreenDebugInfoEnabledChange,
) )
SuperSwitch(
title = "全屏显示虚拟按钮",
summary = "在全屏模式底部显示返回/主页等虚拟按钮",
checked = showFullscreenVirtualButtons,
onCheckedChange = onShowFullscreenVirtualButtonsChange,
)
SuperSwitch( SuperSwitch(
title = "投屏时不允许息屏", title = "投屏时不允许息屏",
summary = "Scrcpy 启动后保持本机常亮,避免锁屏导致 ADB 断开", summary = "Scrcpy 启动后保持本机常亮,避免锁屏导致 ADB 断开",
@@ -127,7 +116,11 @@ fun SettingsScreen(
title = "预览卡高度", title = "预览卡高度",
summary = "设备页预览卡高度", summary = "设备页预览卡高度",
value = devicePreviewCardHeightDp.toFloat(), value = devicePreviewCardHeightDp.toFloat(),
onValueChange = { onDevicePreviewCardHeightDpChange(it.roundToInt().coerceAtLeast(120)) }, onValueChange = {
onDevicePreviewCardHeightDpChange(
it.roundToInt().coerceAtLeast(120)
)
},
valueRange = 160f..600f, valueRange = 160f..600f,
steps = 439, steps = 439,
unit = "dp", unit = "dp",
@@ -136,19 +129,10 @@ fun SettingsScreen(
inputFilter = { it.filter(Char::isDigit) }, inputFilter = { it.filter(Char::isDigit) },
inputValueRange = 120f..Float.MAX_VALUE, inputValueRange = 120f..Float.MAX_VALUE,
onInputConfirm = { raw -> onInputConfirm = { raw ->
raw.toIntOrNull()?.let { onDevicePreviewCardHeightDpChange(it.coerceAtLeast(120)) } raw.toIntOrNull()
?.let { onDevicePreviewCardHeightDpChange(it.coerceAtLeast(120)) }
}, },
) )
SuperArrow(
title = "调整设备排序",
summary = "调整设备页快捷设备列表顺序",
onClick = onOpenReorderDevices,
)
SuperArrow(
title = "虚拟按钮排序",
summary = "调整底部虚拟按钮与更多菜单顺序",
onClick = onOpenVirtualButtonOrder,
)
} }
SectionSmallTitle(text = "scrcpy-server") SectionSmallTitle(text = "scrcpy-server")
@@ -192,7 +176,7 @@ fun SettingsScreen(
TextField( TextField(
value = serverRemotePath, value = serverRemotePath,
onValueChange = onServerRemotePathChange, onValueChange = onServerRemotePathChange,
label = AppDefaults.DefaultServerRemotePath, label = AppDefaults.SERVER_REMOTE_PATH,
useLabelAsPlaceholder = true, useLabelAsPlaceholder = true,
singleLine = true, singleLine = true,
modifier = Modifier modifier = Modifier
@@ -214,7 +198,7 @@ fun SettingsScreen(
TextField( TextField(
value = adbKeyName, value = adbKeyName,
onValueChange = onAdbKeyNameChange, onValueChange = onAdbKeyNameChange,
label = AppDefaults.DefaultAdbKeyName, label = AppDefaults.ADB_KEY_NAME,
useLabelAsPlaceholder = true, useLabelAsPlaceholder = true,
singleLine = true, singleLine = true,
modifier = Modifier modifier = Modifier

View File

@@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -54,7 +53,11 @@ internal fun VirtualButtonOrderPage(
onLayoutChange(outsideState.toList(), moreState.toList()) onLayoutChange(outsideState.toList(), moreState.toList())
} }
fun reorderInside(list: androidx.compose.runtime.snapshots.SnapshotStateList<String>, itemId: String, deltaY: Float) { fun reorderInside(
list: androidx.compose.runtime.snapshots.SnapshotStateList<String>,
itemId: String,
deltaY: Float
) {
val fromIndex = list.indexOf(itemId) val fromIndex = list.indexOf(itemId)
if (fromIndex < 0) return if (fromIndex < 0) return
@@ -88,7 +91,8 @@ internal fun VirtualButtonOrderPage(
} }
fun handleOutsideDrop(payload: SortDropPayload) { fun handleOutsideDrop(payload: SortDropPayload) {
val transfer = payload.transferDirection == SortTransferDirection.TO_RIGHT && payload.deltaX >= LIST_TRANSFER_STEP_PX val transfer =
payload.transferDirection == SortTransferDirection.TO_RIGHT && payload.deltaX >= LIST_TRANSFER_STEP_PX
if (transfer) { if (transfer) {
if (payload.itemId == VirtualButtonAction.MORE.id) return if (payload.itemId == VirtualButtonAction.MORE.id) return
transferToOther(outsideState, moreState, payload.itemId, payload.deltaY) transferToOther(outsideState, moreState, payload.itemId, payload.deltaY)
@@ -98,7 +102,8 @@ internal fun VirtualButtonOrderPage(
} }
fun handleMoreDrop(payload: SortDropPayload) { fun handleMoreDrop(payload: SortDropPayload) {
val transfer = payload.transferDirection == SortTransferDirection.TO_LEFT && payload.deltaX <= -LIST_TRANSFER_STEP_PX val transfer =
payload.transferDirection == SortTransferDirection.TO_LEFT && payload.deltaX <= -LIST_TRANSFER_STEP_PX
if (transfer) { if (transfer) {
transferToOther(moreState, outsideState, payload.itemId, payload.deltaY) transferToOther(moreState, outsideState, payload.itemId, payload.deltaY)
} else { } else {

View File

@@ -58,7 +58,8 @@ fun SuperSlide(
holdDownState = holdArrow, holdDownState = holdArrow,
endActions = { endActions = {
val isZeroState = value == 0f && zeroStateText != null val isZeroState = value == 0f && zeroStateText != null
val valueText = if (isZeroState) zeroStateText else (displayText ?: displayFormatter(value)) val valueText =
if (isZeroState) zeroStateText else (displayText ?: displayFormatter(value))
val shouldShowUnit = unit.isNotBlank() && (!isZeroState || showUnitWhenZeroState) val shouldShowUnit = unit.isNotBlank() && (!isZeroState || showUnitWhenZeroState)
val text = if (shouldShowUnit) "$valueText $unit" else valueText val text = if (shouldShowUnit) "$valueText $unit" else valueText
Text( Text(

View File

@@ -6,215 +6,495 @@ import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults
import io.github.miuzarte.scrcpyforandroid.constants.AppPreferenceKeys import io.github.miuzarte.scrcpyforandroid.constants.AppPreferenceKeys
internal data class MainSettings( internal data class MainSettings(
val themeBaseIndex: Int = AppDefaults.DefaultThemeBaseIndex, val audioEnabled: Boolean = AppDefaults.AUDIO_ENABLED,
val monetEnabled: Boolean = AppDefaults.DefaultMonetEnabled, val audioCodec: String = AppDefaults.AUDIO_CODEC,
val fullscreenDebugInfoEnabled: Boolean = AppDefaults.DefaultFullscreenDebugInfoEnabled, val videoCodec: String = AppDefaults.VIDEO_CODEC,
val showFullscreenVirtualButtons: Boolean = AppDefaults.DefaultShowFullscreenVirtualButtons, val themeBaseIndex: Int = AppDefaults.THEME_BASE_INDEX,
val devicePreviewCardHeightDp: Int = AppDefaults.DefaultDevicePreviewCardHeightDp, val monetEnabled: Boolean = AppDefaults.MONET,
val keepScreenOnWhenStreamingEnabled: Boolean = AppDefaults.DefaultKeepScreenOnWhenStreamingEnabled, val fullscreenDebugInfoEnabled: Boolean = AppDefaults.FULLSCREEN_DEBUG_INFO,
val virtualButtonsOutside: String = AppDefaults.DefaultVirtualButtonsOutside, val showFullscreenVirtualButtons: Boolean = AppDefaults.SHOW_FULLSCREEN_VIRTUAL_BUTTONS,
val virtualButtonsInMore: String = AppDefaults.DefaultVirtualButtonsInMore, val keepScreenOnWhenStreamingEnabled: Boolean = AppDefaults.KEEP_SCREEN_ON_WHEN_STREAMING,
val customServerUri: String? = null, val devicePreviewCardHeightDp: Int = AppDefaults.DEVICE_PREVIEW_CARD_HEIGHT_DP,
val serverRemotePath: String = AppDefaults.DefaultServerRemotePathInput, val virtualButtonsOutside: String = AppDefaults.VIRTUAL_BUTTONS_OUTSIDE,
val videoCodec: String = AppDefaults.DefaultVideoCodec, val virtualButtonsInMore: String = AppDefaults.VIRTUAL_BUTTONS_IN_MORE,
val audioEnabled: Boolean = AppDefaults.DefaultAudioEnabled, val customServerUri: String? = AppDefaults.CUSTOM_SERVER_URI,
val audioCodec: String = AppDefaults.DefaultAudioCodec, val serverRemotePath: String = AppDefaults.SERVER_REMOTE_PATH_INPUT,
val adbKeyName: String = AppDefaults.DefaultAdbKeyNameInput, val adbKeyName: String = AppDefaults.ADB_KEY_NAME_INPUT,
) )
internal data class DevicePageSettings( internal data class DevicePageSettings(
val pairHost: String = AppDefaults.DefaultPairHost, val quickConnectInput: String = AppDefaults.QUICK_CONNECT_INPUT,
val pairPort: String = AppDefaults.DefaultPairPort, val pairHost: String = AppDefaults.PAIR_HOST,
val pairCode: String = AppDefaults.DefaultPairCode, val pairPort: String = AppDefaults.PAIR_PORT,
val quickConnectInput: String = AppDefaults.DefaultQuickConnectInput, val pairCode: String = AppDefaults.PAIR_CODE,
val bitRateMbps: Float = AppDefaults.DefaultBitRateMbps, val audioBitRateKbps: Int = AppDefaults.AUDIO_BIT_RATE_KBPS,
val bitRateInput: String = AppDefaults.DefaultBitRateInput, val audioBitRateInput: String = AppDefaults.AUDIO_BIT_RATE_INPUT,
val audioBitRateKbps: Int = AppDefaults.DefaultAudioBitRateKbps, val videoBitRateMbps: Float = AppDefaults.VIDEO_BIT_RATE_MBPS,
val maxSizeInput: String = AppDefaults.DefaultMaxSizeInput, val videoBitRateInput: String = AppDefaults.VIDEO_BIT_RATE_INPUT,
val maxFpsInput: String = AppDefaults.DefaultMaxFpsInput, val turnScreenOff: Boolean = AppDefaults.TURN_SCREEN_OFF,
val noControl: Boolean = AppDefaults.DefaultNoControl, val noControl: Boolean = AppDefaults.NO_CONTROL,
val videoEncoder: String = AppDefaults.DefaultVideoEncoder, val noVideo: Boolean = AppDefaults.NO_VIDEO,
val videoCodecOptions: String = AppDefaults.DefaultVideoCodecOptions, val videoSourcePreset: String = AppDefaults.VIDEO_SOURCE_PRESET,
val audioEncoder: String = AppDefaults.DefaultAudioEncoder, val displayIdInput: String = AppDefaults.DISPLAY_ID,
val audioCodecOptions: String = AppDefaults.DefaultAudioCodecOptions, val cameraIdInput: String = AppDefaults.CAMERA_ID,
val audioDup: Boolean = AppDefaults.DefaultAudioDup, val cameraFacingPreset: String = AppDefaults.CAMERA_FACING_PRESET,
val audioSourcePreset: String = AppDefaults.DefaultAudioSourcePreset, val cameraSizePreset: String = AppDefaults.CAMERA_SIZE_PRESET,
val audioSourceCustom: String = AppDefaults.DefaultAudioSourceCustom, val cameraSizeCustom: String = AppDefaults.CAMERA_SIZE_CUSTOM,
val videoSourcePreset: String = AppDefaults.DefaultVideoSourcePreset, val cameraAr: String = AppDefaults.CAMERA_AR,
val cameraIdInput: String = AppDefaults.DefaultCameraIdInput, val cameraFps: String = AppDefaults.CAMERA_FPS,
val cameraFacingPreset: String = AppDefaults.DefaultCameraFacingPreset, val cameraHighSpeed: Boolean = AppDefaults.CAMERA_HIGH_SPEED,
val cameraSizePreset: String = AppDefaults.DefaultCameraSizePreset, val audioSourcePreset: String = AppDefaults.AUDIO_SOURCE_PRESET,
val cameraSizeCustom: String = AppDefaults.DefaultCameraSizeCustom, val audioSourceCustom: String = AppDefaults.AUDIO_SOURCE_CUSTOM,
val cameraArInput: String = AppDefaults.DefaultCameraArInput, val audioDup: Boolean = AppDefaults.AUDIO_DUP,
val cameraFpsInput: String = AppDefaults.DefaultCameraFpsInput, val noAudioPlayback: Boolean = AppDefaults.NO_AUDIO_PLAYBACK,
val cameraHighSpeed: Boolean = AppDefaults.DefaultCameraHighSpeed, val requireAudio: Boolean = AppDefaults.REQUIRE_AUDIO,
val noAudioPlayback: Boolean = AppDefaults.DefaultNoAudioPlayback, val maxSizeInput: String = AppDefaults.MAX_SIZE_INPUT,
val noVideo: Boolean = AppDefaults.DefaultNoVideo, val maxFpsInput: String = AppDefaults.MAX_FPS_INPUT,
val requireAudio: Boolean = AppDefaults.DefaultRequireAudio, val videoEncoder: String = AppDefaults.VIDEO_ENCODER,
val turnScreenOff: Boolean = AppDefaults.DefaultTurnScreenOff, val videoCodecOptions: String = AppDefaults.VIDEO_CODEC_OPTION,
val newDisplayWidth: String = AppDefaults.DefaultNewDisplayWidth, val audioEncoder: String = AppDefaults.AUDIO_ENCODER,
val newDisplayHeight: String = AppDefaults.DefaultNewDisplayHeight, val audioCodecOptions: String = AppDefaults.AUDIO_CODEC_OPTION,
val newDisplayDpi: String = AppDefaults.DefaultNewDisplayDpi, val newDisplayWidth: String = AppDefaults.NEW_DISPLAY_WIDTH,
val displayIdInput: String = AppDefaults.DefaultDisplayIdInput, val newDisplayHeight: String = AppDefaults.NEW_DISPLAY_HEIGHT,
val cropWidth: String = AppDefaults.DefaultCropWidth, val newDisplayDpi: String = AppDefaults.NEW_DISPLAY_DPI,
val cropHeight: String = AppDefaults.DefaultCropHeight, val cropWidth: String = AppDefaults.CROP_WIDTH,
val cropX: String = AppDefaults.DefaultCropX, val cropHeight: String = AppDefaults.CROP_HEIGHT,
val cropY: String = AppDefaults.DefaultCropY, val cropX: String = AppDefaults.CROP_X,
val cropY: String = AppDefaults.CROP_Y,
) )
internal fun loadMainSettings(context: Context): MainSettings { internal fun loadMainSettings(context: Context): MainSettings {
val prefs = context.getSharedPreferences(AppPreferenceKeys.PrefsName, Context.MODE_PRIVATE) val prefs = context.getSharedPreferences(
AppPreferenceKeys.PREFS_NAME,
Context.MODE_PRIVATE,
)
return MainSettings( return MainSettings(
themeBaseIndex = prefs.getInt(AppPreferenceKeys.ThemeBaseIndex, AppDefaults.DefaultThemeBaseIndex), audioEnabled = prefs.getBoolean(
monetEnabled = prefs.getBoolean(AppPreferenceKeys.MonetEnabled, AppDefaults.DefaultMonetEnabled), AppPreferenceKeys.AUDIO_ENABLED,
AppDefaults.AUDIO_ENABLED,
),
audioCodec = prefs.getString(
AppPreferenceKeys.AUDIO_CODEC,
AppDefaults.AUDIO_CODEC,
).orEmpty().ifBlank { AppDefaults.AUDIO_CODEC },
videoCodec = prefs.getString(
AppPreferenceKeys.VIDEO_CODEC,
AppDefaults.VIDEO_CODEC,
).orEmpty().ifBlank { AppDefaults.VIDEO_CODEC },
themeBaseIndex = prefs.getInt(
AppPreferenceKeys.THEME_BASE_INDEX,
AppDefaults.THEME_BASE_INDEX,
),
monetEnabled = prefs.getBoolean(
AppPreferenceKeys.MONET,
AppDefaults.MONET,
),
fullscreenDebugInfoEnabled = prefs.getBoolean( fullscreenDebugInfoEnabled = prefs.getBoolean(
AppPreferenceKeys.FullscreenDebugInfoEnabled, AppPreferenceKeys.FULLSCREEN_DEBUG_INFO,
AppDefaults.DefaultFullscreenDebugInfoEnabled, AppDefaults.FULLSCREEN_DEBUG_INFO,
), ),
showFullscreenVirtualButtons = prefs.getBoolean( showFullscreenVirtualButtons = prefs.getBoolean(
AppPreferenceKeys.ShowFullscreenVirtualButtons, AppPreferenceKeys.SHOW_FULLSCREEN_VIRTUAL_BUTTONS,
AppDefaults.DefaultShowFullscreenVirtualButtons, AppDefaults.SHOW_FULLSCREEN_VIRTUAL_BUTTONS,
),
keepScreenOnWhenStreamingEnabled = prefs.getBoolean(
AppPreferenceKeys.KEEP_SCREEN_ON_WHEN_STREAMING,
AppDefaults.KEEP_SCREEN_ON_WHEN_STREAMING,
), ),
devicePreviewCardHeightDp = prefs.getInt( devicePreviewCardHeightDp = prefs.getInt(
AppPreferenceKeys.DevicePreviewCardHeightDp, AppPreferenceKeys.DEVICE_PREVIEW_CARD_HEIGHT_DP,
AppDefaults.DefaultDevicePreviewCardHeightDp, AppDefaults.DEVICE_PREVIEW_CARD_HEIGHT_DP,
).coerceAtLeast(120), ).coerceAtLeast(120),
keepScreenOnWhenStreamingEnabled = prefs.getBoolean(
AppPreferenceKeys.KeepScreenOnWhenStreamingEnabled,
AppDefaults.DefaultKeepScreenOnWhenStreamingEnabled,
),
virtualButtonsOutside = prefs.getString( virtualButtonsOutside = prefs.getString(
AppPreferenceKeys.VirtualButtonsOutside, AppPreferenceKeys.VIRTUAL_BUTTONS_OUTSIDE,
AppDefaults.DefaultVirtualButtonsOutside, AppDefaults.VIRTUAL_BUTTONS_OUTSIDE,
).orEmpty().ifBlank { AppDefaults.DefaultVirtualButtonsOutside }, ).orEmpty().ifBlank { AppDefaults.VIRTUAL_BUTTONS_OUTSIDE },
virtualButtonsInMore = prefs.getString( virtualButtonsInMore = prefs.getString(
AppPreferenceKeys.VirtualButtonsInMore, AppPreferenceKeys.VIRTUAL_BUTTONS_IN_MORE,
AppDefaults.DefaultVirtualButtonsInMore, AppDefaults.VIRTUAL_BUTTONS_IN_MORE,
).orEmpty().ifBlank { AppDefaults.DefaultVirtualButtonsInMore }, ).orEmpty().ifBlank { AppDefaults.VIRTUAL_BUTTONS_IN_MORE },
customServerUri = prefs.getString(AppPreferenceKeys.CustomServerUri, null), customServerUri = prefs.getString(
AppPreferenceKeys.CUSTOM_SERVER_URI,
AppDefaults.CUSTOM_SERVER_URI
).orEmpty().ifBlank { null },
serverRemotePath = prefs.getString( serverRemotePath = prefs.getString(
AppPreferenceKeys.ServerRemotePath, AppPreferenceKeys.SERVER_REMOTE_PATH,
AppDefaults.DefaultServerRemotePathInput, AppDefaults.SERVER_REMOTE_PATH_INPUT,
).orEmpty(),
adbKeyName = prefs.getString(
AppPreferenceKeys.ADB_KEY_NAME,
AppDefaults.ADB_KEY_NAME_INPUT,
).orEmpty(), ).orEmpty(),
videoCodec = prefs.getString(AppPreferenceKeys.VideoCodec, AppDefaults.DefaultVideoCodec)
.orEmpty()
.ifBlank { AppDefaults.DefaultVideoCodec },
audioEnabled = prefs.getBoolean(AppPreferenceKeys.AudioEnabled, AppDefaults.DefaultAudioEnabled),
audioCodec = prefs.getString(AppPreferenceKeys.AudioCodec, AppDefaults.DefaultAudioCodec)
.orEmpty()
.ifBlank { AppDefaults.DefaultAudioCodec },
adbKeyName = prefs.getString(AppPreferenceKeys.AdbKeyName, AppDefaults.DefaultAdbKeyNameInput).orEmpty(),
) )
} }
internal fun saveMainSettings(context: Context, settings: MainSettings) { internal fun saveMainSettings(context: Context, settings: MainSettings) {
context.getSharedPreferences(AppPreferenceKeys.PrefsName, Context.MODE_PRIVATE) context.getSharedPreferences(
.edit { AppPreferenceKeys.PREFS_NAME,
putInt(AppPreferenceKeys.ThemeBaseIndex, settings.themeBaseIndex) Context.MODE_PRIVATE,
.putBoolean(AppPreferenceKeys.MonetEnabled, settings.monetEnabled) ).edit {
.putBoolean(AppPreferenceKeys.FullscreenDebugInfoEnabled, settings.fullscreenDebugInfoEnabled) putBoolean(
.putBoolean(AppPreferenceKeys.ShowFullscreenVirtualButtons, settings.showFullscreenVirtualButtons) AppPreferenceKeys.AUDIO_ENABLED,
.putInt(AppPreferenceKeys.DevicePreviewCardHeightDp, settings.devicePreviewCardHeightDp.coerceAtLeast(120)) settings.audioEnabled,
.putBoolean(AppPreferenceKeys.KeepScreenOnWhenStreamingEnabled, settings.keepScreenOnWhenStreamingEnabled) )
.putString(AppPreferenceKeys.VirtualButtonsOutside, settings.virtualButtonsOutside) .putString(
.putString(AppPreferenceKeys.VirtualButtonsInMore, settings.virtualButtonsInMore) AppPreferenceKeys.AUDIO_CODEC,
.putString(AppPreferenceKeys.CustomServerUri, settings.customServerUri) settings.audioCodec,
.putString(AppPreferenceKeys.ServerRemotePath, settings.serverRemotePath) )
.putString(AppPreferenceKeys.VideoCodec, settings.videoCodec) .putString(
.putBoolean(AppPreferenceKeys.AudioEnabled, settings.audioEnabled) AppPreferenceKeys.VIDEO_CODEC,
.putString(AppPreferenceKeys.AudioCodec, settings.audioCodec) settings.videoCodec,
.putString(AppPreferenceKeys.AdbKeyName, settings.adbKeyName) )
} .putInt(
AppPreferenceKeys.THEME_BASE_INDEX,
settings.themeBaseIndex,
)
.putBoolean(
AppPreferenceKeys.MONET,
settings.monetEnabled,
)
.putBoolean(
AppPreferenceKeys.FULLSCREEN_DEBUG_INFO,
settings.fullscreenDebugInfoEnabled,
)
.putBoolean(
AppPreferenceKeys.SHOW_FULLSCREEN_VIRTUAL_BUTTONS,
settings.showFullscreenVirtualButtons,
)
.putBoolean(
AppPreferenceKeys.KEEP_SCREEN_ON_WHEN_STREAMING,
settings.keepScreenOnWhenStreamingEnabled,
)
.putInt(
AppPreferenceKeys.DEVICE_PREVIEW_CARD_HEIGHT_DP,
settings.devicePreviewCardHeightDp.coerceAtLeast(120),
)
.putString(
AppPreferenceKeys.VIRTUAL_BUTTONS_OUTSIDE,
settings.virtualButtonsOutside,
)
.putString(
AppPreferenceKeys.VIRTUAL_BUTTONS_IN_MORE,
settings.virtualButtonsInMore,
)
.putString(
AppPreferenceKeys.CUSTOM_SERVER_URI,
settings.customServerUri,
)
.putString(
AppPreferenceKeys.SERVER_REMOTE_PATH,
settings.serverRemotePath,
)
.putString(
AppPreferenceKeys.ADB_KEY_NAME,
settings.adbKeyName,
)
}
} }
internal fun loadDevicePageSettings(context: Context): DevicePageSettings { internal fun loadDevicePageSettings(context: Context): DevicePageSettings {
val prefs = context.getSharedPreferences(AppPreferenceKeys.PrefsName, Context.MODE_PRIVATE) val prefs = context.getSharedPreferences(
AppPreferenceKeys.PREFS_NAME,
Context.MODE_PRIVATE,
)
val audioBitRateKbps = prefs.getInt(
AppPreferenceKeys.AUDIO_BIT_RATE_KBPS,
AppDefaults.AUDIO_BIT_RATE_KBPS,
)
return DevicePageSettings( return DevicePageSettings(
pairHost = AppDefaults.DefaultPairHost, quickConnectInput = prefs.getString(
pairPort = AppDefaults.DefaultPairPort, AppPreferenceKeys.QUICK_CONNECT_INPUT,
pairCode = AppDefaults.DefaultPairCode, AppDefaults.QUICK_CONNECT_INPUT,
quickConnectInput = prefs.getString(AppPreferenceKeys.QuickConnectInput, AppDefaults.DefaultQuickConnectInput).orEmpty(), ).orEmpty(),
bitRateMbps = prefs.getFloat(AppPreferenceKeys.BitRateMbps, AppDefaults.DefaultBitRateMbps), pairHost = AppDefaults.PAIR_HOST,
bitRateInput = prefs.getString(AppPreferenceKeys.BitRateInput, AppDefaults.DefaultBitRateInput) pairPort = AppDefaults.PAIR_PORT,
.orEmpty() pairCode = AppDefaults.PAIR_CODE,
.ifBlank { AppDefaults.DefaultBitRateInput }, audioBitRateKbps = audioBitRateKbps,
maxSizeInput = prefs.getString(AppPreferenceKeys.MaxSizeInput, AppDefaults.DefaultMaxSizeInput).orEmpty(), audioBitRateInput = prefs.getString(
audioBitRateKbps = prefs.getInt(AppPreferenceKeys.AudioBitRateKbps, AppDefaults.DefaultAudioBitRateKbps), AppPreferenceKeys.AUDIO_BIT_RATE_INPUT,
maxFpsInput = prefs.getString(AppPreferenceKeys.MaxFpsInput, AppDefaults.DefaultMaxFpsInput).orEmpty(), AppDefaults.AUDIO_BIT_RATE_INPUT,
noControl = prefs.getBoolean(AppPreferenceKeys.NoControl, AppDefaults.DefaultNoControl), ).orEmpty().ifBlank { audioBitRateKbps.toString() },
videoEncoder = prefs.getString(AppPreferenceKeys.VideoEncoder, AppDefaults.DefaultVideoEncoder).orEmpty(), videoBitRateMbps = prefs.getFloat(
videoCodecOptions = prefs.getString(AppPreferenceKeys.VideoCodecOptions, AppDefaults.DefaultVideoCodecOptions).orEmpty(), AppPreferenceKeys.VIDEO_BIT_RATE_MBPS,
audioEncoder = prefs.getString(AppPreferenceKeys.AudioEncoder, AppDefaults.DefaultAudioEncoder).orEmpty(), AppDefaults.VIDEO_BIT_RATE_MBPS,
audioCodecOptions = prefs.getString(AppPreferenceKeys.AudioCodecOptions, AppDefaults.DefaultAudioCodecOptions).orEmpty(), ),
audioDup = prefs.getBoolean(AppPreferenceKeys.AudioDup, AppDefaults.DefaultAudioDup), videoBitRateInput = prefs.getString(
audioSourcePreset = prefs.getString(AppPreferenceKeys.AudioSourcePreset, AppDefaults.DefaultAudioSourcePreset) AppPreferenceKeys.VIDEO_BIT_RATE_INPUT,
.orEmpty() AppDefaults.VIDEO_BIT_RATE_INPUT
.ifBlank { AppDefaults.DefaultAudioSourcePreset }, ).orEmpty().ifBlank { AppDefaults.VIDEO_BIT_RATE_INPUT },
audioSourceCustom = prefs.getString(AppPreferenceKeys.AudioSourceCustom, AppDefaults.DefaultAudioSourceCustom).orEmpty(), turnScreenOff = prefs.getBoolean(
videoSourcePreset = prefs.getString(AppPreferenceKeys.VideoSourcePreset, AppDefaults.DefaultVideoSourcePreset) AppPreferenceKeys.TURN_SCREEN_OFF,
.orEmpty() AppDefaults.TURN_SCREEN_OFF,
.ifBlank { AppDefaults.DefaultVideoSourcePreset }, ),
cameraIdInput = prefs.getString(AppPreferenceKeys.CameraIdInput, AppDefaults.DefaultCameraIdInput).orEmpty(), noControl = prefs.getBoolean(
cameraFacingPreset = prefs.getString(AppPreferenceKeys.CameraFacingPreset, AppDefaults.DefaultCameraFacingPreset).orEmpty(), AppPreferenceKeys.NO_CONTROL,
cameraSizePreset = prefs.getString(AppPreferenceKeys.CameraSizePreset, AppDefaults.DefaultCameraSizePreset).orEmpty(), AppDefaults.NO_CONTROL,
cameraSizeCustom = prefs.getString(AppPreferenceKeys.CameraSizeCustom, AppDefaults.DefaultCameraSizeCustom).orEmpty(), ),
cameraArInput = prefs.getString(AppPreferenceKeys.CameraArInput, AppDefaults.DefaultCameraArInput).orEmpty(), noVideo = prefs.getBoolean(
cameraFpsInput = prefs.getString(AppPreferenceKeys.CameraFpsInput, AppDefaults.DefaultCameraFpsInput).orEmpty(), AppPreferenceKeys.NO_VIDEO,
cameraHighSpeed = prefs.getBoolean(AppPreferenceKeys.CameraHighSpeed, AppDefaults.DefaultCameraHighSpeed), AppDefaults.NO_VIDEO,
noAudioPlayback = prefs.getBoolean(AppPreferenceKeys.NoAudioPlayback, AppDefaults.DefaultNoAudioPlayback), ),
noVideo = prefs.getBoolean(AppPreferenceKeys.NoVideo, AppDefaults.DefaultNoVideo), videoSourcePreset = prefs.getString(
requireAudio = prefs.getBoolean(AppPreferenceKeys.RequireAudio, AppDefaults.DefaultRequireAudio), AppPreferenceKeys.VIDEO_SOURCE_PRESET,
turnScreenOff = prefs.getBoolean(AppPreferenceKeys.TurnScreenOff, AppDefaults.DefaultTurnScreenOff), AppDefaults.VIDEO_SOURCE_PRESET,
newDisplayWidth = prefs.getString(AppPreferenceKeys.NewDisplayWidth, AppDefaults.DefaultNewDisplayWidth).orEmpty(), ).orEmpty().ifBlank { AppDefaults.VIDEO_SOURCE_PRESET },
newDisplayHeight = prefs.getString(AppPreferenceKeys.NewDisplayHeight, AppDefaults.DefaultNewDisplayHeight).orEmpty(), displayIdInput = prefs.getString(
newDisplayDpi = prefs.getString(AppPreferenceKeys.NewDisplayDpi, AppDefaults.DefaultNewDisplayDpi).orEmpty(), AppPreferenceKeys.DISPLAY_ID,
displayIdInput = prefs.getString(AppPreferenceKeys.DisplayIdInput, AppDefaults.DefaultDisplayIdInput).orEmpty(), AppDefaults.DISPLAY_ID,
cropWidth = prefs.getString(AppPreferenceKeys.CropWidth, AppDefaults.DefaultCropWidth).orEmpty(), )
cropHeight = prefs.getString(AppPreferenceKeys.CropHeight, AppDefaults.DefaultCropHeight).orEmpty(), .orEmpty(),
cropX = prefs.getString(AppPreferenceKeys.CropX, AppDefaults.DefaultCropX).orEmpty(), cameraIdInput = prefs.getString(
cropY = prefs.getString(AppPreferenceKeys.CropY, AppDefaults.DefaultCropY).orEmpty(), AppPreferenceKeys.CAMERA_ID,
AppDefaults.CAMERA_ID,
)
.orEmpty(),
cameraFacingPreset = prefs.getString(
AppPreferenceKeys.CAMERA_FACING_PRESET,
AppDefaults.CAMERA_FACING_PRESET,
).orEmpty(),
cameraSizePreset = prefs.getString(
AppPreferenceKeys.CAMERA_SIZE_PRESET,
AppDefaults.CAMERA_SIZE_PRESET,
).orEmpty(),
cameraSizeCustom = prefs.getString(
AppPreferenceKeys.CAMERA_SIZE_CUSTOM,
AppDefaults.CAMERA_SIZE_CUSTOM,
).orEmpty(),
cameraAr = prefs.getString(
AppPreferenceKeys.CAMERA_AR,
AppDefaults.CAMERA_AR,
).orEmpty(),
cameraFps = prefs.getString(
AppPreferenceKeys.CAMERA_FPS,
AppDefaults.CAMERA_FPS,
).orEmpty(),
cameraHighSpeed = prefs.getBoolean(
AppPreferenceKeys.CAMERA_HIGH_SPEED,
AppDefaults.CAMERA_HIGH_SPEED,
),
audioSourcePreset = prefs.getString(
AppPreferenceKeys.AUDIO_SOURCE_PRESET,
AppDefaults.AUDIO_SOURCE_PRESET,
).orEmpty().ifBlank { AppDefaults.AUDIO_SOURCE_PRESET },
audioSourceCustom = prefs.getString(
AppPreferenceKeys.AUDIO_SOURCE_CUSTOM,
AppDefaults.AUDIO_SOURCE_CUSTOM,
).orEmpty(),
audioDup = prefs.getBoolean(
AppPreferenceKeys.AUDIO_DUP,
AppDefaults.AUDIO_DUP,
),
noAudioPlayback = prefs.getBoolean(
AppPreferenceKeys.NO_AUDIO_PLAYBACK,
AppDefaults.NO_AUDIO_PLAYBACK,
),
requireAudio = prefs.getBoolean(
AppPreferenceKeys.REQUIRE_AUDIO,
AppDefaults.REQUIRE_AUDIO,
),
maxSizeInput = prefs.getString(
AppPreferenceKeys.MAX_SIZE_INPUT,
AppDefaults.MAX_SIZE_INPUT,
)
.orEmpty(),
maxFpsInput = prefs.getString(
AppPreferenceKeys.MAX_FPS_INPUT,
AppDefaults.MAX_FPS_INPUT,
)
.orEmpty(),
videoEncoder = prefs.getString(
AppPreferenceKeys.VIDEO_ENCODER,
AppDefaults.VIDEO_ENCODER,
)
.orEmpty(),
videoCodecOptions = prefs.getString(
AppPreferenceKeys.VIDEO_CODEC_OPTION,
AppDefaults.VIDEO_CODEC_OPTION,
).orEmpty(),
audioEncoder = prefs.getString(
AppPreferenceKeys.AUDIO_ENCODER,
AppDefaults.AUDIO_ENCODER,
).orEmpty(),
audioCodecOptions = prefs.getString(
AppPreferenceKeys.AUDIO_CODEC_OPTION,
AppDefaults.AUDIO_CODEC_OPTION,
).orEmpty(),
newDisplayWidth = prefs.getString(
AppPreferenceKeys.NEW_DISPLAY_WIDTH,
AppDefaults.NEW_DISPLAY_WIDTH,
).orEmpty(),
newDisplayHeight = prefs.getString(
AppPreferenceKeys.NEW_DISPLAY_HEIGHT,
AppDefaults.NEW_DISPLAY_HEIGHT,
).orEmpty(),
newDisplayDpi = prefs.getString(
AppPreferenceKeys.NEW_DISPLAY_DPI,
AppDefaults.NEW_DISPLAY_DPI,
).orEmpty(),
cropWidth = prefs.getString(
AppPreferenceKeys.CROP_WIDTH,
AppDefaults.CROP_WIDTH,
).orEmpty(),
cropHeight = prefs.getString(
AppPreferenceKeys.CROP_HEIGHT,
AppDefaults.CROP_HEIGHT,
).orEmpty(),
cropX = prefs.getString(
AppPreferenceKeys.CROP_X,
AppDefaults.CROP_X,
).orEmpty(),
cropY = prefs.getString(
AppPreferenceKeys.CROP_Y,
AppDefaults.CROP_Y,
).orEmpty(),
) )
} }
internal fun saveDevicePageSettings(context: Context, settings: DevicePageSettings) { internal fun saveDevicePageSettings(context: Context, settings: DevicePageSettings) {
context.getSharedPreferences(AppPreferenceKeys.PrefsName, Context.MODE_PRIVATE) context.getSharedPreferences(AppPreferenceKeys.PREFS_NAME, Context.MODE_PRIVATE)
.edit { .edit {
remove(AppPreferenceKeys.PairHost) remove(AppPreferenceKeys.PAIR_HOST)
.remove(AppPreferenceKeys.PairPort) .remove(AppPreferenceKeys.PAIR_PORT)
.remove(AppPreferenceKeys.PairCode) .remove(AppPreferenceKeys.PAIR_CODE)
.putString(AppPreferenceKeys.QuickConnectInput, settings.quickConnectInput) .putString(
.putFloat(AppPreferenceKeys.BitRateMbps, settings.bitRateMbps) AppPreferenceKeys.QUICK_CONNECT_INPUT,
.putString(AppPreferenceKeys.BitRateInput, settings.bitRateInput) settings.quickConnectInput,
.putInt(AppPreferenceKeys.AudioBitRateKbps, settings.audioBitRateKbps) )
.putString(AppPreferenceKeys.MaxSizeInput, settings.maxSizeInput) .putInt(
.putString(AppPreferenceKeys.MaxFpsInput, settings.maxFpsInput) AppPreferenceKeys.AUDIO_BIT_RATE_KBPS,
.putBoolean(AppPreferenceKeys.NoControl, settings.noControl) settings.audioBitRateKbps,
.putString(AppPreferenceKeys.VideoEncoder, settings.videoEncoder) )
.putString(AppPreferenceKeys.VideoCodecOptions, settings.videoCodecOptions) .putString(
.putString(AppPreferenceKeys.AudioEncoder, settings.audioEncoder) AppPreferenceKeys.AUDIO_BIT_RATE_INPUT,
.putString(AppPreferenceKeys.AudioCodecOptions, settings.audioCodecOptions) settings.audioBitRateInput,
.putBoolean(AppPreferenceKeys.AudioDup, settings.audioDup) )
.putString(AppPreferenceKeys.AudioSourcePreset, settings.audioSourcePreset) .putFloat(
.putString(AppPreferenceKeys.AudioSourceCustom, settings.audioSourceCustom) AppPreferenceKeys.VIDEO_BIT_RATE_MBPS,
.putString(AppPreferenceKeys.VideoSourcePreset, settings.videoSourcePreset) settings.videoBitRateMbps,
.putString(AppPreferenceKeys.CameraIdInput, settings.cameraIdInput) )
.putString(AppPreferenceKeys.CameraFacingPreset, settings.cameraFacingPreset) .putString(
.putString(AppPreferenceKeys.CameraSizePreset, settings.cameraSizePreset) AppPreferenceKeys.VIDEO_BIT_RATE_INPUT,
.putString(AppPreferenceKeys.CameraSizeCustom, settings.cameraSizeCustom) settings.videoBitRateInput,
.putString(AppPreferenceKeys.CameraArInput, settings.cameraArInput) )
.putString(AppPreferenceKeys.CameraFpsInput, settings.cameraFpsInput) .putBoolean(
.putBoolean(AppPreferenceKeys.CameraHighSpeed, settings.cameraHighSpeed) AppPreferenceKeys.TURN_SCREEN_OFF,
.putBoolean(AppPreferenceKeys.NoAudioPlayback, settings.noAudioPlayback) settings.turnScreenOff,
.putBoolean(AppPreferenceKeys.NoVideo, settings.noVideo) )
.putBoolean(AppPreferenceKeys.RequireAudio, settings.requireAudio) .putBoolean(
.putBoolean(AppPreferenceKeys.TurnScreenOff, settings.turnScreenOff) AppPreferenceKeys.NO_CONTROL,
.putString(AppPreferenceKeys.NewDisplayWidth, settings.newDisplayWidth) settings.noControl,
.putString(AppPreferenceKeys.NewDisplayHeight, settings.newDisplayHeight) )
.putString(AppPreferenceKeys.NewDisplayDpi, settings.newDisplayDpi) .putBoolean(
.putString(AppPreferenceKeys.DisplayIdInput, settings.displayIdInput) AppPreferenceKeys.NO_VIDEO,
.putString(AppPreferenceKeys.CropWidth, settings.cropWidth) settings.noVideo,
.putString(AppPreferenceKeys.CropHeight, settings.cropHeight) )
.putString(AppPreferenceKeys.CropX, settings.cropX) .putString(
.putString(AppPreferenceKeys.CropY, settings.cropY) AppPreferenceKeys.VIDEO_SOURCE_PRESET,
settings.videoSourcePreset,
)
.putString(
AppPreferenceKeys.DISPLAY_ID,
settings.displayIdInput,
)
.putString(
AppPreferenceKeys.CAMERA_ID,
settings.cameraIdInput,
)
.putString(
AppPreferenceKeys.CAMERA_FACING_PRESET,
settings.cameraFacingPreset,
)
.putString(
AppPreferenceKeys.CAMERA_SIZE_PRESET,
settings.cameraSizePreset,
)
.putString(
AppPreferenceKeys.CAMERA_SIZE_CUSTOM,
settings.cameraSizeCustom,
)
.putString(
AppPreferenceKeys.CAMERA_AR,
settings.cameraAr,
)
.putString(
AppPreferenceKeys.CAMERA_FPS,
settings.cameraFps,
)
.putBoolean(
AppPreferenceKeys.CAMERA_HIGH_SPEED,
settings.cameraHighSpeed,
)
.putString(
AppPreferenceKeys.AUDIO_SOURCE_PRESET,
settings.audioSourcePreset,
)
.putString(
AppPreferenceKeys.AUDIO_SOURCE_CUSTOM,
settings.audioSourceCustom,
)
.putBoolean(
AppPreferenceKeys.AUDIO_DUP,
settings.audioDup,
)
.putBoolean(
AppPreferenceKeys.NO_AUDIO_PLAYBACK,
settings.noAudioPlayback,
)
.putBoolean(
AppPreferenceKeys.REQUIRE_AUDIO,
settings.requireAudio,
)
.putString(
AppPreferenceKeys.MAX_SIZE_INPUT,
settings.maxSizeInput,
)
.putString(
AppPreferenceKeys.MAX_FPS_INPUT,
settings.maxFpsInput,
)
.putString(
AppPreferenceKeys.VIDEO_ENCODER,
settings.videoEncoder,
)
.putString(
AppPreferenceKeys.VIDEO_CODEC_OPTION,
settings.videoCodecOptions,
)
.putString(
AppPreferenceKeys.AUDIO_ENCODER,
settings.audioEncoder,
)
.putString(
AppPreferenceKeys.AUDIO_CODEC_OPTION,
settings.audioCodecOptions,
)
.putString(
AppPreferenceKeys.NEW_DISPLAY_WIDTH,
settings.newDisplayWidth,
)
.putString(
AppPreferenceKeys.NEW_DISPLAY_HEIGHT,
settings.newDisplayHeight,
)
.putString(
AppPreferenceKeys.NEW_DISPLAY_DPI,
settings.newDisplayDpi,
)
.putString(
AppPreferenceKeys.CROP_WIDTH,
settings.cropWidth,
)
.putString(
AppPreferenceKeys.CROP_HEIGHT,
settings.cropHeight,
)
.putString(
AppPreferenceKeys.CROP_X,
settings.cropX,
)
.putString(
AppPreferenceKeys.CROP_Y,
settings.cropY,
)
} }
} }

View File

@@ -12,8 +12,13 @@ internal data class ConnectedDeviceInfo(
val sdkInt: Int, val sdkInt: Int,
) )
internal fun fetchConnectedDeviceInfo(nativeCore: NativeCoreFacade, host: String, port: Int): ConnectedDeviceInfo { internal fun fetchConnectedDeviceInfo(
fun prop(name: String): String = runCatching { nativeCore.adbShell("getprop $name").trim() }.getOrDefault("") nativeCore: NativeCoreFacade,
host: String,
port: Int
): ConnectedDeviceInfo {
fun prop(name: String): String =
runCatching { nativeCore.adbShell("getprop $name").trim() }.getOrDefault("")
val model = prop("ro.product.model") val model = prop("ro.product.model")
val serial = prop("ro.serialno") val serial = prop("ro.serialno")

View File

@@ -8,8 +8,8 @@ import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
internal fun loadQuickDevices(context: Context): List<DeviceShortcut> { internal fun loadQuickDevices(context: Context): List<DeviceShortcut> {
val raw = context.getSharedPreferences(AppPreferenceKeys.PrefsName, Context.MODE_PRIVATE) val raw = context.getSharedPreferences(AppPreferenceKeys.PREFS_NAME, Context.MODE_PRIVATE)
.getString(AppPreferenceKeys.QuickDevices, "") .getString(AppPreferenceKeys.QUICK_DEVICES, "")
.orEmpty() .orEmpty()
if (raw.isBlank()) return emptyList() if (raw.isBlank()) return emptyList()
@@ -21,7 +21,7 @@ internal fun loadQuickDevices(context: Context): List<DeviceShortcut> {
3 -> { 3 -> {
val name = parts[0].trim() val name = parts[0].trim()
val host = parts[1].trim() val host = parts[1].trim()
val port = parts[2].trim().toIntOrNull() ?: AppDefaults.DefaultAdbPort val port = parts[2].trim().toIntOrNull() ?: AppDefaults.ADB_PORT
if (host.isNotBlank()) { if (host.isNotBlank()) {
result.add( result.add(
DeviceShortcut( DeviceShortcut(
@@ -39,7 +39,8 @@ internal fun loadQuickDevices(context: Context): List<DeviceShortcut> {
// Backward compatibility with old format: name|host:port // Backward compatibility with old format: name|host:port
val name = parts[0].trim() val name = parts[0].trim()
val host = parts[1].substringBefore(":").trim() val host = parts[1].substringBefore(":").trim()
val port = parts[1].substringAfter(":", AppDefaults.DefaultAdbPort.toString()).trim().toIntOrNull() ?: AppDefaults.DefaultAdbPort val port = parts[1].substringAfter(":", AppDefaults.ADB_PORT.toString()).trim()
.toIntOrNull() ?: AppDefaults.ADB_PORT
if (host.isNotBlank()) { if (host.isNotBlank()) {
result.add( result.add(
DeviceShortcut( DeviceShortcut(
@@ -59,9 +60,9 @@ internal fun loadQuickDevices(context: Context): List<DeviceShortcut> {
internal fun saveQuickDevices(context: Context, quickDevices: List<DeviceShortcut>) { internal fun saveQuickDevices(context: Context, quickDevices: List<DeviceShortcut>) {
val raw = quickDevices.joinToString("\n") { "${it.name}|${it.host}|${it.port}" } val raw = quickDevices.joinToString("\n") { "${it.name}|${it.host}|${it.port}" }
context.getSharedPreferences(AppPreferenceKeys.PrefsName, Context.MODE_PRIVATE) context.getSharedPreferences(AppPreferenceKeys.PREFS_NAME, Context.MODE_PRIVATE)
.edit { .edit {
putString(AppPreferenceKeys.QuickDevices, raw) putString(AppPreferenceKeys.QUICK_DEVICES, raw)
} }
} }
@@ -70,7 +71,8 @@ internal fun parseQuickTarget(raw: String): ConnectionTarget? {
if (value.isEmpty()) return null if (value.isEmpty()) return null
val host = value.substringBefore(':').trim() val host = value.substringBefore(':').trim()
if (host.isEmpty()) return null if (host.isEmpty()) return null
val port = value.substringAfter(':', AppDefaults.DefaultAdbPort.toString()).trim().toIntOrNull() ?: AppDefaults.DefaultAdbPort val port = value.substringAfter(':', AppDefaults.ADB_PORT.toString()).trim().toIntOrNull()
?: AppDefaults.ADB_PORT
return ConnectionTarget(host, port) return ConnectionTarget(host, port)
} }

View File

@@ -13,7 +13,6 @@ import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -32,11 +31,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.AddLink import androidx.compose.material.icons.filled.AddLink
import androidx.compose.material.icons.filled.Fullscreen import androidx.compose.material.icons.filled.Fullscreen
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.rounded.CheckCircleOutline import androidx.compose.material.icons.rounded.CheckCircleOutline
import androidx.compose.material.icons.rounded.LinkOff import androidx.compose.material.icons.rounded.LinkOff
import androidx.compose.material.icons.rounded.Wifi import androidx.compose.material.icons.rounded.Wifi
@@ -63,7 +59,6 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
@@ -96,7 +91,7 @@ import kotlin.math.roundToInt
private val VIDEO_CODEC_OPTIONS = listOf( private val VIDEO_CODEC_OPTIONS = listOf(
"h264" to "H.264", "h264" to "H.264",
"h265" to "H.265", "h265" to "H.265",
"av1" to "AV1", "av1" to "AV1",
) )
private val AUDIO_CODEC_OPTIONS = listOf( private val AUDIO_CODEC_OPTIONS = listOf(
@@ -266,9 +261,13 @@ internal fun PreviewCard(
} }
val containerAspect = maxWidth.value / maxHeight.value val containerAspect = maxWidth.value / maxHeight.value
val fittedModifier = if (sessionAspect > containerAspect) { val fittedModifier = if (sessionAspect > containerAspect) {
Modifier.fillMaxWidth().aspectRatio(sessionAspect) Modifier
.fillMaxWidth()
.aspectRatio(sessionAspect)
} else { } else {
Modifier.fillMaxHeight().aspectRatio(sessionAspect) Modifier
.fillMaxHeight()
.aspectRatio(sessionAspect)
} }
Box( Box(
@@ -358,10 +357,13 @@ internal fun ConfigPanel(
sessionStarted: Boolean, sessionStarted: Boolean,
) { ) {
val videoCodecItems = remember { VIDEO_CODEC_OPTIONS.map { it.second } } val videoCodecItems = remember { VIDEO_CODEC_OPTIONS.map { it.second } }
val videoCodecIndex = VIDEO_CODEC_OPTIONS.indexOfFirst { it.first == videoCodec }.coerceAtLeast(0) val videoCodecIndex =
VIDEO_CODEC_OPTIONS.indexOfFirst { it.first == videoCodec }.coerceAtLeast(0)
val audioCodecItems = remember { AUDIO_CODEC_OPTIONS.map { it.second } } val audioCodecItems = remember { AUDIO_CODEC_OPTIONS.map { it.second } }
val audioCodecIndex = AUDIO_CODEC_OPTIONS.indexOfFirst { it.first == audioCodec }.coerceAtLeast(0) val audioCodecIndex =
val audioBitRatePresetIndex = presetIndexFromInput(audioBitRateKbps.toString(), ScrcpyPresets.AudioBitRate) AUDIO_CODEC_OPTIONS.indexOfFirst { it.first == audioCodec }.coerceAtLeast(0)
val audioBitRatePresetIndex =
presetIndexFromInput(audioBitRateKbps.toString(), ScrcpyPresets.AudioBitRate)
SectionSmallTitle(text = "Scrcpy") SectionSmallTitle(text = "Scrcpy")
Card { Card {
@@ -433,6 +435,7 @@ internal fun ConfigPanel(
dotUsed = true dotUsed = true
true true
} }
else -> false else -> false
} }
} }
@@ -506,7 +509,9 @@ private fun PairingDialog(
onValueChange = { host = it }, onValueChange = { host = it },
label = "IP 地址", label = "IP 地址",
singleLine = true, singleLine = true,
modifier = Modifier.fillMaxWidth().padding(bottom = UiSpacing.CardContent), modifier = Modifier
.fillMaxWidth()
.padding(bottom = UiSpacing.CardContent),
) )
TextField( TextField(
value = port, value = port,
@@ -514,14 +519,18 @@ private fun PairingDialog(
label = "端口", label = "端口",
singleLine = true, singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth().padding(bottom = UiSpacing.CardContent), modifier = Modifier
.fillMaxWidth()
.padding(bottom = UiSpacing.CardContent),
) )
TextField( TextField(
value = code, value = code,
onValueChange = { code = it }, onValueChange = { code = it },
label = "WLAN 配对码", label = "WLAN 配对码",
singleLine = true, singleLine = true,
modifier = Modifier.fillMaxWidth().padding(bottom = UiSpacing.Large), modifier = Modifier
.fillMaxWidth()
.padding(bottom = UiSpacing.Large),
) )
Row(horizontalArrangement = Arrangement.spacedBy(UiSpacing.PopupHorizontal)) { Row(horizontalArrangement = Arrangement.spacedBy(UiSpacing.PopupHorizontal)) {
TextButton( TextButton(
@@ -599,8 +608,10 @@ fun FullscreenControlScreen(
} }
fun mapToDevice(rawX: Float, rawY: Float): Pair<Int, Int> { fun mapToDevice(rawX: Float, rawY: Float): Pair<Int, Int> {
val x = ((rawX / touchAreaSize.width) * session.width).roundToInt().coerceIn(0, (session.width - 1).coerceAtLeast(0)) val x = ((rawX / touchAreaSize.width) * session.width).roundToInt()
val y = ((rawY / touchAreaSize.height) * session.height).roundToInt().coerceIn(0, (session.height - 1).coerceAtLeast(0)) .coerceIn(0, (session.width - 1).coerceAtLeast(0))
val y = ((rawY / touchAreaSize.height) * session.height).roundToInt()
.coerceIn(0, (session.height - 1).coerceAtLeast(0))
return x to y return x to y
} }
@@ -612,7 +623,14 @@ fun FullscreenControlScreen(
val py = event.getY(i) val py = event.getY(i)
activePointerPositions[pointerId] = Offset(px, py) activePointerPositions[pointerId] = Offset(px, py)
val (x, y) = mapToDevice(px, py) val (x, y) = mapToDevice(px, py)
onInjectTouch(UiMotionActions.MOVE, pointerId.toLong(), x, y, event.getPressure(i).coerceIn(0f, 1f), 1) onInjectTouch(
UiMotionActions.MOVE,
pointerId.toLong(),
x,
y,
event.getPressure(i).coerceIn(0f, 1f),
1
)
} }
} }
@@ -626,7 +644,14 @@ fun FullscreenControlScreen(
activePointerIds += pointerId activePointerIds += pointerId
activePointerPositions[pointerId] = Offset(event.x, event.y) activePointerPositions[pointerId] = Offset(event.x, event.y)
activeTouchCount = activePointerIds.size activeTouchCount = activePointerIds.size
onInjectTouch(UiMotionActions.DOWN, pointerId.toLong(), x, y, event.getPressure(0).coerceIn(0f, 1f), 1) onInjectTouch(
UiMotionActions.DOWN,
pointerId.toLong(),
x,
y,
event.getPressure(0).coerceIn(0f, 1f),
1
)
} }
MotionEvent.ACTION_POINTER_DOWN -> { MotionEvent.ACTION_POINTER_DOWN -> {
@@ -638,7 +663,14 @@ fun FullscreenControlScreen(
activePointerIds += pointerId activePointerIds += pointerId
activePointerPositions[pointerId] = Offset(px, py) activePointerPositions[pointerId] = Offset(px, py)
activeTouchCount = activePointerIds.size activeTouchCount = activePointerIds.size
onInjectTouch(UiMotionActions.POINTER_DOWN, pointerId.toLong(), x, y, event.getPressure(index).coerceIn(0f, 1f), 1) onInjectTouch(
UiMotionActions.POINTER_DOWN,
pointerId.toLong(),
x,
y,
event.getPressure(index).coerceIn(0f, 1f),
1
)
syncActivePointersFromEvent() syncActivePointersFromEvent()
} }
@@ -650,7 +682,14 @@ fun FullscreenControlScreen(
val py = event.getY(i) val py = event.getY(i)
activePointerPositions[pointerId] = Offset(px, py) activePointerPositions[pointerId] = Offset(px, py)
val (x, y) = mapToDevice(px, py) val (x, y) = mapToDevice(px, py)
onInjectTouch(UiMotionActions.MOVE, pointerId.toLong(), x, y, event.getPressure(i).coerceIn(0f, 1f), 1) onInjectTouch(
UiMotionActions.MOVE,
pointerId.toLong(),
x,
y,
event.getPressure(i).coerceIn(0f, 1f),
1
)
} }
} }
@@ -698,9 +737,13 @@ fun FullscreenControlScreen(
} }
val containerAspect = maxWidth.value / maxHeight.value val containerAspect = maxWidth.value / maxHeight.value
val fittedModifier = if (sessionAspect > containerAspect) { val fittedModifier = if (sessionAspect > containerAspect) {
Modifier.fillMaxWidth().aspectRatio(sessionAspect) Modifier
.fillMaxWidth()
.aspectRatio(sessionAspect)
} else { } else {
Modifier.fillMaxHeight().aspectRatio(sessionAspect) Modifier
.fillMaxHeight()
.aspectRatio(sessionAspect)
} }
Box( Box(
@@ -780,13 +823,21 @@ private fun ScrcpyVideoSurface(
factory = { context -> factory = { context ->
TextureView(context).apply { TextureView(context).apply {
surfaceTextureListener = object : TextureView.SurfaceTextureListener { surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(surfaceTexture: SurfaceTexture, width: Int, height: Int) { override fun onSurfaceTextureAvailable(
surfaceTexture: SurfaceTexture,
width: Int,
height: Int
) {
currentSurface?.release() // Release stale surface if any currentSurface?.release() // Release stale surface if any
@SuppressLint("Recycle") @SuppressLint("Recycle")
currentSurface = Surface(surfaceTexture) currentSurface = Surface(surfaceTexture)
} }
override fun onSurfaceTextureSizeChanged(surfaceTexture: SurfaceTexture, width: Int, height: Int) = Unit override fun onSurfaceTextureSizeChanged(
surfaceTexture: SurfaceTexture,
width: Int,
height: Int
) = Unit
override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean { override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean {
val released = currentSurface val released = currentSurface
@@ -820,7 +871,7 @@ internal fun DeviceTile(
Card( Card(
colors = CardDefaults.defaultColors( colors = CardDefaults.defaultColors(
color = if (device.online) MiuixTheme.colorScheme.surfaceContainer color = if (device.online) MiuixTheme.colorScheme.surfaceContainer
else MiuixTheme.colorScheme.surfaceContainer.copy(alpha = 0.6f), else MiuixTheme.colorScheme.surfaceContainer.copy(alpha = 0.6f),
), ),
pressFeedbackType = PressFeedbackType.Sink, pressFeedbackType = PressFeedbackType.Sink,
onClick = haptics.press, onClick = haptics.press,
@@ -1016,7 +1067,9 @@ internal fun DeviceEditorScreen(
.padding(top = UiSpacing.CardContent), .padding(top = UiSpacing.CardContent),
) )
Row( Row(
modifier = Modifier.fillMaxWidth().padding(UiSpacing.CardContent), modifier = Modifier
.fillMaxWidth()
.padding(UiSpacing.CardContent),
horizontalArrangement = Arrangement.spacedBy(UiSpacing.Medium), horizontalArrangement = Arrangement.spacedBy(UiSpacing.Medium),
) { ) {
TextButton( TextButton(
@@ -1032,7 +1085,7 @@ internal fun DeviceEditorScreen(
TextButton( TextButton(
text = "保存", text = "保存",
onClick = { onClick = {
val p = port.toIntOrNull() ?: AppDefaults.DefaultAdbPort val p = port.toIntOrNull() ?: AppDefaults.ADB_PORT
val h = host.trim() val h = host.trim()
if (h.isNotBlank()) { if (h.isNotBlank()) {
onSave( onSave(

View File

@@ -110,7 +110,10 @@ fun SortableCardList(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = UiSpacing.CardContent, vertical = UiSpacing.FieldLabelBottom), .padding(
horizontal = UiSpacing.CardContent,
vertical = UiSpacing.FieldLabelBottom
),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {

View File

@@ -69,12 +69,16 @@ internal fun StatusCardLayout(
) { ) {
val haptics = rememberAppHaptics() val haptics = rememberAppHaptics()
Row( Row(
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min), modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min),
horizontalArrangement = Arrangement.spacedBy(UiSpacing.PageItem), horizontalArrangement = Arrangement.spacedBy(UiSpacing.PageItem),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Card( Card(
modifier = Modifier.weight(1f).fillMaxHeight(), modifier = Modifier
.weight(1f)
.fillMaxHeight(),
colors = defaultColors(color = spec.big.containerColor), colors = defaultColors(color = spec.big.containerColor),
pressFeedbackType = PressFeedbackType.Tilt, pressFeedbackType = PressFeedbackType.Tilt,
onClick = haptics.press, onClick = haptics.press,
@@ -125,15 +129,21 @@ internal fun StatusCardLayout(
} }
} }
Column(modifier = Modifier.weight(1f).fillMaxHeight()) { Column(modifier = Modifier
.weight(1f)
.fillMaxHeight()) {
StatusMetricCard( StatusMetricCard(
spec = spec.firstSmall, spec = spec.firstSmall,
modifier = Modifier.fillMaxWidth().weight(1f), modifier = Modifier
.fillMaxWidth()
.weight(1f),
) )
Spacer(Modifier.height(UiSpacing.PageItem)) Spacer(Modifier.height(UiSpacing.PageItem))
StatusMetricCard( StatusMetricCard(
spec = spec.secondSmall, spec = spec.secondSmall,
modifier = Modifier.fillMaxWidth().weight(1f), modifier = Modifier
.fillMaxWidth()
.weight(1f),
) )
} }
} }

View File

@@ -19,9 +19,6 @@ import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Notifications import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.PhotoCamera import androidx.compose.material.icons.filled.PhotoCamera
import androidx.compose.material.icons.filled.PowerSettingsNew import androidx.compose.material.icons.filled.PowerSettingsNew
import androidx.compose.material.icons.filled.VolumeDown
import androidx.compose.material.icons.filled.VolumeOff
import androidx.compose.material.icons.filled.VolumeUp
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -50,36 +47,76 @@ enum class VirtualButtonAction(
val icon: ImageVector, val icon: ImageVector,
val keycode: Int?, val keycode: Int?,
) { ) {
MORE("more", "更多", Icons.Default.MoreVert, null), MORE(
HOME("home", "主页", Icons.Default.Home, UiAndroidKeycodes.HOME), "more",
BACK("back", "返回", Icons.AutoMirrored.Filled.ArrowBack, UiAndroidKeycodes.BACK), "更多",
APP_SWITCH("app_switch", "多任务", Icons.Default.Apps, UiAndroidKeycodes.APP_SWITCH), Icons.Default.MoreVert,
MENU("menu", "菜单", Icons.Default.Menu, UiAndroidKeycodes.MENU), null
NOTIFICATION("notification", "通知栏", Icons.Default.Notifications, UiAndroidKeycodes.NOTIFICATION), ),
VOLUME_UP("volume_up", "音量+", Icons.AutoMirrored.Filled.VolumeUp, UiAndroidKeycodes.VOLUME_UP), HOME(
VOLUME_DOWN("volume_down", "音量-", Icons.AutoMirrored.Filled.VolumeDown, UiAndroidKeycodes.VOLUME_DOWN), "home",
VOLUME_MUTE("volume_mute", "静音", Icons.AutoMirrored.Filled.VolumeOff, UiAndroidKeycodes.VOLUME_MUTE), "主页",
POWER("power", "锁屏", Icons.Default.PowerSettingsNew, UiAndroidKeycodes.POWER), Icons.Default.Home,
SCREENSHOT("screenshot", "截图", Icons.Default.PhotoCamera, UiAndroidKeycodes.SYSRQ), UiAndroidKeycodes.HOME
),
BACK(
"back",
"返回",
Icons.AutoMirrored.Filled.ArrowBack,
UiAndroidKeycodes.BACK
),
APP_SWITCH(
"app_switch",
"多任务",
Icons.Default.Apps,
UiAndroidKeycodes.APP_SWITCH
),
MENU(
"menu",
"菜单",
Icons.Default.Menu,
UiAndroidKeycodes.MENU
),
NOTIFICATION(
"notification",
"通知栏",
Icons.Default.Notifications,
UiAndroidKeycodes.NOTIFICATION
),
VOLUME_UP(
"volume_up",
"音量+",
Icons.AutoMirrored.Filled.VolumeUp,
UiAndroidKeycodes.VOLUME_UP
),
VOLUME_DOWN(
"volume_down",
"音量-",
Icons.AutoMirrored.Filled.VolumeDown,
UiAndroidKeycodes.VOLUME_DOWN
),
VOLUME_MUTE(
"volume_mute",
"静音",
Icons.AutoMirrored.Filled.VolumeOff,
UiAndroidKeycodes.VOLUME_MUTE
),
POWER(
"power",
"锁屏",
Icons.Default.PowerSettingsNew,
UiAndroidKeycodes.POWER
),
SCREENSHOT(
"screenshot",
"截图",
Icons.Default.PhotoCamera,
UiAndroidKeycodes.SYSRQ
),
} }
object VirtualButtonActions { object VirtualButtonActions {
val all = VirtualButtonAction.entries val all = VirtualButtonAction.entries
val defaultOutsideIds = listOf(
VirtualButtonAction.MORE.id,
VirtualButtonAction.HOME.id,
VirtualButtonAction.BACK.id,
)
val defaultMoreIds = listOf(
VirtualButtonAction.APP_SWITCH.id,
VirtualButtonAction.MENU.id,
VirtualButtonAction.NOTIFICATION.id,
VirtualButtonAction.VOLUME_UP.id,
VirtualButtonAction.VOLUME_DOWN.id,
VirtualButtonAction.VOLUME_MUTE.id,
VirtualButtonAction.POWER.id,
VirtualButtonAction.SCREENSHOT.id,
)
private val byId = all.associateBy { it.id } private val byId = all.associateBy { it.id }