refactor: organize codes; add README.md
This commit is contained in:
45
README.md
Normal file
45
README.md
Normal 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)
|
||||
@@ -28,6 +28,7 @@ android {
|
||||
|
||||
ndk {
|
||||
abiFilters.clear()
|
||||
//noinspection ChromeOsAbiSupport
|
||||
abiFilters += configuredAbiList
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ import java.io.File
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.ArrayDeque
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@@ -34,7 +34,8 @@ class NativeCoreFacade(private val appContext: Context) {
|
||||
private val bootstrapLock = Any()
|
||||
private val bootstrapPackets = ArrayDeque<CachedPacket>()
|
||||
private var packetCount: Long = 0
|
||||
@Volatile private var audioPlayer: ScrcpyAudioPlayer? = null
|
||||
@Volatile
|
||||
private var audioPlayer: ScrcpyAudioPlayer? = null
|
||||
|
||||
@Volatile
|
||||
private var currentSessionInfo: ScrcpySessionInfo? = null
|
||||
@@ -72,7 +73,10 @@ class NativeCoreFacade(private val appContext: Context) {
|
||||
val currentId = surfaceIdentityMap[tag]
|
||||
val requestId = surface?.let { System.identityHashCode(it) }
|
||||
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
|
||||
}
|
||||
Log.i(TAG, "unregisterVideoSurface(): tag=$tag surfaceId=$requestId")
|
||||
@@ -178,7 +182,12 @@ class NativeCoreFacade(private val appContext: Context) {
|
||||
audioPlayer?.release()
|
||||
audioPlayer = null
|
||||
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)
|
||||
audioPlayer = player
|
||||
sessionManager.attachAudioConsumer { packet ->
|
||||
@@ -208,7 +217,11 @@ class NativeCoreFacade(private val appContext: Context) {
|
||||
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 {
|
||||
val serverJar = if (customServerUri.isNullOrBlank()) {
|
||||
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 {
|
||||
val serverJar = if (customServerUri.isNullOrBlank()) {
|
||||
extractAssetToCache(DEFAULT_SERVER_ASSET)
|
||||
@@ -307,7 +324,17 @@ class NativeCoreFacade(private val appContext: Context) {
|
||||
buttons: Int = 0,
|
||||
) {
|
||||
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()) {
|
||||
"h264" -> "video/avc"
|
||||
"h265" -> "video/hevc"
|
||||
"av1" -> "video/av01"
|
||||
"av1" -> "video/av01"
|
||||
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(
|
||||
width = session.width,
|
||||
height = session.height,
|
||||
@@ -489,7 +519,10 @@ class NativeCoreFacade(private val appContext: Context) {
|
||||
if (current == null || (current.width == width && current.height == height)) {
|
||||
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)
|
||||
mainHandler.post {
|
||||
videoSizeListeners.forEach { listener ->
|
||||
@@ -542,14 +575,22 @@ class NativeCoreFacade(private val appContext: Context) {
|
||||
cacheBootstrapPacket(packet)
|
||||
packetCount += 1
|
||||
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) ->
|
||||
if (!surfaceIdentityMap.containsKey(tag)) {
|
||||
return@forEach
|
||||
}
|
||||
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 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
|
||||
}
|
||||
|
||||
@@ -1,75 +1,79 @@
|
||||
package io.github.miuzarte.scrcpyforandroid.constants
|
||||
|
||||
object AppDefaults {
|
||||
const val MaxEventLogLines = 512
|
||||
const val DefaultAdbPort = 5555
|
||||
const val EVENT_LOG_LINES = 512
|
||||
const val ADB_PORT = 5555
|
||||
|
||||
// Devices
|
||||
const val DefaultQuickConnectInput = ""
|
||||
const val QUICK_CONNECT_INPUT = ""
|
||||
|
||||
const val DefaultPairHost = ""
|
||||
const val DefaultServerRemotePathInput = ""
|
||||
const val DefaultAdbKeyNameInput = ""
|
||||
const val DefaultPairPort = ""
|
||||
const val DefaultPairCode = ""
|
||||
const val PAIR_HOST = ""
|
||||
const val PAIR_PORT = ""
|
||||
const val PAIR_CODE = ""
|
||||
|
||||
const val DefaultAudioEnabled = true
|
||||
const val DefaultAudioCodec = "opus"
|
||||
const val DefaultAudioBitRateKbps = 128
|
||||
const val DefaultVideoCodec = "h264"
|
||||
const val DefaultBitRateMbps = 8f
|
||||
const val DefaultBitRateInput = "8.0"
|
||||
const val AUDIO_ENABLED = true
|
||||
const val AUDIO_CODEC = "opus"
|
||||
const val AUDIO_BIT_RATE_KBPS = 128
|
||||
const val AUDIO_BIT_RATE_INPUT = "128"
|
||||
const val VIDEO_CODEC = "h264"
|
||||
const val VIDEO_BIT_RATE_MBPS = 8f
|
||||
const val VIDEO_BIT_RATE_INPUT = "8.0"
|
||||
|
||||
const val DefaultTurnScreenOff = false
|
||||
const val DefaultNoControl = false
|
||||
const val DefaultNoVideo = false
|
||||
const val TURN_SCREEN_OFF = false
|
||||
const val NO_CONTROL = false
|
||||
const val NO_VIDEO = false
|
||||
|
||||
const val DefaultVideoSourcePreset = "display"
|
||||
const val DefaultDisplayIdInput = ""
|
||||
const val VIDEO_SOURCE_PRESET = "display"
|
||||
const val DISPLAY_ID = ""
|
||||
|
||||
const val DefaultCameraIdInput = ""
|
||||
const val DefaultCameraFacingPreset = ""
|
||||
const val DefaultCameraSizePreset = ""
|
||||
const val DefaultCameraSizeCustom = ""
|
||||
const val DefaultCameraArInput = ""
|
||||
const val DefaultCameraFpsInput = ""
|
||||
const val DefaultCameraHighSpeed = false
|
||||
const val CAMERA_ID = ""
|
||||
const val CAMERA_FACING_PRESET = ""
|
||||
const val CAMERA_SIZE_PRESET = ""
|
||||
const val CAMERA_SIZE_CUSTOM = ""
|
||||
const val CAMERA_AR = ""
|
||||
const val CAMERA_FPS = ""
|
||||
const val CAMERA_HIGH_SPEED = false
|
||||
|
||||
const val DefaultAudioSourcePreset = "output"
|
||||
const val DefaultAudioSourceCustom = ""
|
||||
const val DefaultAudioDup = false
|
||||
const val DefaultNoAudioPlayback = false
|
||||
const val DefaultRequireAudio = false
|
||||
const val AUDIO_SOURCE_PRESET = "output"
|
||||
const val AUDIO_SOURCE_CUSTOM = ""
|
||||
const val AUDIO_DUP = false
|
||||
const val NO_AUDIO_PLAYBACK = false
|
||||
const val REQUIRE_AUDIO = false
|
||||
|
||||
const val DefaultMaxSizeInput = ""
|
||||
const val DefaultMaxFpsInput = ""
|
||||
const val MAX_SIZE_INPUT = ""
|
||||
const val MAX_FPS_INPUT = ""
|
||||
|
||||
const val DefaultVideoEncoder = ""
|
||||
const val DefaultVideoCodecOptions = ""
|
||||
const val DefaultAudioEncoder = ""
|
||||
const val DefaultAudioCodecOptions = ""
|
||||
const val VIDEO_ENCODER = ""
|
||||
const val VIDEO_CODEC_OPTION = ""
|
||||
const val AUDIO_ENCODER = ""
|
||||
const val AUDIO_CODEC_OPTION = ""
|
||||
|
||||
const val DefaultNewDisplayWidth = ""
|
||||
const val DefaultNewDisplayHeight = ""
|
||||
const val DefaultNewDisplayDpi = ""
|
||||
const val NEW_DISPLAY_WIDTH = ""
|
||||
const val NEW_DISPLAY_HEIGHT = ""
|
||||
const val NEW_DISPLAY_DPI = ""
|
||||
|
||||
const val DefaultCropWidth = ""
|
||||
const val DefaultCropHeight = ""
|
||||
const val DefaultCropX = ""
|
||||
const val DefaultCropY = ""
|
||||
const val CROP_WIDTH = ""
|
||||
const val CROP_HEIGHT = ""
|
||||
const val CROP_X = ""
|
||||
const val CROP_Y = ""
|
||||
|
||||
// Settings
|
||||
const val DefaultThemeBaseIndex = 0
|
||||
const val DefaultMonetEnabled = false
|
||||
const val THEME_BASE_INDEX = 0
|
||||
const val MONET = false
|
||||
|
||||
const val DefaultFullscreenDebugInfoEnabled = false
|
||||
const val DefaultShowFullscreenVirtualButtons = true
|
||||
const val DefaultDevicePreviewCardHeightDp = 320
|
||||
const val DefaultKeepScreenOnWhenStreamingEnabled = false
|
||||
const val DefaultVirtualButtonsOutside = "more,home,back"
|
||||
const val DefaultVirtualButtonsInMore = "app_switch,menu,notification,volume_up,volume_down,volume_mute,power,screenshot"
|
||||
const val FULLSCREEN_DEBUG_INFO = false
|
||||
const val SHOW_FULLSCREEN_VIRTUAL_BUTTONS = true
|
||||
const val KEEP_SCREEN_ON_WHEN_STREAMING = false
|
||||
const val DEVICE_PREVIEW_CARD_HEIGHT_DP = 320
|
||||
const val VIRTUAL_BUTTONS_OUTSIDE = "more,home,back"
|
||||
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 = ""
|
||||
}
|
||||
|
||||
@@ -1,59 +1,78 @@
|
||||
package io.github.miuzarte.scrcpyforandroid.constants
|
||||
|
||||
object AppPreferenceKeys {
|
||||
const val PrefsName = "scrcpy_app_prefs"
|
||||
const val NativeAdbKeyPrefsName = "nativecore_adb_rsa"
|
||||
const val NativeAdbPrivateKey = "priv"
|
||||
const val ThemeBaseIndex = "theme_base_index"
|
||||
const val MonetEnabled = "monet_enabled"
|
||||
const val FullscreenDebugInfoEnabled = "fullscreen_debug_info_enabled"
|
||||
const val ShowFullscreenVirtualButtons = "show_fullscreen_virtual_buttons"
|
||||
const val DevicePreviewCardHeightDp = "device_preview_card_height_dp"
|
||||
const val KeepScreenOnWhenStreamingEnabled = "keep_screen_on_when_streaming_enabled"
|
||||
const val VirtualButtonsOutside = "virtual_buttons_outside"
|
||||
const val VirtualButtonsInMore = "virtual_buttons_in_more"
|
||||
const val CustomServerUri = "custom_server_uri"
|
||||
const val ServerRemotePath = "server_remote_path"
|
||||
const val VideoCodec = "video_codec"
|
||||
const val AudioEnabled = "audio_enabled"
|
||||
const val AudioCodec = "audio_codec"
|
||||
const val PairHost = "pair_host"
|
||||
const val PairPort = "pair_port"
|
||||
const val PairCode = "pair_code"
|
||||
const val QuickConnectInput = "quick_connect_input"
|
||||
const val BitRateMbps = "bit_rate_mbps"
|
||||
const val BitRateInput = "bit_rate_input"
|
||||
const val AudioBitRateKbps = "audio_bit_rate_kbps"
|
||||
const val MaxSizeInput = "max_size_input"
|
||||
const val MaxFpsInput = "max_fps_input"
|
||||
const val NoControl = "no_control"
|
||||
const val VideoEncoder = "video_encoder"
|
||||
const val VideoCodecOptions = "video_codec_options"
|
||||
const val AudioEncoder = "audio_encoder"
|
||||
const val AudioCodecOptions = "audio_codec_options"
|
||||
const val AudioDup = "audio_dup"
|
||||
const val AudioSourcePreset = "audio_source_preset"
|
||||
const val AudioSourceCustom = "audio_source_custom"
|
||||
const val VideoSourcePreset = "video_source_preset"
|
||||
const val CameraIdInput = "camera_id_input"
|
||||
const val CameraFacingPreset = "camera_facing_preset"
|
||||
const val CameraSizePreset = "camera_size_preset"
|
||||
const val CameraSizeCustom = "camera_size_custom"
|
||||
const val CameraArInput = "camera_ar_input"
|
||||
const val CameraFpsInput = "camera_fps_input"
|
||||
const val CameraHighSpeed = "camera_high_speed"
|
||||
const val NoAudioPlayback = "no_audio_playback"
|
||||
const val NoVideo = "no_video"
|
||||
const val RequireAudio = "require_audio"
|
||||
const val TurnScreenOff = "turn_screen_off"
|
||||
const val NewDisplayWidth = "new_display_width"
|
||||
const val NewDisplayHeight = "new_display_height"
|
||||
const val NewDisplayDpi = "new_display_dpi"
|
||||
const val DisplayIdInput = "display_id_input"
|
||||
const val CropWidth = "crop_width"
|
||||
const val CropHeight = "crop_height"
|
||||
const val CropX = "crop_x"
|
||||
const val CropY = "crop_y"
|
||||
const val AdbKeyName = "adb_key_name"
|
||||
const val QuickDevices = "quick_devices"
|
||||
const val PREFS_NAME = "scrcpy_app_prefs"
|
||||
const val NATIVE_ADB_KEY_PREFS_NAME = "nativecore_adb_rsa"
|
||||
const val NATIVE_ADB_PRIVATE_KEY = "priv"
|
||||
|
||||
// Devices
|
||||
const val QUICK_DEVICES = "quick_devices"
|
||||
const val QUICK_CONNECT_INPUT = "quick_connect_input"
|
||||
|
||||
const val PAIR_HOST = "pair_host"
|
||||
const val PAIR_PORT = "pair_port"
|
||||
const val PAIR_CODE = "pair_code"
|
||||
|
||||
const val AUDIO_ENABLED = "audio_enabled"
|
||||
const val AUDIO_CODEC = "audio_codec"
|
||||
const val AUDIO_BIT_RATE_INPUT = "audio_bit_rate_input"
|
||||
const val AUDIO_BIT_RATE_KBPS = "audio_bit_rate_kbps"
|
||||
const val VIDEO_CODEC = "video_codec"
|
||||
const val VIDEO_BIT_RATE_MBPS = "video_bit_rate_mbps"
|
||||
const val VIDEO_BIT_RATE_INPUT = "video_bit_rate_input"
|
||||
|
||||
const val TURN_SCREEN_OFF = "turn_screen_off"
|
||||
const val NO_CONTROL = "no_control"
|
||||
const val NO_VIDEO = "no_video"
|
||||
|
||||
const val VIDEO_SOURCE_PRESET = "video_source_preset"
|
||||
const val DISPLAY_ID = "display_id"
|
||||
|
||||
const val CAMERA_ID = "camera_id"
|
||||
const val CAMERA_FACING_PRESET = "camera_facing_preset"
|
||||
const val CAMERA_SIZE_PRESET = "camera_size_preset"
|
||||
const val CAMERA_SIZE_CUSTOM = "camera_size_custom"
|
||||
const val CAMERA_AR = "camera_ar"
|
||||
const val CAMERA_FPS = "camera_fps"
|
||||
const val CAMERA_HIGH_SPEED = "camera_high_speed"
|
||||
|
||||
const val AUDIO_SOURCE_PRESET = "audio_source_preset"
|
||||
const val AUDIO_SOURCE_CUSTOM = "audio_source_custom"
|
||||
const val AUDIO_DUP = "audio_dup"
|
||||
const val NO_AUDIO_PLAYBACK = "no_audio_playback"
|
||||
const val REQUIRE_AUDIO = "require_audio"
|
||||
|
||||
const val MAX_SIZE_INPUT = "max_size_input"
|
||||
const val MAX_FPS_INPUT = "max_fps_input"
|
||||
|
||||
const val VIDEO_ENCODER = "video_encoder"
|
||||
const val VIDEO_CODEC_OPTION = "video_codec_options"
|
||||
const val AUDIO_ENCODER = "audio_encoder"
|
||||
const val AUDIO_CODEC_OPTION = "audio_codec_options"
|
||||
|
||||
const val NEW_DISPLAY_WIDTH = "new_display_width"
|
||||
const val NEW_DISPLAY_HEIGHT = "new_display_height"
|
||||
const val NEW_DISPLAY_DPI = "new_display_dpi"
|
||||
|
||||
const val CROP_WIDTH = "crop_width"
|
||||
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"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.github.miuzarte.scrcpyforandroid.constants
|
||||
|
||||
object UiMotion {
|
||||
const val PageSwitchDampingRatio = 0.9f
|
||||
const val PageSwitchStiffness = 700f
|
||||
const val PAGE_SWITCH_DAMPING_RATIO = 0.9f
|
||||
const val PAGE_SWITCH_STIFFNESS = 700f
|
||||
}
|
||||
@@ -24,6 +24,7 @@ class AnnexBDecoder(
|
||||
private var outCount = 0L
|
||||
private var fpsWindowStartNs = System.nanoTime()
|
||||
private var fpsWindowFrameCount = 0
|
||||
|
||||
@Volatile
|
||||
private var released = false
|
||||
|
||||
@@ -65,16 +66,26 @@ class AnnexBDecoder(
|
||||
codec.queueInputBuffer(inputIndex, 0, data.size, ptsUs, flags)
|
||||
inCount += 1
|
||||
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 {
|
||||
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()
|
||||
}.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")
|
||||
}
|
||||
}
|
||||
|
||||
outIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
|
||||
notifyOutputSize(codec.outputFormat)
|
||||
continue
|
||||
}
|
||||
|
||||
else -> return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,23 +41,33 @@ internal class DirectAdbTransport(private val context: Context) {
|
||||
val privateKey: PrivateKey get() = keys.first
|
||||
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 {
|
||||
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()
|
||||
Log.i(TAG, "connect(): handshake success for $host:$port")
|
||||
return conn
|
||||
}
|
||||
|
||||
private fun loadOrCreate(): Pair<PrivateKey, ByteArray> {
|
||||
val prefs = context.getSharedPreferences(AppPreferenceKeys.NativeAdbKeyPrefsName, Context.MODE_PRIVATE)
|
||||
val privB64 = prefs.getString(AppPreferenceKeys.NativeAdbPrivateKey, null)
|
||||
val prefs = context.getSharedPreferences(
|
||||
AppPreferenceKeys.NATIVE_ADB_KEY_PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
val privB64 = prefs.getString(AppPreferenceKeys.NATIVE_ADB_PRIVATE_KEY, null)
|
||||
if (privB64 != null) {
|
||||
try {
|
||||
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)
|
||||
Log.i(TAG, "loadOrCreate(): loaded persisted RSA key pair, fp=${fingerprint(pub)}")
|
||||
return Pair(priv, pub)
|
||||
@@ -68,8 +78,16 @@ internal class DirectAdbTransport(private val context: Context) {
|
||||
val kpg = KeyPairGenerator.getInstance("RSA")
|
||||
kpg.initialize(2048)
|
||||
val kp = kpg.generateKeyPair()
|
||||
prefs.edit { putString(AppPreferenceKeys.NativeAdbPrivateKey, Base64.encodeToString(kp.private.encoded, Base64.NO_WRAP)) }
|
||||
Log.i(TAG, "loadOrCreate(): generated new RSA key pair, fp=${fingerprint(kp.public.encoded)}")
|
||||
prefs.edit {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -96,7 +114,7 @@ internal class DirectAdbConnection(
|
||||
val port: Int,
|
||||
private val privateKey: PrivateKey,
|
||||
private val publicKeyX509: ByteArray,
|
||||
private val keyName: String = AppDefaults.DefaultAdbKeyName,
|
||||
private val keyName: String = AppDefaults.ADB_KEY_NAME,
|
||||
) : AutoCloseable {
|
||||
|
||||
private val sha1DigestInfoPrefix = byteArrayOf(
|
||||
@@ -122,7 +140,9 @@ internal class DirectAdbConnection(
|
||||
private lateinit var rawOut: OutputStream
|
||||
private val nextLocalId = AtomicInteger(1)
|
||||
private val streams = ConcurrentHashMap<Int, AdbSocketStream>()
|
||||
@Volatile private var closed = false
|
||||
|
||||
@Volatile
|
||||
private var closed = false
|
||||
private var readerThread: Thread? = null
|
||||
|
||||
companion object {
|
||||
@@ -171,9 +191,17 @@ internal class DirectAdbConnection(
|
||||
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)}")
|
||||
}
|
||||
|
||||
@@ -262,6 +290,7 @@ internal class DirectAdbConnection(
|
||||
sendMsg(A_CLSE, 0, msg.arg0)
|
||||
}
|
||||
}
|
||||
|
||||
A_CLSE -> streams.remove(msg.arg1)?.forceClose()
|
||||
A_OPEN -> sendMsg(A_CLSE, 0, msg.arg0)
|
||||
}
|
||||
@@ -275,7 +304,12 @@ internal class DirectAdbConnection(
|
||||
}
|
||||
|
||||
@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 header = ByteBuffer.allocate(24).order(ByteOrder.LITTLE_ENDIAN)
|
||||
.putInt(command).putInt(arg0).putInt(arg1)
|
||||
@@ -296,7 +330,8 @@ internal class DirectAdbConnection(
|
||||
val dataLen = 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)
|
||||
}
|
||||
|
||||
@@ -391,12 +426,16 @@ internal class AdbSocketStream(
|
||||
private const val A_CLSE = 0x45534c43
|
||||
}
|
||||
|
||||
@Volatile var remoteId: Int = 0
|
||||
@Volatile var closed: Boolean = false
|
||||
@Volatile
|
||||
var remoteId: Int = 0
|
||||
|
||||
@Volatile
|
||||
var closed: Boolean = false
|
||||
|
||||
private val latch = CountDownLatch(1)
|
||||
private val latchOk = AtomicBoolean(false)
|
||||
private val queue = LinkedBlockingQueue<Any>()
|
||||
|
||||
private object EndOfStreamMarker
|
||||
|
||||
val inputStream: InputStream = InStream()
|
||||
@@ -476,6 +515,7 @@ internal class AdbSocketStream(
|
||||
if (len == 0) return
|
||||
sender(A_WRTE, localId, remoteId, b.copyOfRange(off, off + len))
|
||||
}
|
||||
|
||||
override fun flush() {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,18 @@ import java.nio.file.Path
|
||||
class NativeAdbService(appContext: Context) {
|
||||
|
||||
private val transport = DirectAdbTransport(appContext)
|
||||
@Volatile private var connection: DirectAdbConnection? = null
|
||||
@Volatile private var connectedHost: String? = null
|
||||
@Volatile private var connectedPort: Int? = null
|
||||
@Volatile
|
||||
private var connection: DirectAdbConnection? = null
|
||||
@Volatile
|
||||
private var connectedHost: String? = null
|
||||
@Volatile
|
||||
private var connectedPort: Int? = null
|
||||
|
||||
var keyName: String
|
||||
get() = transport.keyName
|
||||
set(value) { transport.keyName = value }
|
||||
set(value) {
|
||||
transport.keyName = value
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun pair(host: String, port: Int, pairingCode: String): Boolean {
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.media.AudioAttributes
|
||||
import android.media.AudioFormat
|
||||
import android.media.AudioManager
|
||||
import android.media.AudioTrack
|
||||
import android.os.Build
|
||||
import android.media.MediaCodec
|
||||
import android.media.MediaFormat
|
||||
import android.util.Log
|
||||
@@ -23,15 +22,22 @@ class ScrcpyAudioPlayer(private val codecId: Int) {
|
||||
private var audioTrack: AudioTrack? = null
|
||||
private val bufferInfo = MediaCodec.BufferInfo()
|
||||
|
||||
@Volatile private var prepared = false
|
||||
@Volatile private var released = false
|
||||
@Volatile
|
||||
private var prepared = false
|
||||
@Volatile
|
||||
private var released = false
|
||||
private var packetCount = 0L
|
||||
|
||||
fun feedPacket(data: ByteArray, ptsUs: Long, isConfig: Boolean) {
|
||||
if (released) return
|
||||
|
||||
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) {
|
||||
AUDIO_CODEC_OPUS -> prepareOpus(data)
|
||||
AUDIO_CODEC_AAC -> prepareAac(data)
|
||||
@@ -47,13 +53,7 @@ class ScrcpyAudioPlayer(private val codecId: Int) {
|
||||
}
|
||||
|
||||
if (codecId == AUDIO_CODEC_RAW) {
|
||||
ensureRawAudioTrack()?.let { track ->
|
||||
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)
|
||||
}
|
||||
}
|
||||
ensureRawAudioTrack()?.write(data, 0, data.size, AudioTrack.WRITE_NON_BLOCKING)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -74,11 +74,16 @@ class ScrcpyAudioPlayer(private val codecId: Int) {
|
||||
private fun prepareOpus(opusHead: ByteArray) {
|
||||
if (prepared || released) return
|
||||
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))
|
||||
// pre-skip field: 2 bytes LE at offset 10 of the OpusHead
|
||||
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
|
||||
format.setByteBuffer("csd-1", longBuffer(codecDelayNs))
|
||||
format.setByteBuffer("csd-2", longBuffer(OPUS_SEEK_PREROLL_NS))
|
||||
@@ -90,7 +95,8 @@ class ScrcpyAudioPlayer(private val codecId: Int) {
|
||||
private fun prepareAac(aacConfig: ByteArray) {
|
||||
if (prepared || released) return
|
||||
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))
|
||||
startCodecAndTrack(format)
|
||||
}.onFailure { Log.w(TAG, "prepareAac failed", it) }
|
||||
@@ -99,7 +105,11 @@ class ScrcpyAudioPlayer(private val codecId: Int) {
|
||||
private fun prepareFlac(flacConfig: ByteArray) {
|
||||
if (prepared || released) return
|
||||
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()) {
|
||||
format.setByteBuffer("csd-0", ByteBuffer.wrap(flacConfig))
|
||||
}
|
||||
@@ -161,11 +171,7 @@ class ScrcpyAudioPlayer(private val codecId: Int) {
|
||||
val pcm = ByteArray(size)
|
||||
outBuf.position(bufferInfo.offset)
|
||||
outBuf.get(pcm)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
track.write(pcm, 0, size, AudioTrack.WRITE_NON_BLOCKING)
|
||||
} else {
|
||||
track.write(pcm, 0, size)
|
||||
}
|
||||
track.write(pcm, 0, size, AudioTrack.WRITE_NON_BLOCKING)
|
||||
}
|
||||
codec.releaseOutputBuffer(idx, false)
|
||||
idx = codec.dequeueOutputBuffer(bufferInfo, 0L)
|
||||
@@ -190,9 +196,9 @@ class ScrcpyAudioPlayer(private val codecId: Int) {
|
||||
companion object {
|
||||
private const val TAG = "ScrcpyAudioPlayer"
|
||||
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_RAW = 0x00726177
|
||||
const val AUDIO_CODEC_RAW = 0x00726177
|
||||
private const val SAMPLE_RATE = 48000
|
||||
private const val CHANNELS = 2
|
||||
private const val CODEC_TIMEOUT_US = 10_000L
|
||||
|
||||
@@ -11,21 +11,25 @@ import java.io.InputStreamReader
|
||||
import java.nio.file.Path
|
||||
import java.security.SecureRandom
|
||||
import java.util.ArrayDeque
|
||||
import java.util.LinkedHashSet
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
||||
@Volatile
|
||||
private var activeSession: ActiveSession? = null
|
||||
|
||||
@Volatile
|
||||
private var videoConsumer: ((VideoPacket) -> Unit)? = null
|
||||
|
||||
@Volatile
|
||||
private var videoReaderThread: Thread? = null
|
||||
|
||||
@Volatile
|
||||
private var audioConsumer: ((AudioPacket) -> Unit)? = null
|
||||
|
||||
@Volatile
|
||||
private var audioReaderThread: Thread? = null
|
||||
|
||||
@Volatile
|
||||
private var lastServerCommand: String? = null
|
||||
private val serverLogBuffer = ArrayDeque<String>()
|
||||
@@ -44,7 +48,10 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
||||
adbService.push(serverJarPath, targetPath)
|
||||
val cmd = buildServerCommand(targetPath, scid, options)
|
||||
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 serverLogThread = startServerLogThread(serverStream, socketName)
|
||||
Thread.sleep(SERVER_BOOT_DELAY_MS)
|
||||
@@ -64,13 +71,16 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
||||
videoStream = firstStream
|
||||
videoInput = firstInput
|
||||
}
|
||||
|
||||
options.audio -> {
|
||||
audioStream = firstStream
|
||||
audioInput = firstInput
|
||||
}
|
||||
|
||||
options.control -> {
|
||||
controlStream = firstStream
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw IllegalArgumentException("At least one of video/audio/control must be enabled")
|
||||
}
|
||||
@@ -128,11 +138,15 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
||||
audioStream = audioStream,
|
||||
audioInput = audioInput,
|
||||
controlStream = controlStream,
|
||||
controlWriter = controlStream?.outputStream?.let { ScrcpyControlWriter(DataOutputStream(it)) },
|
||||
controlWriter = controlStream?.outputStream?.let {
|
||||
ScrcpyControlWriter(
|
||||
DataOutputStream(it)
|
||||
)
|
||||
},
|
||||
)
|
||||
return sessionInfo
|
||||
} catch (t: Throwable) {
|
||||
val tail = snapshotServerLogs(maxLines = 120)
|
||||
val tail = snapshotServerLogs()
|
||||
val detail = if (tail.isBlank()) "" else " | server_log_tail=\n$tail"
|
||||
throw IllegalStateException("scrcpy start failed: ${t.message}$detail", t)
|
||||
}
|
||||
@@ -173,7 +187,7 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
||||
)
|
||||
} catch (_: EOFException) {
|
||||
break
|
||||
} catch (e: InterruptedException) {
|
||||
} catch (_: InterruptedException) {
|
||||
// Ignore transient interrupts while session remains active.
|
||||
if (activeSession !== session || vStream.closed) {
|
||||
break
|
||||
@@ -212,10 +226,12 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
||||
Log.w(TAG, "audio disabled by server")
|
||||
return@thread
|
||||
}
|
||||
|
||||
AUDIO_ERROR -> {
|
||||
Log.e(TAG, "audio stream configuration error from server")
|
||||
return@thread
|
||||
}
|
||||
|
||||
else -> {
|
||||
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) {
|
||||
Log.w(
|
||||
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 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) {
|
||||
break
|
||||
} catch (e: InterruptedException) {
|
||||
} catch (_: InterruptedException) {
|
||||
if (activeSession !== session || aStream.closed) break
|
||||
Thread.interrupted()
|
||||
} catch (e: Exception) {
|
||||
@@ -279,12 +303,38 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
||||
actionButton: 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
|
||||
fun injectScroll(x: Int, y: Int, screenWidth: Int, screenHeight: Int, hScroll: Float, vScroll: Float, buttons: Int) {
|
||||
requireControlWriter().injectScroll(x, y, screenWidth, screenHeight, hScroll, vScroll, buttons)
|
||||
fun injectScroll(
|
||||
x: Int,
|
||||
y: Int,
|
||||
screenWidth: Int,
|
||||
screenHeight: Int,
|
||||
hScroll: Float,
|
||||
vScroll: Float,
|
||||
buttons: Int
|
||||
) {
|
||||
requireControlWriter().injectScroll(
|
||||
x,
|
||||
y,
|
||||
screenWidth,
|
||||
screenHeight,
|
||||
hScroll,
|
||||
vScroll,
|
||||
buttons
|
||||
)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@@ -384,10 +434,15 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
||||
}
|
||||
|
||||
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)
|
||||
return "CLASSPATH=$targetPath app_process / com.genymobile.scrcpy.Server ${options.serverVersion} $serverArgs"
|
||||
}
|
||||
@@ -474,7 +529,7 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
||||
ServerArg(
|
||||
type = ServerArgType.NUMBER,
|
||||
key = "video_bit_rate",
|
||||
value = options.videoBitRate, zeroValueBehavior = ZeroValueBehavior.KEEP,
|
||||
value = options.videoBitRate,
|
||||
),
|
||||
ServerArg(
|
||||
type = ServerArgType.NUMBER,
|
||||
@@ -528,7 +583,6 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
||||
key = "audio_bit_rate",
|
||||
value = options.audioBitRate,
|
||||
includeWhen = options.audio,
|
||||
zeroValueBehavior = ZeroValueBehavior.KEEP,
|
||||
),
|
||||
ServerArg(
|
||||
type = ServerArgType.STRING,
|
||||
@@ -539,7 +593,8 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
||||
ServerArg(
|
||||
type = ServerArgType.BOOLEAN,
|
||||
key = "audio_dup",
|
||||
value = options.audioDup, includeWhen = options.audioDup,
|
||||
value = options.audioDup,
|
||||
includeWhen = options.audioDup,
|
||||
),
|
||||
ServerArg(
|
||||
type = ServerArgType.STRING,
|
||||
@@ -579,15 +634,22 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
||||
ServerArg(
|
||||
type = ServerArgType.BOOLEAN,
|
||||
key = "list_encoders",
|
||||
value = options.listEncoders, includeWhen = options.listEncoders,
|
||||
value = options.listEncoders,
|
||||
includeWhen = options.listEncoders,
|
||||
),
|
||||
ServerArg(
|
||||
type = ServerArgType.BOOLEAN,
|
||||
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.
|
||||
// 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() }
|
||||
@@ -608,8 +670,8 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
||||
}
|
||||
|
||||
private enum class ZeroValueBehavior {
|
||||
KEEP,
|
||||
OMIT,
|
||||
KEEP,
|
||||
}
|
||||
|
||||
private data class ServerArg<T>(
|
||||
@@ -658,7 +720,7 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
||||
?.filter { it.isNotBlank() }
|
||||
.orEmpty()
|
||||
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 {
|
||||
return thread(start = true, name = "scrcpy-server-log") {
|
||||
try {
|
||||
BufferedReader(InputStreamReader(serverStream.inputStream, Charsets.UTF_8)).use { reader ->
|
||||
BufferedReader(
|
||||
InputStreamReader(
|
||||
serverStream.inputStream,
|
||||
Charsets.UTF_8
|
||||
)
|
||||
).use { reader ->
|
||||
while (true) {
|
||||
val line = reader.readLine() ?: break
|
||||
appendServerLog(line)
|
||||
@@ -740,7 +807,7 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun snapshotServerLogs(maxLines: Int): String {
|
||||
private fun snapshotServerLogs(maxLines: Int = 120): String {
|
||||
if (serverLogBuffer.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
@@ -748,7 +815,10 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
||||
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
|
||||
repeat(CONNECT_RETRY_COUNT) { attempt ->
|
||||
try {
|
||||
@@ -963,7 +1033,15 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
||||
}
|
||||
|
||||
@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)
|
||||
writePosition(x, y, screenWidth, screenHeight)
|
||||
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_FLAC = 0x666c6163
|
||||
private const val AUDIO_CODEC_RAW = 0x00726177
|
||||
|
||||
// Audio stream disable codes from server (writeDisableStream)
|
||||
private const val AUDIO_DISABLED = 0
|
||||
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 val VIDEO_ENCODER_REGEX = Regex("--video-encoder=([\\w.\\-]+)")
|
||||
private val AUDIO_ENCODER_REGEX = Regex("--audio-encoder=([\\w.\\-]+)")
|
||||
private val VIDEO_ENCODER_FALLBACK_REGEX = Regex("""--video-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 AUDIO_ENCODER_TYPE_REGEX = Regex("""--audio-codec=[^\s]+\s+--audio-encoder=([^\s]+).*?\((hw|sw)\)""")
|
||||
private val VIDEO_ENCODER_FALLBACK_REGEX = Regex("""--video-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 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_FALLBACK_REGEX = Regex("\\b([1-9][0-9]{1,4}x[1-9][0-9]{1,4})\\b")
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package io.github.miuzarte.scrcpyforandroid.pages
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
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.KeyboardOptions
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
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.scaffolds.AppPageLazyColumn
|
||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlide
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.launch
|
||||
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.SnackbarHostState
|
||||
import top.yukonga.miuix.kmp.basic.SpinnerEntry
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
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.SuperSpinner
|
||||
import top.yukonga.miuix.kmp.extra.SuperSwitch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private val AUDIO_SOURCE_OPTIONS = listOf(
|
||||
"output" to "output",
|
||||
@@ -142,15 +142,20 @@ internal fun AdvancedConfigPage(
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val maxSizePresetIndex = presetIndexFromInputForAdvancedPage(maxSizeInput, ScrcpyPresets.MaxSize)
|
||||
val maxSizePresetIndex =
|
||||
presetIndexFromInputForAdvancedPage(maxSizeInput, ScrcpyPresets.MaxSize)
|
||||
val maxFpsPresetIndex = presetIndexFromInputForAdvancedPage(maxFpsInput, ScrcpyPresets.MaxFPS)
|
||||
val audioSourceItems = AUDIO_SOURCE_OPTIONS.map { it.second }
|
||||
val audioSourceIndex = AUDIO_SOURCE_OPTIONS.indexOfFirst { it.first == audioSourcePreset }.let { if (it >= 0) it else 0 }
|
||||
val audioSourceIndex = AUDIO_SOURCE_OPTIONS.indexOfFirst { it.first == audioSourcePreset }
|
||||
.let { if (it >= 0) it else 0 }
|
||||
val videoSourceItems = VIDEO_SOURCE_OPTIONS.map { it.second }
|
||||
val videoSourceIndex = VIDEO_SOURCE_OPTIONS.indexOfFirst { it.first == videoSourcePreset }.let { if (it >= 0) it else 0 }
|
||||
val videoSourceIndex = VIDEO_SOURCE_OPTIONS.indexOfFirst { it.first == videoSourcePreset }
|
||||
.let { if (it >= 0) it else 0 }
|
||||
val cameraFacingItems = CAMERA_FACING_OPTIONS.map { it.second }
|
||||
val cameraFacingIndex = CAMERA_FACING_OPTIONS.indexOfFirst { it.first == cameraFacingPreset }.let { if (it >= 0) it else 0 }
|
||||
val cameraFpsPresetIndex = presetIndexFromInputForAdvancedPage(cameraFpsInput, CAMERA_FPS_PRESETS)
|
||||
val cameraFacingIndex = CAMERA_FACING_OPTIONS.indexOfFirst { it.first == cameraFacingPreset }
|
||||
.let { if (it >= 0) it else 0 }
|
||||
val cameraFpsPresetIndex =
|
||||
presetIndexFromInputForAdvancedPage(cameraFpsInput, CAMERA_FPS_PRESETS)
|
||||
val videoEncoderEntries = videoEncoderDropdownItems.map { encoderName ->
|
||||
if (encoderName == "默认") {
|
||||
SpinnerEntry(title = encoderName)
|
||||
@@ -269,7 +274,10 @@ internal fun AdvancedConfigPage(
|
||||
title = "摄像头分辨率",
|
||||
summary = "--camera-size",
|
||||
items = cameraSizeDropdownItems,
|
||||
selectedIndex = cameraSizeIndex.coerceIn(0, (cameraSizeDropdownItems.size - 1).coerceAtLeast(0)),
|
||||
selectedIndex = cameraSizeIndex.coerceIn(
|
||||
0,
|
||||
(cameraSizeDropdownItems.size - 1).coerceAtLeast(0)
|
||||
),
|
||||
onSelectedIndexChange = { index ->
|
||||
onCameraSizePresetChange(
|
||||
when (index) {
|
||||
|
||||
@@ -5,56 +5,48 @@ import android.app.Activity
|
||||
import android.util.Log
|
||||
import android.view.WindowManager
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.saveable.listSaver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
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.ScrcpySessionInfo
|
||||
import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults
|
||||
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
||||
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics
|
||||
import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget
|
||||
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
|
||||
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics
|
||||
import io.github.miuzarte.scrcpyforandroid.services.fetchConnectedDeviceInfo
|
||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn
|
||||
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.loadQuickDevices
|
||||
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.saveQuickDevices
|
||||
import io.github.miuzarte.scrcpyforandroid.services.updateQuickDeviceNameIfEmpty
|
||||
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.PairingCard
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.DeviceEditorScreen
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.DeviceTile
|
||||
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.QuickConnectCard
|
||||
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.VirtualButtonActions
|
||||
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.TimeoutCancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -77,11 +66,10 @@ import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import top.yukonga.miuix.kmp.basic.ScrollBehavior
|
||||
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.theme.MiuixTheme
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
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 LOG_TAG = "DevicePage"
|
||||
|
||||
private val DeviceShortcutStateListSaver = listSaver<androidx.compose.runtime.snapshots.SnapshotStateList<DeviceShortcut>, String>(
|
||||
save = { list ->
|
||||
list.map { item ->
|
||||
listOf(
|
||||
item.id,
|
||||
item.name,
|
||||
item.host,
|
||||
item.port.toString(),
|
||||
if (item.online) "1" else "0",
|
||||
).joinToString(DEVICE_SHORTCUT_SEPARATOR)
|
||||
}
|
||||
},
|
||||
restore = { saved ->
|
||||
saved.mapNotNull { line ->
|
||||
val parts = line.split(DEVICE_SHORTCUT_SEPARATOR)
|
||||
if (parts.size != 5) return@mapNotNull null
|
||||
val port = parts[3].toIntOrNull() ?: return@mapNotNull null
|
||||
DeviceShortcut(
|
||||
id = parts[0],
|
||||
name = parts[1],
|
||||
host = parts[2],
|
||||
port = port,
|
||||
online = parts[4] == "1",
|
||||
)
|
||||
}.toMutableStateList()
|
||||
},
|
||||
)
|
||||
private val DeviceShortcutStateListSaver =
|
||||
listSaver<androidx.compose.runtime.snapshots.SnapshotStateList<DeviceShortcut>, String>(
|
||||
save = { list ->
|
||||
list.map { item ->
|
||||
listOf(
|
||||
item.id,
|
||||
item.name,
|
||||
item.host,
|
||||
item.port.toString(),
|
||||
if (item.online) "1" else "0",
|
||||
).joinToString(DEVICE_SHORTCUT_SEPARATOR)
|
||||
}
|
||||
},
|
||||
restore = { saved ->
|
||||
saved.mapNotNull { line ->
|
||||
val parts = line.split(DEVICE_SHORTCUT_SEPARATOR)
|
||||
if (parts.size != 5) return@mapNotNull null
|
||||
val port = parts[3].toIntOrNull() ?: return@mapNotNull null
|
||||
DeviceShortcut(
|
||||
id = parts[0],
|
||||
name = parts[1],
|
||||
host = parts[2],
|
||||
port = port,
|
||||
online = parts[4] == "1",
|
||||
)
|
||||
}.toMutableStateList()
|
||||
},
|
||||
)
|
||||
|
||||
private val StringStateListSaver = listSaver<androidx.compose.runtime.snapshots.SnapshotStateList<String>, String>(
|
||||
save = { it.toList() },
|
||||
restore = { it.toMutableStateList() },
|
||||
)
|
||||
private val StringStateListSaver =
|
||||
listSaver<androidx.compose.runtime.snapshots.SnapshotStateList<String>, String>(
|
||||
save = { it.toList() },
|
||||
restore = { it.toMutableStateList() },
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun DeviceTabScreen(
|
||||
@@ -231,7 +221,7 @@ fun DeviceTabScreen(
|
||||
var statusLine by rememberSaveable { mutableStateOf("未连接") }
|
||||
var adbConnected by rememberSaveable { mutableStateOf(false) }
|
||||
var currentTargetHost by rememberSaveable { mutableStateOf("") }
|
||||
var currentTargetPort by rememberSaveable { mutableIntStateOf(AppDefaults.DefaultAdbPort) }
|
||||
var currentTargetPort by rememberSaveable { mutableIntStateOf(AppDefaults.ADB_PORT) }
|
||||
var connectedDeviceLabel by rememberSaveable { mutableStateOf("未连接") }
|
||||
var sessionInfoWidth by rememberSaveable { mutableIntStateOf(0) }
|
||||
var sessionInfoHeight by rememberSaveable { mutableIntStateOf(0) }
|
||||
@@ -241,7 +231,13 @@ fun DeviceTabScreen(
|
||||
var sessionInfo by remember {
|
||||
mutableStateOf<ScrcpySessionInfo?>(null)
|
||||
}
|
||||
LaunchedEffect(sessionInfoWidth, sessionInfoHeight, sessionInfoDeviceName, sessionInfoCodec, sessionInfoControlEnabled) {
|
||||
LaunchedEffect(
|
||||
sessionInfoWidth,
|
||||
sessionInfoHeight,
|
||||
sessionInfoDeviceName,
|
||||
sessionInfoCodec,
|
||||
sessionInfoControlEnabled
|
||||
) {
|
||||
sessionInfo = if (sessionInfoDeviceName.isNotBlank()) {
|
||||
ScrcpySessionInfo(
|
||||
width = sessionInfoWidth,
|
||||
@@ -261,18 +257,22 @@ fun DeviceTabScreen(
|
||||
var adbConnecting by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
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 audioForwardingSupported by rememberSaveable { mutableStateOf(true) }
|
||||
var cameraMirroringSupported by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
var bitRateMbps by rememberSaveable { mutableFloatStateOf(initialSettings.bitRateMbps) }
|
||||
var bitRateInput by rememberSaveable { mutableStateOf(initialSettings.bitRateInput) }
|
||||
var bitRateMbps by rememberSaveable { mutableFloatStateOf(initialSettings.videoBitRateMbps) }
|
||||
var bitRateInput by rememberSaveable { mutableStateOf(initialSettings.videoBitRateInput) }
|
||||
var audioBitRateKbps by rememberSaveable { mutableIntStateOf(initialSettings.audioBitRateKbps) }
|
||||
val currentTarget = if (currentTargetHost.isNotBlank()) ConnectionTarget(currentTargetHost, currentTargetPort) else null
|
||||
val currentTarget = if (currentTargetHost.isNotBlank()) ConnectionTarget(
|
||||
currentTargetHost,
|
||||
currentTargetPort
|
||||
) else null
|
||||
|
||||
val eventLog = rememberSaveable(saver = StringStateListSaver) { mutableStateListOf() }
|
||||
val quickDevices = rememberSaveable(saver = DeviceShortcutStateListSaver) { mutableStateListOf() }
|
||||
val quickDevices =
|
||||
rememberSaveable(saver = DeviceShortcutStateListSaver) { mutableStateListOf() }
|
||||
|
||||
LaunchedEffect(eventLog.size) {
|
||||
onCanClearLogsChange(eventLog.isNotEmpty())
|
||||
@@ -282,13 +282,25 @@ fun DeviceTabScreen(
|
||||
val timestamp = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())
|
||||
val line = "[$timestamp] $message"
|
||||
eventLog.add(0, line)
|
||||
if (eventLog.size > AppDefaults.MaxEventLogLines) {
|
||||
eventLog.removeRange(AppDefaults.MaxEventLogLines, eventLog.size)
|
||||
if (eventLog.size > AppDefaults.EVENT_LOG_LINES) {
|
||||
eventLog.removeRange(AppDefaults.EVENT_LOG_LINES, eventLog.size)
|
||||
}
|
||||
when (level) {
|
||||
Log.ERROR -> if (error != null) Log.e(LOG_TAG, message, error) else Log.e(LOG_TAG, 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)
|
||||
Log.ERROR -> if (error != null) Log.e(LOG_TAG, message, error) else Log.e(
|
||||
LOG_TAG,
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -298,13 +310,19 @@ fun DeviceTabScreen(
|
||||
audioForwardingSupported = audioSupported
|
||||
if (!audioSupported && audioEnabled) {
|
||||
onAudioEnabledChange(false)
|
||||
logEvent("设备 Android ${release.ifBlank { "?" }} (SDK $sdkInt) 不支持音频转发,已自动关闭", Log.WARN)
|
||||
logEvent(
|
||||
"设备 Android ${release.ifBlank { "?" }} (SDK $sdkInt) 不支持音频转发,已自动关闭",
|
||||
Log.WARN
|
||||
)
|
||||
}
|
||||
val cameraSupported = sdkInt !in 0..<31
|
||||
cameraMirroringSupported = cameraSupported
|
||||
if (!cameraSupported && videoSourcePreset == "camera") {
|
||||
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() {
|
||||
if (!adbConnected) return
|
||||
val remotePath = serverRemotePath.trim().ifBlank { AppDefaults.DefaultServerRemotePath }
|
||||
val remotePath = serverRemotePath.trim().ifBlank { AppDefaults.SERVER_REMOTE_PATH }
|
||||
runCatching {
|
||||
nativeCore.scrcpyListEncoders(
|
||||
customServerUri = customServerUri,
|
||||
@@ -413,7 +431,7 @@ fun DeviceTabScreen(
|
||||
|
||||
fun refreshCameraSizeLists() {
|
||||
if (!adbConnected) return
|
||||
val remotePath = serverRemotePath.trim().ifBlank { AppDefaults.DefaultServerRemotePath }
|
||||
val remotePath = serverRemotePath.trim().ifBlank { AppDefaults.SERVER_REMOTE_PATH }
|
||||
runCatching {
|
||||
nativeCore.scrcpyListCameraSizes(
|
||||
customServerUri = customServerUri,
|
||||
@@ -470,20 +488,14 @@ fun DeviceTabScreen(
|
||||
|
||||
LaunchedEffect(
|
||||
quickConnectInput,
|
||||
audioBitRateKbps,
|
||||
bitRateMbps,
|
||||
bitRateInput,
|
||||
audioBitRateKbps,
|
||||
maxSizeInput,
|
||||
maxFpsInput,
|
||||
turnScreenOff,
|
||||
noControl,
|
||||
videoEncoder,
|
||||
videoCodecOptions,
|
||||
audioEncoder,
|
||||
audioCodecOptions,
|
||||
audioDup,
|
||||
audioSourcePreset,
|
||||
audioSourceCustom,
|
||||
noVideo,
|
||||
videoSourcePreset,
|
||||
displayIdInput,
|
||||
cameraIdInput,
|
||||
cameraFacingPreset,
|
||||
cameraSizePreset,
|
||||
@@ -491,14 +503,20 @@ fun DeviceTabScreen(
|
||||
cameraArInput,
|
||||
cameraFpsInput,
|
||||
cameraHighSpeed,
|
||||
audioSourcePreset,
|
||||
audioSourceCustom,
|
||||
audioDup,
|
||||
noAudioPlayback,
|
||||
noVideo,
|
||||
requireAudio,
|
||||
turnScreenOff,
|
||||
maxSizeInput,
|
||||
maxFpsInput,
|
||||
videoEncoder,
|
||||
videoCodecOptions,
|
||||
audioEncoder,
|
||||
audioCodecOptions,
|
||||
newDisplayWidth,
|
||||
newDisplayHeight,
|
||||
newDisplayDpi,
|
||||
displayIdInput,
|
||||
cropWidth,
|
||||
cropHeight,
|
||||
cropX,
|
||||
@@ -508,35 +526,36 @@ fun DeviceTabScreen(
|
||||
context,
|
||||
DevicePageSettings(
|
||||
quickConnectInput = quickConnectInput,
|
||||
bitRateMbps = bitRateMbps,
|
||||
bitRateInput = bitRateInput,
|
||||
audioBitRateKbps = audioBitRateKbps,
|
||||
maxSizeInput = maxSizeInput,
|
||||
maxFpsInput = maxFpsInput,
|
||||
audioBitRateInput = audioBitRateKbps.toString(),
|
||||
videoBitRateMbps = bitRateMbps,
|
||||
videoBitRateInput = bitRateInput,
|
||||
turnScreenOff = turnScreenOff,
|
||||
noControl = noControl,
|
||||
videoEncoder = videoEncoder,
|
||||
videoCodecOptions = videoCodecOptions,
|
||||
audioEncoder = audioEncoder,
|
||||
audioCodecOptions = audioCodecOptions,
|
||||
audioDup = audioDup,
|
||||
audioSourcePreset = audioSourcePreset,
|
||||
audioSourceCustom = audioSourceCustom,
|
||||
noVideo = noVideo,
|
||||
videoSourcePreset = videoSourcePreset,
|
||||
displayIdInput = displayIdInput,
|
||||
cameraIdInput = cameraIdInput,
|
||||
cameraFacingPreset = cameraFacingPreset,
|
||||
cameraSizePreset = cameraSizePreset,
|
||||
cameraSizeCustom = cameraSizeCustom,
|
||||
cameraArInput = cameraArInput,
|
||||
cameraFpsInput = cameraFpsInput,
|
||||
cameraAr = cameraArInput,
|
||||
cameraFps = cameraFpsInput,
|
||||
cameraHighSpeed = cameraHighSpeed,
|
||||
audioSourcePreset = audioSourcePreset,
|
||||
audioSourceCustom = audioSourceCustom,
|
||||
audioDup = audioDup,
|
||||
noAudioPlayback = noAudioPlayback,
|
||||
noVideo = noVideo,
|
||||
requireAudio = requireAudio,
|
||||
turnScreenOff = turnScreenOff,
|
||||
maxSizeInput = maxSizeInput,
|
||||
maxFpsInput = maxFpsInput,
|
||||
videoEncoder = videoEncoder,
|
||||
videoCodecOptions = videoCodecOptions,
|
||||
audioEncoder = audioEncoder,
|
||||
audioCodecOptions = audioCodecOptions,
|
||||
newDisplayWidth = newDisplayWidth,
|
||||
newDisplayHeight = newDisplayHeight,
|
||||
newDisplayDpi = newDisplayDpi,
|
||||
displayIdInput = displayIdInput,
|
||||
cropWidth = cropWidth,
|
||||
cropHeight = cropHeight,
|
||||
cropX = cropX,
|
||||
@@ -747,7 +766,8 @@ fun DeviceTabScreen(
|
||||
itemsIndexed(quickDevices, key = { _, device -> device.id }) { _, device ->
|
||||
val host = device.host
|
||||
val port = device.port
|
||||
val isConnectedTarget = adbConnected && currentTarget?.host == host && currentTarget.port == port
|
||||
val isConnectedTarget =
|
||||
adbConnected && currentTarget?.host == host && currentTarget.port == port
|
||||
|
||||
DeviceTile(
|
||||
device = device,
|
||||
@@ -768,7 +788,7 @@ fun DeviceTabScreen(
|
||||
nativeCore.adbDisconnect()
|
||||
adbConnected = false
|
||||
currentTargetHost = ""
|
||||
currentTargetPort = AppDefaults.DefaultAdbPort
|
||||
currentTargetPort = AppDefaults.ADB_PORT
|
||||
audioForwardingSupported = true
|
||||
cameraMirroringSupported = true
|
||||
sessionInfo = null
|
||||
@@ -809,7 +829,13 @@ fun DeviceTabScreen(
|
||||
enabled = !adbConnecting,
|
||||
onAddDevice = {
|
||||
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 {
|
||||
snack.showSnackbar("已添加设备: ${target.host}:${target.port}")
|
||||
}
|
||||
@@ -840,10 +866,13 @@ fun DeviceTabScreen(
|
||||
runBusy("执行配对") {
|
||||
val ok = nativeCore.adbPair(
|
||||
host.trim(),
|
||||
port.toIntOrNull() ?: AppDefaults.DefaultAdbPort,
|
||||
port.toIntOrNull() ?: AppDefaults.ADB_PORT,
|
||||
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 {
|
||||
snack.showSnackbar(if (ok) "配对成功" else "配对失败")
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ package io.github.miuzarte.scrcpyforandroid.pages
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.pm.ActivityInfo
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -21,8 +21,8 @@ import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade
|
||||
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.widgets.FullscreenControlScreen
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonBar
|
||||
import top.yukonga.miuix.kmp.basic.Scaffold
|
||||
@@ -56,7 +56,9 @@ fun FullscreenControlPage(
|
||||
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) {
|
||||
mutableStateOf(
|
||||
ScrcpySessionInfo(
|
||||
@@ -88,12 +90,15 @@ fun FullscreenControlPage(
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
val controller = WindowInsetsControllerCompat(window, window.decorView)
|
||||
controller.hide(WindowInsetsCompat.Type.systemBars())
|
||||
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
controller.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
onDispose {
|
||||
val restoreWindow = activity?.window
|
||||
if (restoreWindow != null) {
|
||||
WindowInsetsControllerCompat(restoreWindow, restoreWindow.decorView).show(WindowInsetsCompat.Type.systemBars())
|
||||
WindowInsetsControllerCompat(restoreWindow, restoreWindow.decorView).show(
|
||||
WindowInsetsCompat.Type.systemBars()
|
||||
)
|
||||
WindowCompat.setDecorFitsSystemWindows(restoreWindow, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import androidx.activity.compose.PredictiveBackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
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.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberDecoratedNavEntries
|
||||
@@ -92,14 +91,18 @@ fun MainPage() {
|
||||
val initialDeviceSettings = remember(context) { loadDevicePageSettings(context) }
|
||||
val snackHostState = remember { SnackbarHostState() }
|
||||
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 saveableStateHolder = rememberSaveableStateHolder()
|
||||
val scope = rememberCoroutineScope()
|
||||
val rootBackStack = remember { mutableStateListOf<NavKey>(RootScreen.Home) }
|
||||
val currentRootScreen = rootBackStack.lastOrNull() as? RootScreen ?: RootScreen.Home
|
||||
val deviceScrollBehavior = MiuixScrollBehavior(canScroll = { currentTab == MainTabDestination.Device })
|
||||
val settingsScrollBehavior = MiuixScrollBehavior(canScroll = { currentTab == MainTabDestination.Settings })
|
||||
val deviceScrollBehavior =
|
||||
MiuixScrollBehavior(canScroll = { currentTab == MainTabDestination.Device })
|
||||
val settingsScrollBehavior =
|
||||
MiuixScrollBehavior(canScroll = { currentTab == MainTabDestination.Settings })
|
||||
val advancedScrollBehavior = MiuixScrollBehavior(
|
||||
canScroll = {
|
||||
currentRootScreen is RootScreen.Advanced || currentRootScreen is RootScreen.VirtualButtonOrder
|
||||
@@ -110,27 +113,23 @@ fun MainPage() {
|
||||
restore = { restored -> restored.toList() },
|
||||
)
|
||||
|
||||
var audioEnabled by rememberSaveable { mutableStateOf(initialSettings.audioEnabled) }
|
||||
var audioCodec by rememberSaveable { mutableStateOf(initialSettings.audioCodec) }
|
||||
var videoCodec by rememberSaveable { mutableStateOf(initialSettings.videoCodec) }
|
||||
var themeBaseIndex by rememberSaveable { mutableIntStateOf(initialSettings.themeBaseIndex) }
|
||||
var monetEnabled by rememberSaveable { mutableStateOf(initialSettings.monetEnabled) }
|
||||
var fullscreenDebugInfoEnabled by rememberSaveable { mutableStateOf(initialSettings.fullscreenDebugInfoEnabled) }
|
||||
var showFullscreenVirtualButtons by rememberSaveable {
|
||||
mutableStateOf(initialSettings.showFullscreenVirtualButtons)
|
||||
}
|
||||
var keepScreenOnWhenStreamingEnabled by rememberSaveable {
|
||||
mutableStateOf(initialSettings.keepScreenOnWhenStreamingEnabled)
|
||||
}
|
||||
var showFullscreenVirtualButtons by rememberSaveable { mutableStateOf(initialSettings.showFullscreenVirtualButtons) }
|
||||
var keepScreenOnWhenStreamingEnabled by rememberSaveable { mutableStateOf(initialSettings.keepScreenOnWhenStreamingEnabled) }
|
||||
var devicePreviewCardHeightDp by rememberSaveable { mutableIntStateOf(initialSettings.devicePreviewCardHeightDp) }
|
||||
var virtualButtonsOutside by rememberSaveable(stateSaver = stringListSaver) {
|
||||
mutableStateOf(VirtualButtonActions.parseStoredIds(initialSettings.virtualButtonsOutside))
|
||||
}
|
||||
var virtualButtonsInMore by rememberSaveable(stateSaver = stringListSaver) {
|
||||
mutableStateOf(VirtualButtonActions.parseStoredIds(initialSettings.virtualButtonsInMore))
|
||||
}
|
||||
var devicePreviewCardHeightDp by rememberSaveable { mutableIntStateOf(initialSettings.devicePreviewCardHeightDp) }
|
||||
var customServerUri by rememberSaveable { mutableStateOf(initialSettings.customServerUri) }
|
||||
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 noControl by rememberSaveable { mutableStateOf(initialDeviceSettings.noControl) }
|
||||
var videoEncoder by rememberSaveable { mutableStateOf(initialDeviceSettings.videoEncoder) }
|
||||
@@ -145,8 +144,8 @@ fun MainPage() {
|
||||
var cameraFacingPreset by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraFacingPreset) }
|
||||
var cameraSizePreset by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraSizePreset) }
|
||||
var cameraSizeCustom by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraSizeCustom) }
|
||||
var cameraArInput by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraArInput) }
|
||||
var cameraFpsInput by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraFpsInput) }
|
||||
var cameraArInput by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraAr) }
|
||||
var cameraFpsInput by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraFps) }
|
||||
var cameraHighSpeed by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraHighSpeed) }
|
||||
var noAudioPlayback by rememberSaveable { mutableStateOf(initialDeviceSettings.noAudioPlayback) }
|
||||
var noVideo by rememberSaveable { mutableStateOf(initialDeviceSettings.noVideo) }
|
||||
@@ -178,47 +177,48 @@ fun MainPage() {
|
||||
val themeController = remember(themeMode) { ThemeController(colorSchemeMode = themeMode) }
|
||||
|
||||
LaunchedEffect(
|
||||
audioEnabled,
|
||||
audioCodec,
|
||||
videoCodec,
|
||||
themeBaseIndex,
|
||||
monetEnabled,
|
||||
fullscreenDebugInfoEnabled,
|
||||
showFullscreenVirtualButtons,
|
||||
keepScreenOnWhenStreamingEnabled,
|
||||
devicePreviewCardHeightDp,
|
||||
virtualButtonsOutside,
|
||||
virtualButtonsInMore,
|
||||
devicePreviewCardHeightDp,
|
||||
customServerUri,
|
||||
serverRemotePath,
|
||||
videoCodec,
|
||||
audioEnabled,
|
||||
audioCodec,
|
||||
adbKeyName,
|
||||
) {
|
||||
saveMainSettings(
|
||||
context,
|
||||
MainSettings(
|
||||
audioEnabled = audioEnabled,
|
||||
audioCodec = audioCodec,
|
||||
videoCodec = videoCodec,
|
||||
themeBaseIndex = themeBaseIndex,
|
||||
monetEnabled = monetEnabled,
|
||||
fullscreenDebugInfoEnabled = fullscreenDebugInfoEnabled,
|
||||
showFullscreenVirtualButtons = showFullscreenVirtualButtons,
|
||||
keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled,
|
||||
devicePreviewCardHeightDp = devicePreviewCardHeightDp,
|
||||
virtualButtonsOutside = VirtualButtonActions.encodeStoredIds(virtualButtonsOutside),
|
||||
virtualButtonsInMore = VirtualButtonActions.encodeStoredIds(virtualButtonsInMore),
|
||||
devicePreviewCardHeightDp = devicePreviewCardHeightDp,
|
||||
customServerUri = customServerUri,
|
||||
serverRemotePath = serverRemotePath,
|
||||
videoCodec = videoCodec,
|
||||
audioEnabled = audioEnabled,
|
||||
audioCodec = audioCodec,
|
||||
adbKeyName = 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() {
|
||||
if (rootBackStack.size > 1) {
|
||||
@@ -234,8 +234,8 @@ fun MainPage() {
|
||||
pagerState.animateScrollToPage(
|
||||
page = MainTabDestination.Device.ordinal,
|
||||
animationSpec = spring(
|
||||
dampingRatio = UiMotion.PageSwitchDampingRatio,
|
||||
stiffness = UiMotion.PageSwitchStiffness,
|
||||
dampingRatio = UiMotion.PAGE_SWITCH_DAMPING_RATIO,
|
||||
stiffness = UiMotion.PAGE_SWITCH_STIFFNESS,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -251,403 +251,424 @@ fun MainPage() {
|
||||
}
|
||||
}
|
||||
|
||||
val picker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? ->
|
||||
if (uri == null) return@rememberLauncherForActivityResult
|
||||
runCatching {
|
||||
context.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
val picker =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? ->
|
||||
if (uri == null) return@rememberLauncherForActivityResult
|
||||
runCatching {
|
||||
context.contentResolver.takePersistableUriPermission(
|
||||
uri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
)
|
||||
}
|
||||
customServerUri = uri.toString()
|
||||
}
|
||||
customServerUri = uri.toString()
|
||||
}
|
||||
|
||||
val rootEntryProvider = entryProvider<NavKey> {
|
||||
entry(RootScreen.Home) {
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
NavigationBar {
|
||||
tabs.forEach { tab ->
|
||||
NavigationBarItem(
|
||||
selected = currentTab == tab,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(
|
||||
page = tab.ordinal,
|
||||
animationSpec = spring(
|
||||
dampingRatio = UiMotion.PageSwitchDampingRatio,
|
||||
stiffness = UiMotion.PageSwitchStiffness,
|
||||
),
|
||||
entry(RootScreen.Home) {
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
NavigationBar {
|
||||
tabs.forEach { tab ->
|
||||
NavigationBarItem(
|
||||
selected = currentTab == tab,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(
|
||||
page = tab.ordinal,
|
||||
animationSpec = spring(
|
||||
dampingRatio = UiMotion.PAGE_SWITCH_DAMPING_RATIO,
|
||||
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,
|
||||
label = tab.label,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
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(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
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 },
|
||||
MainTabDestination.Settings -> Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = tab.title,
|
||||
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) {
|
||||
val videoEncoderDropdownItems = listOf("默认") + videoEncoderOptions
|
||||
val audioEncoderDropdownItems = listOf("默认") + audioEncoderOptions
|
||||
val videoEncoderIndex = (videoEncoderOptions.indexOf(videoEncoder) + 1).coerceAtLeast(0)
|
||||
val audioEncoderIndex = (audioEncoderOptions.indexOf(audioEncoder) + 1).coerceAtLeast(0)
|
||||
entry(RootScreen.Advanced) {
|
||||
val videoEncoderDropdownItems = listOf("默认") + videoEncoderOptions
|
||||
val audioEncoderDropdownItems = listOf("默认") + audioEncoderOptions
|
||||
val videoEncoderIndex = (videoEncoderOptions.indexOf(videoEncoder) + 1).coerceAtLeast(0)
|
||||
val audioEncoderIndex = (audioEncoderOptions.indexOf(audioEncoder) + 1).coerceAtLeast(0)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = "高级参数",
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { popRoot() }) {
|
||||
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
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = "高级参数",
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { popRoot() }) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "返回"
|
||||
)
|
||||
}
|
||||
},
|
||||
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() },
|
||||
},
|
||||
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,
|
||||
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(
|
||||
@@ -724,7 +745,8 @@ private fun DeviceMenuPopupItem(
|
||||
}
|
||||
|
||||
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,
|
||||
fontSize = MiuixTheme.textStyles.body1.fontSize,
|
||||
|
||||
@@ -22,7 +22,6 @@ import top.yukonga.miuix.kmp.basic.IconButton
|
||||
import top.yukonga.miuix.kmp.basic.ScrollBehavior
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
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.SuperSwitch
|
||||
import top.yukonga.miuix.kmp.theme.ColorSchemeMode
|
||||
@@ -61,12 +60,8 @@ fun SettingsScreen(
|
||||
onMonetEnabledChange: (Boolean) -> Unit,
|
||||
fullscreenDebugInfoEnabled: Boolean,
|
||||
onFullscreenDebugInfoEnabledChange: (Boolean) -> Unit,
|
||||
showFullscreenVirtualButtons: Boolean,
|
||||
onShowFullscreenVirtualButtonsChange: (Boolean) -> Unit,
|
||||
keepScreenOnWhenStreamingEnabled: Boolean,
|
||||
onKeepScreenOnWhenStreamingEnabledChange: (Boolean) -> Unit,
|
||||
onOpenReorderDevices: () -> Unit,
|
||||
onOpenVirtualButtonOrder: () -> Unit,
|
||||
devicePreviewCardHeightDp: Int,
|
||||
onDevicePreviewCardHeightDpChange: (Int) -> Unit,
|
||||
customServerUri: String?,
|
||||
@@ -111,12 +106,6 @@ fun SettingsScreen(
|
||||
checked = fullscreenDebugInfoEnabled,
|
||||
onCheckedChange = onFullscreenDebugInfoEnabledChange,
|
||||
)
|
||||
SuperSwitch(
|
||||
title = "全屏显示虚拟按钮",
|
||||
summary = "在全屏模式底部显示返回/主页等虚拟按钮",
|
||||
checked = showFullscreenVirtualButtons,
|
||||
onCheckedChange = onShowFullscreenVirtualButtonsChange,
|
||||
)
|
||||
SuperSwitch(
|
||||
title = "投屏时不允许息屏",
|
||||
summary = "Scrcpy 启动后保持本机常亮,避免锁屏导致 ADB 断开",
|
||||
@@ -127,7 +116,11 @@ fun SettingsScreen(
|
||||
title = "预览卡高度",
|
||||
summary = "设备页预览卡高度",
|
||||
value = devicePreviewCardHeightDp.toFloat(),
|
||||
onValueChange = { onDevicePreviewCardHeightDpChange(it.roundToInt().coerceAtLeast(120)) },
|
||||
onValueChange = {
|
||||
onDevicePreviewCardHeightDpChange(
|
||||
it.roundToInt().coerceAtLeast(120)
|
||||
)
|
||||
},
|
||||
valueRange = 160f..600f,
|
||||
steps = 439,
|
||||
unit = "dp",
|
||||
@@ -136,19 +129,10 @@ fun SettingsScreen(
|
||||
inputFilter = { it.filter(Char::isDigit) },
|
||||
inputValueRange = 120f..Float.MAX_VALUE,
|
||||
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")
|
||||
@@ -192,7 +176,7 @@ fun SettingsScreen(
|
||||
TextField(
|
||||
value = serverRemotePath,
|
||||
onValueChange = onServerRemotePathChange,
|
||||
label = AppDefaults.DefaultServerRemotePath,
|
||||
label = AppDefaults.SERVER_REMOTE_PATH,
|
||||
useLabelAsPlaceholder = true,
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
@@ -214,7 +198,7 @@ fun SettingsScreen(
|
||||
TextField(
|
||||
value = adbKeyName,
|
||||
onValueChange = onAdbKeyNameChange,
|
||||
label = AppDefaults.DefaultAdbKeyName,
|
||||
label = AppDefaults.ADB_KEY_NAME,
|
||||
useLabelAsPlaceholder = true,
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
|
||||
@@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -54,7 +53,11 @@ internal fun VirtualButtonOrderPage(
|
||||
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)
|
||||
if (fromIndex < 0) return
|
||||
|
||||
@@ -88,7 +91,8 @@ internal fun VirtualButtonOrderPage(
|
||||
}
|
||||
|
||||
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 (payload.itemId == VirtualButtonAction.MORE.id) return
|
||||
transferToOther(outsideState, moreState, payload.itemId, payload.deltaY)
|
||||
@@ -98,7 +102,8 @@ internal fun VirtualButtonOrderPage(
|
||||
}
|
||||
|
||||
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) {
|
||||
transferToOther(moreState, outsideState, payload.itemId, payload.deltaY)
|
||||
} else {
|
||||
|
||||
@@ -58,7 +58,8 @@ fun SuperSlide(
|
||||
holdDownState = holdArrow,
|
||||
endActions = {
|
||||
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 text = if (shouldShowUnit) "$valueText $unit" else valueText
|
||||
Text(
|
||||
|
||||
@@ -6,215 +6,495 @@ import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults
|
||||
import io.github.miuzarte.scrcpyforandroid.constants.AppPreferenceKeys
|
||||
|
||||
internal data class MainSettings(
|
||||
val themeBaseIndex: Int = AppDefaults.DefaultThemeBaseIndex,
|
||||
val monetEnabled: Boolean = AppDefaults.DefaultMonetEnabled,
|
||||
val fullscreenDebugInfoEnabled: Boolean = AppDefaults.DefaultFullscreenDebugInfoEnabled,
|
||||
val showFullscreenVirtualButtons: Boolean = AppDefaults.DefaultShowFullscreenVirtualButtons,
|
||||
val devicePreviewCardHeightDp: Int = AppDefaults.DefaultDevicePreviewCardHeightDp,
|
||||
val keepScreenOnWhenStreamingEnabled: Boolean = AppDefaults.DefaultKeepScreenOnWhenStreamingEnabled,
|
||||
val virtualButtonsOutside: String = AppDefaults.DefaultVirtualButtonsOutside,
|
||||
val virtualButtonsInMore: String = AppDefaults.DefaultVirtualButtonsInMore,
|
||||
val customServerUri: String? = null,
|
||||
val serverRemotePath: String = AppDefaults.DefaultServerRemotePathInput,
|
||||
val videoCodec: String = AppDefaults.DefaultVideoCodec,
|
||||
val audioEnabled: Boolean = AppDefaults.DefaultAudioEnabled,
|
||||
val audioCodec: String = AppDefaults.DefaultAudioCodec,
|
||||
val adbKeyName: String = AppDefaults.DefaultAdbKeyNameInput,
|
||||
val audioEnabled: Boolean = AppDefaults.AUDIO_ENABLED,
|
||||
val audioCodec: String = AppDefaults.AUDIO_CODEC,
|
||||
val videoCodec: String = AppDefaults.VIDEO_CODEC,
|
||||
val themeBaseIndex: Int = AppDefaults.THEME_BASE_INDEX,
|
||||
val monetEnabled: Boolean = AppDefaults.MONET,
|
||||
val fullscreenDebugInfoEnabled: Boolean = AppDefaults.FULLSCREEN_DEBUG_INFO,
|
||||
val showFullscreenVirtualButtons: Boolean = AppDefaults.SHOW_FULLSCREEN_VIRTUAL_BUTTONS,
|
||||
val keepScreenOnWhenStreamingEnabled: Boolean = AppDefaults.KEEP_SCREEN_ON_WHEN_STREAMING,
|
||||
val devicePreviewCardHeightDp: Int = AppDefaults.DEVICE_PREVIEW_CARD_HEIGHT_DP,
|
||||
val virtualButtonsOutside: String = AppDefaults.VIRTUAL_BUTTONS_OUTSIDE,
|
||||
val virtualButtonsInMore: String = AppDefaults.VIRTUAL_BUTTONS_IN_MORE,
|
||||
val customServerUri: String? = AppDefaults.CUSTOM_SERVER_URI,
|
||||
val serverRemotePath: String = AppDefaults.SERVER_REMOTE_PATH_INPUT,
|
||||
val adbKeyName: String = AppDefaults.ADB_KEY_NAME_INPUT,
|
||||
)
|
||||
|
||||
internal data class DevicePageSettings(
|
||||
val pairHost: String = AppDefaults.DefaultPairHost,
|
||||
val pairPort: String = AppDefaults.DefaultPairPort,
|
||||
val pairCode: String = AppDefaults.DefaultPairCode,
|
||||
val quickConnectInput: String = AppDefaults.DefaultQuickConnectInput,
|
||||
val bitRateMbps: Float = AppDefaults.DefaultBitRateMbps,
|
||||
val bitRateInput: String = AppDefaults.DefaultBitRateInput,
|
||||
val audioBitRateKbps: Int = AppDefaults.DefaultAudioBitRateKbps,
|
||||
val maxSizeInput: String = AppDefaults.DefaultMaxSizeInput,
|
||||
val maxFpsInput: String = AppDefaults.DefaultMaxFpsInput,
|
||||
val noControl: Boolean = AppDefaults.DefaultNoControl,
|
||||
val videoEncoder: String = AppDefaults.DefaultVideoEncoder,
|
||||
val videoCodecOptions: String = AppDefaults.DefaultVideoCodecOptions,
|
||||
val audioEncoder: String = AppDefaults.DefaultAudioEncoder,
|
||||
val audioCodecOptions: String = AppDefaults.DefaultAudioCodecOptions,
|
||||
val audioDup: Boolean = AppDefaults.DefaultAudioDup,
|
||||
val audioSourcePreset: String = AppDefaults.DefaultAudioSourcePreset,
|
||||
val audioSourceCustom: String = AppDefaults.DefaultAudioSourceCustom,
|
||||
val videoSourcePreset: String = AppDefaults.DefaultVideoSourcePreset,
|
||||
val cameraIdInput: String = AppDefaults.DefaultCameraIdInput,
|
||||
val cameraFacingPreset: String = AppDefaults.DefaultCameraFacingPreset,
|
||||
val cameraSizePreset: String = AppDefaults.DefaultCameraSizePreset,
|
||||
val cameraSizeCustom: String = AppDefaults.DefaultCameraSizeCustom,
|
||||
val cameraArInput: String = AppDefaults.DefaultCameraArInput,
|
||||
val cameraFpsInput: String = AppDefaults.DefaultCameraFpsInput,
|
||||
val cameraHighSpeed: Boolean = AppDefaults.DefaultCameraHighSpeed,
|
||||
val noAudioPlayback: Boolean = AppDefaults.DefaultNoAudioPlayback,
|
||||
val noVideo: Boolean = AppDefaults.DefaultNoVideo,
|
||||
val requireAudio: Boolean = AppDefaults.DefaultRequireAudio,
|
||||
val turnScreenOff: Boolean = AppDefaults.DefaultTurnScreenOff,
|
||||
val newDisplayWidth: String = AppDefaults.DefaultNewDisplayWidth,
|
||||
val newDisplayHeight: String = AppDefaults.DefaultNewDisplayHeight,
|
||||
val newDisplayDpi: String = AppDefaults.DefaultNewDisplayDpi,
|
||||
val displayIdInput: String = AppDefaults.DefaultDisplayIdInput,
|
||||
val cropWidth: String = AppDefaults.DefaultCropWidth,
|
||||
val cropHeight: String = AppDefaults.DefaultCropHeight,
|
||||
val cropX: String = AppDefaults.DefaultCropX,
|
||||
val cropY: String = AppDefaults.DefaultCropY,
|
||||
val quickConnectInput: String = AppDefaults.QUICK_CONNECT_INPUT,
|
||||
val pairHost: String = AppDefaults.PAIR_HOST,
|
||||
val pairPort: String = AppDefaults.PAIR_PORT,
|
||||
val pairCode: String = AppDefaults.PAIR_CODE,
|
||||
val audioBitRateKbps: Int = AppDefaults.AUDIO_BIT_RATE_KBPS,
|
||||
val audioBitRateInput: String = AppDefaults.AUDIO_BIT_RATE_INPUT,
|
||||
val videoBitRateMbps: Float = AppDefaults.VIDEO_BIT_RATE_MBPS,
|
||||
val videoBitRateInput: String = AppDefaults.VIDEO_BIT_RATE_INPUT,
|
||||
val turnScreenOff: Boolean = AppDefaults.TURN_SCREEN_OFF,
|
||||
val noControl: Boolean = AppDefaults.NO_CONTROL,
|
||||
val noVideo: Boolean = AppDefaults.NO_VIDEO,
|
||||
val videoSourcePreset: String = AppDefaults.VIDEO_SOURCE_PRESET,
|
||||
val displayIdInput: String = AppDefaults.DISPLAY_ID,
|
||||
val cameraIdInput: String = AppDefaults.CAMERA_ID,
|
||||
val cameraFacingPreset: String = AppDefaults.CAMERA_FACING_PRESET,
|
||||
val cameraSizePreset: String = AppDefaults.CAMERA_SIZE_PRESET,
|
||||
val cameraSizeCustom: String = AppDefaults.CAMERA_SIZE_CUSTOM,
|
||||
val cameraAr: String = AppDefaults.CAMERA_AR,
|
||||
val cameraFps: String = AppDefaults.CAMERA_FPS,
|
||||
val cameraHighSpeed: Boolean = AppDefaults.CAMERA_HIGH_SPEED,
|
||||
val audioSourcePreset: String = AppDefaults.AUDIO_SOURCE_PRESET,
|
||||
val audioSourceCustom: String = AppDefaults.AUDIO_SOURCE_CUSTOM,
|
||||
val audioDup: Boolean = AppDefaults.AUDIO_DUP,
|
||||
val noAudioPlayback: Boolean = AppDefaults.NO_AUDIO_PLAYBACK,
|
||||
val requireAudio: Boolean = AppDefaults.REQUIRE_AUDIO,
|
||||
val maxSizeInput: String = AppDefaults.MAX_SIZE_INPUT,
|
||||
val maxFpsInput: String = AppDefaults.MAX_FPS_INPUT,
|
||||
val videoEncoder: String = AppDefaults.VIDEO_ENCODER,
|
||||
val videoCodecOptions: String = AppDefaults.VIDEO_CODEC_OPTION,
|
||||
val audioEncoder: String = AppDefaults.AUDIO_ENCODER,
|
||||
val audioCodecOptions: String = AppDefaults.AUDIO_CODEC_OPTION,
|
||||
val newDisplayWidth: String = AppDefaults.NEW_DISPLAY_WIDTH,
|
||||
val newDisplayHeight: String = AppDefaults.NEW_DISPLAY_HEIGHT,
|
||||
val newDisplayDpi: String = AppDefaults.NEW_DISPLAY_DPI,
|
||||
val cropWidth: String = AppDefaults.CROP_WIDTH,
|
||||
val cropHeight: String = AppDefaults.CROP_HEIGHT,
|
||||
val cropX: String = AppDefaults.CROP_X,
|
||||
val cropY: String = AppDefaults.CROP_Y,
|
||||
)
|
||||
|
||||
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(
|
||||
themeBaseIndex = prefs.getInt(AppPreferenceKeys.ThemeBaseIndex, AppDefaults.DefaultThemeBaseIndex),
|
||||
monetEnabled = prefs.getBoolean(AppPreferenceKeys.MonetEnabled, AppDefaults.DefaultMonetEnabled),
|
||||
audioEnabled = prefs.getBoolean(
|
||||
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(
|
||||
AppPreferenceKeys.FullscreenDebugInfoEnabled,
|
||||
AppDefaults.DefaultFullscreenDebugInfoEnabled,
|
||||
AppPreferenceKeys.FULLSCREEN_DEBUG_INFO,
|
||||
AppDefaults.FULLSCREEN_DEBUG_INFO,
|
||||
),
|
||||
showFullscreenVirtualButtons = prefs.getBoolean(
|
||||
AppPreferenceKeys.ShowFullscreenVirtualButtons,
|
||||
AppDefaults.DefaultShowFullscreenVirtualButtons,
|
||||
AppPreferenceKeys.SHOW_FULLSCREEN_VIRTUAL_BUTTONS,
|
||||
AppDefaults.SHOW_FULLSCREEN_VIRTUAL_BUTTONS,
|
||||
),
|
||||
keepScreenOnWhenStreamingEnabled = prefs.getBoolean(
|
||||
AppPreferenceKeys.KEEP_SCREEN_ON_WHEN_STREAMING,
|
||||
AppDefaults.KEEP_SCREEN_ON_WHEN_STREAMING,
|
||||
),
|
||||
devicePreviewCardHeightDp = prefs.getInt(
|
||||
AppPreferenceKeys.DevicePreviewCardHeightDp,
|
||||
AppDefaults.DefaultDevicePreviewCardHeightDp,
|
||||
AppPreferenceKeys.DEVICE_PREVIEW_CARD_HEIGHT_DP,
|
||||
AppDefaults.DEVICE_PREVIEW_CARD_HEIGHT_DP,
|
||||
).coerceAtLeast(120),
|
||||
keepScreenOnWhenStreamingEnabled = prefs.getBoolean(
|
||||
AppPreferenceKeys.KeepScreenOnWhenStreamingEnabled,
|
||||
AppDefaults.DefaultKeepScreenOnWhenStreamingEnabled,
|
||||
),
|
||||
virtualButtonsOutside = prefs.getString(
|
||||
AppPreferenceKeys.VirtualButtonsOutside,
|
||||
AppDefaults.DefaultVirtualButtonsOutside,
|
||||
).orEmpty().ifBlank { AppDefaults.DefaultVirtualButtonsOutside },
|
||||
AppPreferenceKeys.VIRTUAL_BUTTONS_OUTSIDE,
|
||||
AppDefaults.VIRTUAL_BUTTONS_OUTSIDE,
|
||||
).orEmpty().ifBlank { AppDefaults.VIRTUAL_BUTTONS_OUTSIDE },
|
||||
virtualButtonsInMore = prefs.getString(
|
||||
AppPreferenceKeys.VirtualButtonsInMore,
|
||||
AppDefaults.DefaultVirtualButtonsInMore,
|
||||
).orEmpty().ifBlank { AppDefaults.DefaultVirtualButtonsInMore },
|
||||
customServerUri = prefs.getString(AppPreferenceKeys.CustomServerUri, null),
|
||||
AppPreferenceKeys.VIRTUAL_BUTTONS_IN_MORE,
|
||||
AppDefaults.VIRTUAL_BUTTONS_IN_MORE,
|
||||
).orEmpty().ifBlank { AppDefaults.VIRTUAL_BUTTONS_IN_MORE },
|
||||
customServerUri = prefs.getString(
|
||||
AppPreferenceKeys.CUSTOM_SERVER_URI,
|
||||
AppDefaults.CUSTOM_SERVER_URI
|
||||
).orEmpty().ifBlank { null },
|
||||
serverRemotePath = prefs.getString(
|
||||
AppPreferenceKeys.ServerRemotePath,
|
||||
AppDefaults.DefaultServerRemotePathInput,
|
||||
AppPreferenceKeys.SERVER_REMOTE_PATH,
|
||||
AppDefaults.SERVER_REMOTE_PATH_INPUT,
|
||||
).orEmpty(),
|
||||
adbKeyName = prefs.getString(
|
||||
AppPreferenceKeys.ADB_KEY_NAME,
|
||||
AppDefaults.ADB_KEY_NAME_INPUT,
|
||||
).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) {
|
||||
context.getSharedPreferences(AppPreferenceKeys.PrefsName, Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putInt(AppPreferenceKeys.ThemeBaseIndex, settings.themeBaseIndex)
|
||||
.putBoolean(AppPreferenceKeys.MonetEnabled, settings.monetEnabled)
|
||||
.putBoolean(AppPreferenceKeys.FullscreenDebugInfoEnabled, settings.fullscreenDebugInfoEnabled)
|
||||
.putBoolean(AppPreferenceKeys.ShowFullscreenVirtualButtons, settings.showFullscreenVirtualButtons)
|
||||
.putInt(AppPreferenceKeys.DevicePreviewCardHeightDp, settings.devicePreviewCardHeightDp.coerceAtLeast(120))
|
||||
.putBoolean(AppPreferenceKeys.KeepScreenOnWhenStreamingEnabled, settings.keepScreenOnWhenStreamingEnabled)
|
||||
.putString(AppPreferenceKeys.VirtualButtonsOutside, settings.virtualButtonsOutside)
|
||||
.putString(AppPreferenceKeys.VirtualButtonsInMore, settings.virtualButtonsInMore)
|
||||
.putString(AppPreferenceKeys.CustomServerUri, settings.customServerUri)
|
||||
.putString(AppPreferenceKeys.ServerRemotePath, settings.serverRemotePath)
|
||||
.putString(AppPreferenceKeys.VideoCodec, settings.videoCodec)
|
||||
.putBoolean(AppPreferenceKeys.AudioEnabled, settings.audioEnabled)
|
||||
.putString(AppPreferenceKeys.AudioCodec, settings.audioCodec)
|
||||
.putString(AppPreferenceKeys.AdbKeyName, settings.adbKeyName)
|
||||
}
|
||||
context.getSharedPreferences(
|
||||
AppPreferenceKeys.PREFS_NAME,
|
||||
Context.MODE_PRIVATE,
|
||||
).edit {
|
||||
putBoolean(
|
||||
AppPreferenceKeys.AUDIO_ENABLED,
|
||||
settings.audioEnabled,
|
||||
)
|
||||
.putString(
|
||||
AppPreferenceKeys.AUDIO_CODEC,
|
||||
settings.audioCodec,
|
||||
)
|
||||
.putString(
|
||||
AppPreferenceKeys.VIDEO_CODEC,
|
||||
settings.videoCodec,
|
||||
)
|
||||
.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 {
|
||||
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(
|
||||
pairHost = AppDefaults.DefaultPairHost,
|
||||
pairPort = AppDefaults.DefaultPairPort,
|
||||
pairCode = AppDefaults.DefaultPairCode,
|
||||
quickConnectInput = prefs.getString(AppPreferenceKeys.QuickConnectInput, AppDefaults.DefaultQuickConnectInput).orEmpty(),
|
||||
bitRateMbps = prefs.getFloat(AppPreferenceKeys.BitRateMbps, AppDefaults.DefaultBitRateMbps),
|
||||
bitRateInput = prefs.getString(AppPreferenceKeys.BitRateInput, AppDefaults.DefaultBitRateInput)
|
||||
.orEmpty()
|
||||
.ifBlank { AppDefaults.DefaultBitRateInput },
|
||||
maxSizeInput = prefs.getString(AppPreferenceKeys.MaxSizeInput, AppDefaults.DefaultMaxSizeInput).orEmpty(),
|
||||
audioBitRateKbps = prefs.getInt(AppPreferenceKeys.AudioBitRateKbps, AppDefaults.DefaultAudioBitRateKbps),
|
||||
maxFpsInput = prefs.getString(AppPreferenceKeys.MaxFpsInput, AppDefaults.DefaultMaxFpsInput).orEmpty(),
|
||||
noControl = prefs.getBoolean(AppPreferenceKeys.NoControl, AppDefaults.DefaultNoControl),
|
||||
videoEncoder = prefs.getString(AppPreferenceKeys.VideoEncoder, AppDefaults.DefaultVideoEncoder).orEmpty(),
|
||||
videoCodecOptions = prefs.getString(AppPreferenceKeys.VideoCodecOptions, AppDefaults.DefaultVideoCodecOptions).orEmpty(),
|
||||
audioEncoder = prefs.getString(AppPreferenceKeys.AudioEncoder, AppDefaults.DefaultAudioEncoder).orEmpty(),
|
||||
audioCodecOptions = prefs.getString(AppPreferenceKeys.AudioCodecOptions, AppDefaults.DefaultAudioCodecOptions).orEmpty(),
|
||||
audioDup = prefs.getBoolean(AppPreferenceKeys.AudioDup, AppDefaults.DefaultAudioDup),
|
||||
audioSourcePreset = prefs.getString(AppPreferenceKeys.AudioSourcePreset, AppDefaults.DefaultAudioSourcePreset)
|
||||
.orEmpty()
|
||||
.ifBlank { AppDefaults.DefaultAudioSourcePreset },
|
||||
audioSourceCustom = prefs.getString(AppPreferenceKeys.AudioSourceCustom, AppDefaults.DefaultAudioSourceCustom).orEmpty(),
|
||||
videoSourcePreset = prefs.getString(AppPreferenceKeys.VideoSourcePreset, AppDefaults.DefaultVideoSourcePreset)
|
||||
.orEmpty()
|
||||
.ifBlank { AppDefaults.DefaultVideoSourcePreset },
|
||||
cameraIdInput = prefs.getString(AppPreferenceKeys.CameraIdInput, AppDefaults.DefaultCameraIdInput).orEmpty(),
|
||||
cameraFacingPreset = prefs.getString(AppPreferenceKeys.CameraFacingPreset, AppDefaults.DefaultCameraFacingPreset).orEmpty(),
|
||||
cameraSizePreset = prefs.getString(AppPreferenceKeys.CameraSizePreset, AppDefaults.DefaultCameraSizePreset).orEmpty(),
|
||||
cameraSizeCustom = prefs.getString(AppPreferenceKeys.CameraSizeCustom, AppDefaults.DefaultCameraSizeCustom).orEmpty(),
|
||||
cameraArInput = prefs.getString(AppPreferenceKeys.CameraArInput, AppDefaults.DefaultCameraArInput).orEmpty(),
|
||||
cameraFpsInput = prefs.getString(AppPreferenceKeys.CameraFpsInput, AppDefaults.DefaultCameraFpsInput).orEmpty(),
|
||||
cameraHighSpeed = prefs.getBoolean(AppPreferenceKeys.CameraHighSpeed, AppDefaults.DefaultCameraHighSpeed),
|
||||
noAudioPlayback = prefs.getBoolean(AppPreferenceKeys.NoAudioPlayback, AppDefaults.DefaultNoAudioPlayback),
|
||||
noVideo = prefs.getBoolean(AppPreferenceKeys.NoVideo, AppDefaults.DefaultNoVideo),
|
||||
requireAudio = prefs.getBoolean(AppPreferenceKeys.RequireAudio, AppDefaults.DefaultRequireAudio),
|
||||
turnScreenOff = prefs.getBoolean(AppPreferenceKeys.TurnScreenOff, AppDefaults.DefaultTurnScreenOff),
|
||||
newDisplayWidth = prefs.getString(AppPreferenceKeys.NewDisplayWidth, AppDefaults.DefaultNewDisplayWidth).orEmpty(),
|
||||
newDisplayHeight = prefs.getString(AppPreferenceKeys.NewDisplayHeight, AppDefaults.DefaultNewDisplayHeight).orEmpty(),
|
||||
newDisplayDpi = prefs.getString(AppPreferenceKeys.NewDisplayDpi, AppDefaults.DefaultNewDisplayDpi).orEmpty(),
|
||||
displayIdInput = prefs.getString(AppPreferenceKeys.DisplayIdInput, AppDefaults.DefaultDisplayIdInput).orEmpty(),
|
||||
cropWidth = prefs.getString(AppPreferenceKeys.CropWidth, AppDefaults.DefaultCropWidth).orEmpty(),
|
||||
cropHeight = prefs.getString(AppPreferenceKeys.CropHeight, AppDefaults.DefaultCropHeight).orEmpty(),
|
||||
cropX = prefs.getString(AppPreferenceKeys.CropX, AppDefaults.DefaultCropX).orEmpty(),
|
||||
cropY = prefs.getString(AppPreferenceKeys.CropY, AppDefaults.DefaultCropY).orEmpty(),
|
||||
quickConnectInput = prefs.getString(
|
||||
AppPreferenceKeys.QUICK_CONNECT_INPUT,
|
||||
AppDefaults.QUICK_CONNECT_INPUT,
|
||||
).orEmpty(),
|
||||
pairHost = AppDefaults.PAIR_HOST,
|
||||
pairPort = AppDefaults.PAIR_PORT,
|
||||
pairCode = AppDefaults.PAIR_CODE,
|
||||
audioBitRateKbps = audioBitRateKbps,
|
||||
audioBitRateInput = prefs.getString(
|
||||
AppPreferenceKeys.AUDIO_BIT_RATE_INPUT,
|
||||
AppDefaults.AUDIO_BIT_RATE_INPUT,
|
||||
).orEmpty().ifBlank { audioBitRateKbps.toString() },
|
||||
videoBitRateMbps = prefs.getFloat(
|
||||
AppPreferenceKeys.VIDEO_BIT_RATE_MBPS,
|
||||
AppDefaults.VIDEO_BIT_RATE_MBPS,
|
||||
),
|
||||
videoBitRateInput = prefs.getString(
|
||||
AppPreferenceKeys.VIDEO_BIT_RATE_INPUT,
|
||||
AppDefaults.VIDEO_BIT_RATE_INPUT
|
||||
).orEmpty().ifBlank { AppDefaults.VIDEO_BIT_RATE_INPUT },
|
||||
turnScreenOff = prefs.getBoolean(
|
||||
AppPreferenceKeys.TURN_SCREEN_OFF,
|
||||
AppDefaults.TURN_SCREEN_OFF,
|
||||
),
|
||||
noControl = prefs.getBoolean(
|
||||
AppPreferenceKeys.NO_CONTROL,
|
||||
AppDefaults.NO_CONTROL,
|
||||
),
|
||||
noVideo = prefs.getBoolean(
|
||||
AppPreferenceKeys.NO_VIDEO,
|
||||
AppDefaults.NO_VIDEO,
|
||||
),
|
||||
videoSourcePreset = prefs.getString(
|
||||
AppPreferenceKeys.VIDEO_SOURCE_PRESET,
|
||||
AppDefaults.VIDEO_SOURCE_PRESET,
|
||||
).orEmpty().ifBlank { AppDefaults.VIDEO_SOURCE_PRESET },
|
||||
displayIdInput = prefs.getString(
|
||||
AppPreferenceKeys.DISPLAY_ID,
|
||||
AppDefaults.DISPLAY_ID,
|
||||
)
|
||||
.orEmpty(),
|
||||
cameraIdInput = prefs.getString(
|
||||
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) {
|
||||
context.getSharedPreferences(AppPreferenceKeys.PrefsName, Context.MODE_PRIVATE)
|
||||
context.getSharedPreferences(AppPreferenceKeys.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
remove(AppPreferenceKeys.PairHost)
|
||||
.remove(AppPreferenceKeys.PairPort)
|
||||
.remove(AppPreferenceKeys.PairCode)
|
||||
.putString(AppPreferenceKeys.QuickConnectInput, settings.quickConnectInput)
|
||||
.putFloat(AppPreferenceKeys.BitRateMbps, settings.bitRateMbps)
|
||||
.putString(AppPreferenceKeys.BitRateInput, settings.bitRateInput)
|
||||
.putInt(AppPreferenceKeys.AudioBitRateKbps, settings.audioBitRateKbps)
|
||||
.putString(AppPreferenceKeys.MaxSizeInput, settings.maxSizeInput)
|
||||
.putString(AppPreferenceKeys.MaxFpsInput, settings.maxFpsInput)
|
||||
.putBoolean(AppPreferenceKeys.NoControl, settings.noControl)
|
||||
.putString(AppPreferenceKeys.VideoEncoder, settings.videoEncoder)
|
||||
.putString(AppPreferenceKeys.VideoCodecOptions, settings.videoCodecOptions)
|
||||
.putString(AppPreferenceKeys.AudioEncoder, settings.audioEncoder)
|
||||
.putString(AppPreferenceKeys.AudioCodecOptions, settings.audioCodecOptions)
|
||||
.putBoolean(AppPreferenceKeys.AudioDup, settings.audioDup)
|
||||
.putString(AppPreferenceKeys.AudioSourcePreset, settings.audioSourcePreset)
|
||||
.putString(AppPreferenceKeys.AudioSourceCustom, settings.audioSourceCustom)
|
||||
.putString(AppPreferenceKeys.VideoSourcePreset, settings.videoSourcePreset)
|
||||
.putString(AppPreferenceKeys.CameraIdInput, settings.cameraIdInput)
|
||||
.putString(AppPreferenceKeys.CameraFacingPreset, settings.cameraFacingPreset)
|
||||
.putString(AppPreferenceKeys.CameraSizePreset, settings.cameraSizePreset)
|
||||
.putString(AppPreferenceKeys.CameraSizeCustom, settings.cameraSizeCustom)
|
||||
.putString(AppPreferenceKeys.CameraArInput, settings.cameraArInput)
|
||||
.putString(AppPreferenceKeys.CameraFpsInput, settings.cameraFpsInput)
|
||||
.putBoolean(AppPreferenceKeys.CameraHighSpeed, settings.cameraHighSpeed)
|
||||
.putBoolean(AppPreferenceKeys.NoAudioPlayback, settings.noAudioPlayback)
|
||||
.putBoolean(AppPreferenceKeys.NoVideo, settings.noVideo)
|
||||
.putBoolean(AppPreferenceKeys.RequireAudio, settings.requireAudio)
|
||||
.putBoolean(AppPreferenceKeys.TurnScreenOff, settings.turnScreenOff)
|
||||
.putString(AppPreferenceKeys.NewDisplayWidth, settings.newDisplayWidth)
|
||||
.putString(AppPreferenceKeys.NewDisplayHeight, settings.newDisplayHeight)
|
||||
.putString(AppPreferenceKeys.NewDisplayDpi, settings.newDisplayDpi)
|
||||
.putString(AppPreferenceKeys.DisplayIdInput, settings.displayIdInput)
|
||||
.putString(AppPreferenceKeys.CropWidth, settings.cropWidth)
|
||||
.putString(AppPreferenceKeys.CropHeight, settings.cropHeight)
|
||||
.putString(AppPreferenceKeys.CropX, settings.cropX)
|
||||
.putString(AppPreferenceKeys.CropY, settings.cropY)
|
||||
remove(AppPreferenceKeys.PAIR_HOST)
|
||||
.remove(AppPreferenceKeys.PAIR_PORT)
|
||||
.remove(AppPreferenceKeys.PAIR_CODE)
|
||||
.putString(
|
||||
AppPreferenceKeys.QUICK_CONNECT_INPUT,
|
||||
settings.quickConnectInput,
|
||||
)
|
||||
.putInt(
|
||||
AppPreferenceKeys.AUDIO_BIT_RATE_KBPS,
|
||||
settings.audioBitRateKbps,
|
||||
)
|
||||
.putString(
|
||||
AppPreferenceKeys.AUDIO_BIT_RATE_INPUT,
|
||||
settings.audioBitRateInput,
|
||||
)
|
||||
.putFloat(
|
||||
AppPreferenceKeys.VIDEO_BIT_RATE_MBPS,
|
||||
settings.videoBitRateMbps,
|
||||
)
|
||||
.putString(
|
||||
AppPreferenceKeys.VIDEO_BIT_RATE_INPUT,
|
||||
settings.videoBitRateInput,
|
||||
)
|
||||
.putBoolean(
|
||||
AppPreferenceKeys.TURN_SCREEN_OFF,
|
||||
settings.turnScreenOff,
|
||||
)
|
||||
.putBoolean(
|
||||
AppPreferenceKeys.NO_CONTROL,
|
||||
settings.noControl,
|
||||
)
|
||||
.putBoolean(
|
||||
AppPreferenceKeys.NO_VIDEO,
|
||||
settings.noVideo,
|
||||
)
|
||||
.putString(
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,13 @@ internal data class ConnectedDeviceInfo(
|
||||
val sdkInt: Int,
|
||||
)
|
||||
|
||||
internal fun fetchConnectedDeviceInfo(nativeCore: NativeCoreFacade, host: String, port: Int): ConnectedDeviceInfo {
|
||||
fun prop(name: String): String = runCatching { nativeCore.adbShell("getprop $name").trim() }.getOrDefault("")
|
||||
internal fun fetchConnectedDeviceInfo(
|
||||
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 serial = prop("ro.serialno")
|
||||
|
||||
@@ -8,8 +8,8 @@ import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget
|
||||
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
|
||||
|
||||
internal fun loadQuickDevices(context: Context): List<DeviceShortcut> {
|
||||
val raw = context.getSharedPreferences(AppPreferenceKeys.PrefsName, Context.MODE_PRIVATE)
|
||||
.getString(AppPreferenceKeys.QuickDevices, "")
|
||||
val raw = context.getSharedPreferences(AppPreferenceKeys.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.getString(AppPreferenceKeys.QUICK_DEVICES, "")
|
||||
.orEmpty()
|
||||
|
||||
if (raw.isBlank()) return emptyList()
|
||||
@@ -21,7 +21,7 @@ internal fun loadQuickDevices(context: Context): List<DeviceShortcut> {
|
||||
3 -> {
|
||||
val name = parts[0].trim()
|
||||
val host = parts[1].trim()
|
||||
val port = parts[2].trim().toIntOrNull() ?: AppDefaults.DefaultAdbPort
|
||||
val port = parts[2].trim().toIntOrNull() ?: AppDefaults.ADB_PORT
|
||||
if (host.isNotBlank()) {
|
||||
result.add(
|
||||
DeviceShortcut(
|
||||
@@ -39,7 +39,8 @@ internal fun loadQuickDevices(context: Context): List<DeviceShortcut> {
|
||||
// Backward compatibility with old format: name|host:port
|
||||
val name = parts[0].trim()
|
||||
val host = parts[1].substringBefore(":").trim()
|
||||
val port = parts[1].substringAfter(":", AppDefaults.DefaultAdbPort.toString()).trim().toIntOrNull() ?: AppDefaults.DefaultAdbPort
|
||||
val port = parts[1].substringAfter(":", AppDefaults.ADB_PORT.toString()).trim()
|
||||
.toIntOrNull() ?: AppDefaults.ADB_PORT
|
||||
if (host.isNotBlank()) {
|
||||
result.add(
|
||||
DeviceShortcut(
|
||||
@@ -59,9 +60,9 @@ internal fun loadQuickDevices(context: Context): List<DeviceShortcut> {
|
||||
|
||||
internal fun saveQuickDevices(context: Context, quickDevices: List<DeviceShortcut>) {
|
||||
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 {
|
||||
putString(AppPreferenceKeys.QuickDevices, raw)
|
||||
putString(AppPreferenceKeys.QUICK_DEVICES, raw)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +71,8 @@ internal fun parseQuickTarget(raw: String): ConnectionTarget? {
|
||||
if (value.isEmpty()) return null
|
||||
val host = value.substringBefore(':').trim()
|
||||
if (host.isEmpty()) return null
|
||||
val port = value.substringAfter(':', AppDefaults.DefaultAdbPort.toString()).trim().toIntOrNull() ?: AppDefaults.DefaultAdbPort
|
||||
val port = value.substringAfter(':', AppDefaults.ADB_PORT.toString()).trim().toIntOrNull()
|
||||
?: AppDefaults.ADB_PORT
|
||||
return ConnectionTarget(host, port)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.KeyboardOptions
|
||||
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.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.LinkOff
|
||||
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.style.TextOverflow
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
@@ -96,7 +91,7 @@ import kotlin.math.roundToInt
|
||||
private val VIDEO_CODEC_OPTIONS = listOf(
|
||||
"h264" to "H.264",
|
||||
"h265" to "H.265",
|
||||
"av1" to "AV1",
|
||||
"av1" to "AV1",
|
||||
)
|
||||
|
||||
private val AUDIO_CODEC_OPTIONS = listOf(
|
||||
@@ -266,9 +261,13 @@ internal fun PreviewCard(
|
||||
}
|
||||
val containerAspect = maxWidth.value / maxHeight.value
|
||||
val fittedModifier = if (sessionAspect > containerAspect) {
|
||||
Modifier.fillMaxWidth().aspectRatio(sessionAspect)
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(sessionAspect)
|
||||
} else {
|
||||
Modifier.fillMaxHeight().aspectRatio(sessionAspect)
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.aspectRatio(sessionAspect)
|
||||
}
|
||||
|
||||
Box(
|
||||
@@ -358,10 +357,13 @@ internal fun ConfigPanel(
|
||||
sessionStarted: Boolean,
|
||||
) {
|
||||
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 audioCodecIndex = AUDIO_CODEC_OPTIONS.indexOfFirst { it.first == audioCodec }.coerceAtLeast(0)
|
||||
val audioBitRatePresetIndex = presetIndexFromInput(audioBitRateKbps.toString(), ScrcpyPresets.AudioBitRate)
|
||||
val audioCodecIndex =
|
||||
AUDIO_CODEC_OPTIONS.indexOfFirst { it.first == audioCodec }.coerceAtLeast(0)
|
||||
val audioBitRatePresetIndex =
|
||||
presetIndexFromInput(audioBitRateKbps.toString(), ScrcpyPresets.AudioBitRate)
|
||||
|
||||
SectionSmallTitle(text = "Scrcpy")
|
||||
Card {
|
||||
@@ -433,6 +435,7 @@ internal fun ConfigPanel(
|
||||
dotUsed = true
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@@ -506,7 +509,9 @@ private fun PairingDialog(
|
||||
onValueChange = { host = it },
|
||||
label = "IP 地址",
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = UiSpacing.CardContent),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = UiSpacing.CardContent),
|
||||
)
|
||||
TextField(
|
||||
value = port,
|
||||
@@ -514,14 +519,18 @@ private fun PairingDialog(
|
||||
label = "端口",
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = UiSpacing.CardContent),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = UiSpacing.CardContent),
|
||||
)
|
||||
TextField(
|
||||
value = code,
|
||||
onValueChange = { code = it },
|
||||
label = "WLAN 配对码",
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = UiSpacing.Large),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = UiSpacing.Large),
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(UiSpacing.PopupHorizontal)) {
|
||||
TextButton(
|
||||
@@ -599,8 +608,10 @@ fun FullscreenControlScreen(
|
||||
}
|
||||
|
||||
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 y = ((rawY / touchAreaSize.height) * session.height).roundToInt().coerceIn(0, (session.height - 1).coerceAtLeast(0))
|
||||
val x = ((rawX / touchAreaSize.width) * session.width).roundToInt()
|
||||
.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
|
||||
}
|
||||
|
||||
@@ -612,7 +623,14 @@ fun FullscreenControlScreen(
|
||||
val py = event.getY(i)
|
||||
activePointerPositions[pointerId] = Offset(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
|
||||
activePointerPositions[pointerId] = Offset(event.x, event.y)
|
||||
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 -> {
|
||||
@@ -638,7 +663,14 @@ fun FullscreenControlScreen(
|
||||
activePointerIds += pointerId
|
||||
activePointerPositions[pointerId] = Offset(px, py)
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -650,7 +682,14 @@ fun FullscreenControlScreen(
|
||||
val py = event.getY(i)
|
||||
activePointerPositions[pointerId] = Offset(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 fittedModifier = if (sessionAspect > containerAspect) {
|
||||
Modifier.fillMaxWidth().aspectRatio(sessionAspect)
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(sessionAspect)
|
||||
} else {
|
||||
Modifier.fillMaxHeight().aspectRatio(sessionAspect)
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.aspectRatio(sessionAspect)
|
||||
}
|
||||
|
||||
Box(
|
||||
@@ -780,13 +823,21 @@ private fun ScrcpyVideoSurface(
|
||||
factory = { context ->
|
||||
TextureView(context).apply {
|
||||
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
|
||||
@SuppressLint("Recycle")
|
||||
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 {
|
||||
val released = currentSurface
|
||||
@@ -820,7 +871,7 @@ internal fun DeviceTile(
|
||||
Card(
|
||||
colors = CardDefaults.defaultColors(
|
||||
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,
|
||||
onClick = haptics.press,
|
||||
@@ -1016,7 +1067,9 @@ internal fun DeviceEditorScreen(
|
||||
.padding(top = UiSpacing.CardContent),
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(UiSpacing.CardContent),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(UiSpacing.CardContent),
|
||||
horizontalArrangement = Arrangement.spacedBy(UiSpacing.Medium),
|
||||
) {
|
||||
TextButton(
|
||||
@@ -1032,7 +1085,7 @@ internal fun DeviceEditorScreen(
|
||||
TextButton(
|
||||
text = "保存",
|
||||
onClick = {
|
||||
val p = port.toIntOrNull() ?: AppDefaults.DefaultAdbPort
|
||||
val p = port.toIntOrNull() ?: AppDefaults.ADB_PORT
|
||||
val h = host.trim()
|
||||
if (h.isNotBlank()) {
|
||||
onSave(
|
||||
|
||||
@@ -110,7 +110,10 @@ fun SortableCardList(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = UiSpacing.CardContent, vertical = UiSpacing.FieldLabelBottom),
|
||||
.padding(
|
||||
horizontal = UiSpacing.CardContent,
|
||||
vertical = UiSpacing.FieldLabelBottom
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
|
||||
@@ -69,12 +69,16 @@ internal fun StatusCardLayout(
|
||||
) {
|
||||
val haptics = rememberAppHaptics()
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min),
|
||||
horizontalArrangement = Arrangement.spacedBy(UiSpacing.PageItem),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.weight(1f).fillMaxHeight(),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight(),
|
||||
colors = defaultColors(color = spec.big.containerColor),
|
||||
pressFeedbackType = PressFeedbackType.Tilt,
|
||||
onClick = haptics.press,
|
||||
@@ -125,15 +129,21 @@ internal fun StatusCardLayout(
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
|
||||
Column(modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()) {
|
||||
StatusMetricCard(
|
||||
spec = spec.firstSmall,
|
||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
)
|
||||
Spacer(Modifier.height(UiSpacing.PageItem))
|
||||
StatusMetricCard(
|
||||
spec = spec.secondSmall,
|
||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,6 @@ import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material.icons.filled.PhotoCamera
|
||||
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.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -50,36 +47,76 @@ enum class VirtualButtonAction(
|
||||
val icon: ImageVector,
|
||||
val keycode: Int?,
|
||||
) {
|
||||
MORE("more", "更多", Icons.Default.MoreVert, null),
|
||||
HOME("home", "主页", Icons.Default.Home, 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),
|
||||
MORE(
|
||||
"more",
|
||||
"更多",
|
||||
Icons.Default.MoreVert,
|
||||
null
|
||||
),
|
||||
HOME(
|
||||
"home",
|
||||
"主页",
|
||||
Icons.Default.Home,
|
||||
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 {
|
||||
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 }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user