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 {
|
ndk {
|
||||||
abiFilters.clear()
|
abiFilters.clear()
|
||||||
|
//noinspection ChromeOsAbiSupport
|
||||||
abiFilters += configuredAbiList
|
abiFilters += configuredAbiList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import java.io.File
|
|||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.ArrayDeque
|
import java.util.ArrayDeque
|
||||||
import java.util.concurrent.CopyOnWriteArraySet
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.CopyOnWriteArraySet
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
@@ -34,7 +34,8 @@ class NativeCoreFacade(private val appContext: Context) {
|
|||||||
private val bootstrapLock = Any()
|
private val bootstrapLock = Any()
|
||||||
private val bootstrapPackets = ArrayDeque<CachedPacket>()
|
private val bootstrapPackets = ArrayDeque<CachedPacket>()
|
||||||
private var packetCount: Long = 0
|
private var packetCount: Long = 0
|
||||||
@Volatile private var audioPlayer: ScrcpyAudioPlayer? = null
|
@Volatile
|
||||||
|
private var audioPlayer: ScrcpyAudioPlayer? = null
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var currentSessionInfo: ScrcpySessionInfo? = null
|
private var currentSessionInfo: ScrcpySessionInfo? = null
|
||||||
@@ -72,7 +73,10 @@ class NativeCoreFacade(private val appContext: Context) {
|
|||||||
val currentId = surfaceIdentityMap[tag]
|
val currentId = surfaceIdentityMap[tag]
|
||||||
val requestId = surface?.let { System.identityHashCode(it) }
|
val requestId = surface?.let { System.identityHashCode(it) }
|
||||||
if (requestId != null && currentId != null && requestId != currentId) {
|
if (requestId != null && currentId != null && requestId != currentId) {
|
||||||
Log.i(TAG, "unregisterVideoSurface(): skip stale request tag=$tag requestSurfaceId=$requestId currentSurfaceId=$currentId")
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
"unregisterVideoSurface(): skip stale request tag=$tag requestSurfaceId=$requestId currentSurfaceId=$currentId"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Log.i(TAG, "unregisterVideoSurface(): tag=$tag surfaceId=$requestId")
|
Log.i(TAG, "unregisterVideoSurface(): tag=$tag surfaceId=$requestId")
|
||||||
@@ -178,7 +182,12 @@ class NativeCoreFacade(private val appContext: Context) {
|
|||||||
audioPlayer?.release()
|
audioPlayer?.release()
|
||||||
audioPlayer = null
|
audioPlayer = null
|
||||||
if (info.audioCodecId != 0 && !request.noAudioPlayback) {
|
if (info.audioCodecId != 0 && !request.noAudioPlayback) {
|
||||||
Log.i(TAG, "scrcpyStart(): create audio player codecId=0x${info.audioCodecId.toUInt().toString(16)}")
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
"scrcpyStart(): create audio player codecId=0x${
|
||||||
|
info.audioCodecId.toUInt().toString(16)
|
||||||
|
}"
|
||||||
|
)
|
||||||
val player = ScrcpyAudioPlayer(info.audioCodecId)
|
val player = ScrcpyAudioPlayer(info.audioCodecId)
|
||||||
audioPlayer = player
|
audioPlayer = player
|
||||||
sessionManager.attachAudioConsumer { packet ->
|
sessionManager.attachAudioConsumer { packet ->
|
||||||
@@ -208,7 +217,11 @@ class NativeCoreFacade(private val appContext: Context) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun scrcpyListEncoders(customServerUri: String?, remotePath: String, serverVersion: String = "3.3.4"): ScrcpyEncoderLists {
|
fun scrcpyListEncoders(
|
||||||
|
customServerUri: String?,
|
||||||
|
remotePath: String,
|
||||||
|
serverVersion: String = "3.3.4"
|
||||||
|
): ScrcpyEncoderLists {
|
||||||
return ioCall {
|
return ioCall {
|
||||||
val serverJar = if (customServerUri.isNullOrBlank()) {
|
val serverJar = if (customServerUri.isNullOrBlank()) {
|
||||||
extractAssetToCache(DEFAULT_SERVER_ASSET)
|
extractAssetToCache(DEFAULT_SERVER_ASSET)
|
||||||
@@ -232,7 +245,11 @@ class NativeCoreFacade(private val appContext: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun scrcpyListCameraSizes(customServerUri: String?, remotePath: String, serverVersion: String = "3.3.4"): ScrcpyCameraSizeLists {
|
fun scrcpyListCameraSizes(
|
||||||
|
customServerUri: String?,
|
||||||
|
remotePath: String,
|
||||||
|
serverVersion: String = "3.3.4"
|
||||||
|
): ScrcpyCameraSizeLists {
|
||||||
return ioCall {
|
return ioCall {
|
||||||
val serverJar = if (customServerUri.isNullOrBlank()) {
|
val serverJar = if (customServerUri.isNullOrBlank()) {
|
||||||
extractAssetToCache(DEFAULT_SERVER_ASSET)
|
extractAssetToCache(DEFAULT_SERVER_ASSET)
|
||||||
@@ -307,7 +324,17 @@ class NativeCoreFacade(private val appContext: Context) {
|
|||||||
buttons: Int = 0,
|
buttons: Int = 0,
|
||||||
) {
|
) {
|
||||||
ioExecute {
|
ioExecute {
|
||||||
runCatching { sessionManager.injectScroll(x, y, screenWidth, screenHeight, hScroll, vScroll, buttons) }
|
runCatching {
|
||||||
|
sessionManager.injectScroll(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
screenWidth,
|
||||||
|
screenHeight,
|
||||||
|
hScroll,
|
||||||
|
vScroll,
|
||||||
|
buttons
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,10 +502,13 @@ class NativeCoreFacade(private val appContext: Context) {
|
|||||||
val mime = when (session.codec.lowercase()) {
|
val mime = when (session.codec.lowercase()) {
|
||||||
"h264" -> "video/avc"
|
"h264" -> "video/avc"
|
||||||
"h265" -> "video/hevc"
|
"h265" -> "video/hevc"
|
||||||
"av1" -> "video/av01"
|
"av1" -> "video/av01"
|
||||||
else -> "video/avc"
|
else -> "video/avc"
|
||||||
}
|
}
|
||||||
Log.i(TAG, "createOrReplaceDecoder(): tag=$tag codec=$mime size=${session.width}x${session.height}")
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
"createOrReplaceDecoder(): tag=$tag codec=$mime size=${session.width}x${session.height}"
|
||||||
|
)
|
||||||
val decoder = AnnexBDecoder(
|
val decoder = AnnexBDecoder(
|
||||||
width = session.width,
|
width = session.width,
|
||||||
height = session.height,
|
height = session.height,
|
||||||
@@ -489,7 +519,10 @@ class NativeCoreFacade(private val appContext: Context) {
|
|||||||
if (current == null || (current.width == width && current.height == height)) {
|
if (current == null || (current.width == width && current.height == height)) {
|
||||||
return@AnnexBDecoder
|
return@AnnexBDecoder
|
||||||
}
|
}
|
||||||
Log.i(TAG, "videoSizeChanged(): ${current.width}x${current.height} -> ${width}x${height}")
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
"videoSizeChanged(): ${current.width}x${current.height} -> ${width}x${height}"
|
||||||
|
)
|
||||||
currentSessionInfo = current.copy(width = width, height = height)
|
currentSessionInfo = current.copy(width = width, height = height)
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
videoSizeListeners.forEach { listener ->
|
videoSizeListeners.forEach { listener ->
|
||||||
@@ -542,14 +575,22 @@ class NativeCoreFacade(private val appContext: Context) {
|
|||||||
cacheBootstrapPacket(packet)
|
cacheBootstrapPacket(packet)
|
||||||
packetCount += 1
|
packetCount += 1
|
||||||
if (packetCount == 1L || packetCount % 120L == 0L) {
|
if (packetCount == 1L || packetCount % 120L == 0L) {
|
||||||
Log.i(TAG, "videoFeed(): packets=$packetCount key=${packet.isKeyFrame} cfg=${packet.isConfig} decoders=${decoderMap.size}")
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
"videoFeed(): packets=$packetCount key=${packet.isKeyFrame} cfg=${packet.isConfig} decoders=${decoderMap.size}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
decoderMap.forEach { (tag, decoder) ->
|
decoderMap.forEach { (tag, decoder) ->
|
||||||
if (!surfaceIdentityMap.containsKey(tag)) {
|
if (!surfaceIdentityMap.containsKey(tag)) {
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
runCatching {
|
runCatching {
|
||||||
decoder.feedAnnexB(packet.data, packet.ptsUs, packet.isKeyFrame, packet.isConfig)
|
decoder.feedAnnexB(
|
||||||
|
packet.data,
|
||||||
|
packet.ptsUs,
|
||||||
|
packet.isKeyFrame,
|
||||||
|
packet.isConfig
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -634,21 +675,3 @@ data class ScrcpySessionInfo(
|
|||||||
val codec: String,
|
val codec: String,
|
||||||
val controlEnabled: Boolean,
|
val controlEnabled: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
object AndroidKeycodes {
|
|
||||||
const val HOME = 3
|
|
||||||
const val BACK = 4
|
|
||||||
const val POWER = 26
|
|
||||||
const val APP_SWITCH = 187
|
|
||||||
const val VOLUME_UP = 24
|
|
||||||
const val VOLUME_DOWN = 25
|
|
||||||
}
|
|
||||||
|
|
||||||
object MotionActions {
|
|
||||||
const val DOWN = 0
|
|
||||||
const val UP = 1
|
|
||||||
const val MOVE = 2
|
|
||||||
const val CANCEL = 3
|
|
||||||
const val POINTER_DOWN = 5
|
|
||||||
const val POINTER_UP = 6
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,75 +1,79 @@
|
|||||||
package io.github.miuzarte.scrcpyforandroid.constants
|
package io.github.miuzarte.scrcpyforandroid.constants
|
||||||
|
|
||||||
object AppDefaults {
|
object AppDefaults {
|
||||||
const val MaxEventLogLines = 512
|
const val EVENT_LOG_LINES = 512
|
||||||
const val DefaultAdbPort = 5555
|
const val ADB_PORT = 5555
|
||||||
|
|
||||||
// Devices
|
// Devices
|
||||||
const val DefaultQuickConnectInput = ""
|
const val QUICK_CONNECT_INPUT = ""
|
||||||
|
|
||||||
const val DefaultPairHost = ""
|
const val PAIR_HOST = ""
|
||||||
const val DefaultServerRemotePathInput = ""
|
const val PAIR_PORT = ""
|
||||||
const val DefaultAdbKeyNameInput = ""
|
const val PAIR_CODE = ""
|
||||||
const val DefaultPairPort = ""
|
|
||||||
const val DefaultPairCode = ""
|
|
||||||
|
|
||||||
const val DefaultAudioEnabled = true
|
const val AUDIO_ENABLED = true
|
||||||
const val DefaultAudioCodec = "opus"
|
const val AUDIO_CODEC = "opus"
|
||||||
const val DefaultAudioBitRateKbps = 128
|
const val AUDIO_BIT_RATE_KBPS = 128
|
||||||
const val DefaultVideoCodec = "h264"
|
const val AUDIO_BIT_RATE_INPUT = "128"
|
||||||
const val DefaultBitRateMbps = 8f
|
const val VIDEO_CODEC = "h264"
|
||||||
const val DefaultBitRateInput = "8.0"
|
const val VIDEO_BIT_RATE_MBPS = 8f
|
||||||
|
const val VIDEO_BIT_RATE_INPUT = "8.0"
|
||||||
|
|
||||||
const val DefaultTurnScreenOff = false
|
const val TURN_SCREEN_OFF = false
|
||||||
const val DefaultNoControl = false
|
const val NO_CONTROL = false
|
||||||
const val DefaultNoVideo = false
|
const val NO_VIDEO = false
|
||||||
|
|
||||||
const val DefaultVideoSourcePreset = "display"
|
const val VIDEO_SOURCE_PRESET = "display"
|
||||||
const val DefaultDisplayIdInput = ""
|
const val DISPLAY_ID = ""
|
||||||
|
|
||||||
const val DefaultCameraIdInput = ""
|
const val CAMERA_ID = ""
|
||||||
const val DefaultCameraFacingPreset = ""
|
const val CAMERA_FACING_PRESET = ""
|
||||||
const val DefaultCameraSizePreset = ""
|
const val CAMERA_SIZE_PRESET = ""
|
||||||
const val DefaultCameraSizeCustom = ""
|
const val CAMERA_SIZE_CUSTOM = ""
|
||||||
const val DefaultCameraArInput = ""
|
const val CAMERA_AR = ""
|
||||||
const val DefaultCameraFpsInput = ""
|
const val CAMERA_FPS = ""
|
||||||
const val DefaultCameraHighSpeed = false
|
const val CAMERA_HIGH_SPEED = false
|
||||||
|
|
||||||
const val DefaultAudioSourcePreset = "output"
|
const val AUDIO_SOURCE_PRESET = "output"
|
||||||
const val DefaultAudioSourceCustom = ""
|
const val AUDIO_SOURCE_CUSTOM = ""
|
||||||
const val DefaultAudioDup = false
|
const val AUDIO_DUP = false
|
||||||
const val DefaultNoAudioPlayback = false
|
const val NO_AUDIO_PLAYBACK = false
|
||||||
const val DefaultRequireAudio = false
|
const val REQUIRE_AUDIO = false
|
||||||
|
|
||||||
const val DefaultMaxSizeInput = ""
|
const val MAX_SIZE_INPUT = ""
|
||||||
const val DefaultMaxFpsInput = ""
|
const val MAX_FPS_INPUT = ""
|
||||||
|
|
||||||
const val DefaultVideoEncoder = ""
|
const val VIDEO_ENCODER = ""
|
||||||
const val DefaultVideoCodecOptions = ""
|
const val VIDEO_CODEC_OPTION = ""
|
||||||
const val DefaultAudioEncoder = ""
|
const val AUDIO_ENCODER = ""
|
||||||
const val DefaultAudioCodecOptions = ""
|
const val AUDIO_CODEC_OPTION = ""
|
||||||
|
|
||||||
const val DefaultNewDisplayWidth = ""
|
const val NEW_DISPLAY_WIDTH = ""
|
||||||
const val DefaultNewDisplayHeight = ""
|
const val NEW_DISPLAY_HEIGHT = ""
|
||||||
const val DefaultNewDisplayDpi = ""
|
const val NEW_DISPLAY_DPI = ""
|
||||||
|
|
||||||
const val DefaultCropWidth = ""
|
const val CROP_WIDTH = ""
|
||||||
const val DefaultCropHeight = ""
|
const val CROP_HEIGHT = ""
|
||||||
const val DefaultCropX = ""
|
const val CROP_X = ""
|
||||||
const val DefaultCropY = ""
|
const val CROP_Y = ""
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
const val DefaultThemeBaseIndex = 0
|
const val THEME_BASE_INDEX = 0
|
||||||
const val DefaultMonetEnabled = false
|
const val MONET = false
|
||||||
|
|
||||||
const val DefaultFullscreenDebugInfoEnabled = false
|
const val FULLSCREEN_DEBUG_INFO = false
|
||||||
const val DefaultShowFullscreenVirtualButtons = true
|
const val SHOW_FULLSCREEN_VIRTUAL_BUTTONS = true
|
||||||
const val DefaultDevicePreviewCardHeightDp = 320
|
const val KEEP_SCREEN_ON_WHEN_STREAMING = false
|
||||||
const val DefaultKeepScreenOnWhenStreamingEnabled = false
|
const val DEVICE_PREVIEW_CARD_HEIGHT_DP = 320
|
||||||
const val DefaultVirtualButtonsOutside = "more,home,back"
|
const val VIRTUAL_BUTTONS_OUTSIDE = "more,home,back"
|
||||||
const val DefaultVirtualButtonsInMore = "app_switch,menu,notification,volume_up,volume_down,volume_mute,power,screenshot"
|
const val VIRTUAL_BUTTONS_IN_MORE =
|
||||||
|
"app_switch,menu,notification,volume_up,volume_down,volume_mute,power,screenshot"
|
||||||
|
|
||||||
const val DefaultServerRemotePath = "/data/local/tmp/scrcpy-server.jar"
|
const val CUSTOM_SERVER_URI = ""
|
||||||
|
|
||||||
const val DefaultAdbKeyName = "scrcpy"
|
const val SERVER_REMOTE_PATH = "/data/local/tmp/scrcpy-server.jar"
|
||||||
|
const val SERVER_REMOTE_PATH_INPUT = ""
|
||||||
|
|
||||||
|
const val ADB_KEY_NAME = "scrcpy"
|
||||||
|
const val ADB_KEY_NAME_INPUT = ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,78 @@
|
|||||||
package io.github.miuzarte.scrcpyforandroid.constants
|
package io.github.miuzarte.scrcpyforandroid.constants
|
||||||
|
|
||||||
object AppPreferenceKeys {
|
object AppPreferenceKeys {
|
||||||
const val PrefsName = "scrcpy_app_prefs"
|
const val PREFS_NAME = "scrcpy_app_prefs"
|
||||||
const val NativeAdbKeyPrefsName = "nativecore_adb_rsa"
|
const val NATIVE_ADB_KEY_PREFS_NAME = "nativecore_adb_rsa"
|
||||||
const val NativeAdbPrivateKey = "priv"
|
const val NATIVE_ADB_PRIVATE_KEY = "priv"
|
||||||
const val ThemeBaseIndex = "theme_base_index"
|
|
||||||
const val MonetEnabled = "monet_enabled"
|
// Devices
|
||||||
const val FullscreenDebugInfoEnabled = "fullscreen_debug_info_enabled"
|
const val QUICK_DEVICES = "quick_devices"
|
||||||
const val ShowFullscreenVirtualButtons = "show_fullscreen_virtual_buttons"
|
const val QUICK_CONNECT_INPUT = "quick_connect_input"
|
||||||
const val DevicePreviewCardHeightDp = "device_preview_card_height_dp"
|
|
||||||
const val KeepScreenOnWhenStreamingEnabled = "keep_screen_on_when_streaming_enabled"
|
const val PAIR_HOST = "pair_host"
|
||||||
const val VirtualButtonsOutside = "virtual_buttons_outside"
|
const val PAIR_PORT = "pair_port"
|
||||||
const val VirtualButtonsInMore = "virtual_buttons_in_more"
|
const val PAIR_CODE = "pair_code"
|
||||||
const val CustomServerUri = "custom_server_uri"
|
|
||||||
const val ServerRemotePath = "server_remote_path"
|
const val AUDIO_ENABLED = "audio_enabled"
|
||||||
const val VideoCodec = "video_codec"
|
const val AUDIO_CODEC = "audio_codec"
|
||||||
const val AudioEnabled = "audio_enabled"
|
const val AUDIO_BIT_RATE_INPUT = "audio_bit_rate_input"
|
||||||
const val AudioCodec = "audio_codec"
|
const val AUDIO_BIT_RATE_KBPS = "audio_bit_rate_kbps"
|
||||||
const val PairHost = "pair_host"
|
const val VIDEO_CODEC = "video_codec"
|
||||||
const val PairPort = "pair_port"
|
const val VIDEO_BIT_RATE_MBPS = "video_bit_rate_mbps"
|
||||||
const val PairCode = "pair_code"
|
const val VIDEO_BIT_RATE_INPUT = "video_bit_rate_input"
|
||||||
const val QuickConnectInput = "quick_connect_input"
|
|
||||||
const val BitRateMbps = "bit_rate_mbps"
|
const val TURN_SCREEN_OFF = "turn_screen_off"
|
||||||
const val BitRateInput = "bit_rate_input"
|
const val NO_CONTROL = "no_control"
|
||||||
const val AudioBitRateKbps = "audio_bit_rate_kbps"
|
const val NO_VIDEO = "no_video"
|
||||||
const val MaxSizeInput = "max_size_input"
|
|
||||||
const val MaxFpsInput = "max_fps_input"
|
const val VIDEO_SOURCE_PRESET = "video_source_preset"
|
||||||
const val NoControl = "no_control"
|
const val DISPLAY_ID = "display_id"
|
||||||
const val VideoEncoder = "video_encoder"
|
|
||||||
const val VideoCodecOptions = "video_codec_options"
|
const val CAMERA_ID = "camera_id"
|
||||||
const val AudioEncoder = "audio_encoder"
|
const val CAMERA_FACING_PRESET = "camera_facing_preset"
|
||||||
const val AudioCodecOptions = "audio_codec_options"
|
const val CAMERA_SIZE_PRESET = "camera_size_preset"
|
||||||
const val AudioDup = "audio_dup"
|
const val CAMERA_SIZE_CUSTOM = "camera_size_custom"
|
||||||
const val AudioSourcePreset = "audio_source_preset"
|
const val CAMERA_AR = "camera_ar"
|
||||||
const val AudioSourceCustom = "audio_source_custom"
|
const val CAMERA_FPS = "camera_fps"
|
||||||
const val VideoSourcePreset = "video_source_preset"
|
const val CAMERA_HIGH_SPEED = "camera_high_speed"
|
||||||
const val CameraIdInput = "camera_id_input"
|
|
||||||
const val CameraFacingPreset = "camera_facing_preset"
|
const val AUDIO_SOURCE_PRESET = "audio_source_preset"
|
||||||
const val CameraSizePreset = "camera_size_preset"
|
const val AUDIO_SOURCE_CUSTOM = "audio_source_custom"
|
||||||
const val CameraSizeCustom = "camera_size_custom"
|
const val AUDIO_DUP = "audio_dup"
|
||||||
const val CameraArInput = "camera_ar_input"
|
const val NO_AUDIO_PLAYBACK = "no_audio_playback"
|
||||||
const val CameraFpsInput = "camera_fps_input"
|
const val REQUIRE_AUDIO = "require_audio"
|
||||||
const val CameraHighSpeed = "camera_high_speed"
|
|
||||||
const val NoAudioPlayback = "no_audio_playback"
|
const val MAX_SIZE_INPUT = "max_size_input"
|
||||||
const val NoVideo = "no_video"
|
const val MAX_FPS_INPUT = "max_fps_input"
|
||||||
const val RequireAudio = "require_audio"
|
|
||||||
const val TurnScreenOff = "turn_screen_off"
|
const val VIDEO_ENCODER = "video_encoder"
|
||||||
const val NewDisplayWidth = "new_display_width"
|
const val VIDEO_CODEC_OPTION = "video_codec_options"
|
||||||
const val NewDisplayHeight = "new_display_height"
|
const val AUDIO_ENCODER = "audio_encoder"
|
||||||
const val NewDisplayDpi = "new_display_dpi"
|
const val AUDIO_CODEC_OPTION = "audio_codec_options"
|
||||||
const val DisplayIdInput = "display_id_input"
|
|
||||||
const val CropWidth = "crop_width"
|
const val NEW_DISPLAY_WIDTH = "new_display_width"
|
||||||
const val CropHeight = "crop_height"
|
const val NEW_DISPLAY_HEIGHT = "new_display_height"
|
||||||
const val CropX = "crop_x"
|
const val NEW_DISPLAY_DPI = "new_display_dpi"
|
||||||
const val CropY = "crop_y"
|
|
||||||
const val AdbKeyName = "adb_key_name"
|
const val CROP_WIDTH = "crop_width"
|
||||||
const val QuickDevices = "quick_devices"
|
const val CROP_HEIGHT = "crop_height"
|
||||||
|
const val CROP_X = "crop_x"
|
||||||
|
const val CROP_Y = "crop_y"
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
const val THEME_BASE_INDEX = "theme_base_index"
|
||||||
|
const val MONET = "monet"
|
||||||
|
|
||||||
|
const val FULLSCREEN_DEBUG_INFO = "fullscreen_debug_info"
|
||||||
|
const val SHOW_FULLSCREEN_VIRTUAL_BUTTONS = "show_fullscreen_virtual_buttons"
|
||||||
|
const val KEEP_SCREEN_ON_WHEN_STREAMING = "keep_screen_on_when_streaming"
|
||||||
|
const val DEVICE_PREVIEW_CARD_HEIGHT_DP = "device_preview_card_height_dp"
|
||||||
|
const val VIRTUAL_BUTTONS_OUTSIDE = "virtual_buttons_outside"
|
||||||
|
const val VIRTUAL_BUTTONS_IN_MORE = "virtual_buttons_in_more"
|
||||||
|
|
||||||
|
const val CUSTOM_SERVER_URI = "custom_server_uri"
|
||||||
|
|
||||||
|
const val SERVER_REMOTE_PATH = "server_remote_path"
|
||||||
|
|
||||||
|
const val ADB_KEY_NAME = "adb_key_name"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package io.github.miuzarte.scrcpyforandroid.constants
|
package io.github.miuzarte.scrcpyforandroid.constants
|
||||||
|
|
||||||
object UiMotion {
|
object UiMotion {
|
||||||
const val PageSwitchDampingRatio = 0.9f
|
const val PAGE_SWITCH_DAMPING_RATIO = 0.9f
|
||||||
const val PageSwitchStiffness = 700f
|
const val PAGE_SWITCH_STIFFNESS = 700f
|
||||||
}
|
}
|
||||||
@@ -24,6 +24,7 @@ class AnnexBDecoder(
|
|||||||
private var outCount = 0L
|
private var outCount = 0L
|
||||||
private var fpsWindowStartNs = System.nanoTime()
|
private var fpsWindowStartNs = System.nanoTime()
|
||||||
private var fpsWindowFrameCount = 0
|
private var fpsWindowFrameCount = 0
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var released = false
|
private var released = false
|
||||||
|
|
||||||
@@ -65,16 +66,26 @@ class AnnexBDecoder(
|
|||||||
codec.queueInputBuffer(inputIndex, 0, data.size, ptsUs, flags)
|
codec.queueInputBuffer(inputIndex, 0, data.size, ptsUs, flags)
|
||||||
inCount += 1
|
inCount += 1
|
||||||
if (inCount == 1L || inCount % 180L == 0L || isConfig) {
|
if (inCount == 1L || inCount % 180L == 0L || isConfig) {
|
||||||
Log.i(TAG, "feed(): mime=$decoderMime in=$inCount size=${data.size} key=$isKeyFrame cfg=$isConfig pts=$ptsUs")
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
"feed(): mime=$decoderMime in=$inCount size=${data.size} key=$isKeyFrame cfg=$isConfig pts=$ptsUs"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isConfig || isKeyFrame) {
|
if (isConfig || isKeyFrame) {
|
||||||
Log.w(TAG, "drop critical packet: mime=$decoderMime size=${data.size} key=$isKeyFrame cfg=$isConfig")
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
"drop critical packet: mime=$decoderMime size=${data.size} key=$isKeyFrame cfg=$isConfig"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
drainOutput()
|
drainOutput()
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Log.w(TAG, "feed failed: mime=$decoderMime size=${data.size} key=$isKeyFrame cfg=$isConfig", it)
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
"feed failed: mime=$decoderMime size=${data.size} key=$isKeyFrame cfg=$isConfig",
|
||||||
|
it
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,10 +119,12 @@ class AnnexBDecoder(
|
|||||||
Log.i(TAG, "drain(): mime=$decoderMime out=$outCount")
|
Log.i(TAG, "drain(): mime=$decoderMime out=$outCount")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
outIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
|
outIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
|
||||||
notifyOutputSize(codec.outputFormat)
|
notifyOutputSize(codec.outputFormat)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> return
|
else -> return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,23 +41,33 @@ internal class DirectAdbTransport(private val context: Context) {
|
|||||||
val privateKey: PrivateKey get() = keys.first
|
val privateKey: PrivateKey get() = keys.first
|
||||||
val publicKeyX509: ByteArray get() = keys.second
|
val publicKeyX509: ByteArray get() = keys.second
|
||||||
|
|
||||||
@Volatile var keyName: String = AppDefaults.DefaultAdbKeyName
|
@Volatile
|
||||||
|
var keyName: String = AppDefaults.ADB_KEY_NAME
|
||||||
|
|
||||||
fun connect(host: String, port: Int): DirectAdbConnection {
|
fun connect(host: String, port: Int): DirectAdbConnection {
|
||||||
Log.i(TAG, "connect(): opening direct adbd transport to $host:$port")
|
Log.i(TAG, "connect(): opening direct adbd transport to $host:$port")
|
||||||
val conn = DirectAdbConnection(host, port, privateKey, publicKeyX509, keyName.ifBlank { AppDefaults.DefaultAdbKeyName })
|
val conn = DirectAdbConnection(
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
privateKey,
|
||||||
|
publicKeyX509,
|
||||||
|
keyName.ifBlank { AppDefaults.ADB_KEY_NAME })
|
||||||
conn.handshake()
|
conn.handshake()
|
||||||
Log.i(TAG, "connect(): handshake success for $host:$port")
|
Log.i(TAG, "connect(): handshake success for $host:$port")
|
||||||
return conn
|
return conn
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadOrCreate(): Pair<PrivateKey, ByteArray> {
|
private fun loadOrCreate(): Pair<PrivateKey, ByteArray> {
|
||||||
val prefs = context.getSharedPreferences(AppPreferenceKeys.NativeAdbKeyPrefsName, Context.MODE_PRIVATE)
|
val prefs = context.getSharedPreferences(
|
||||||
val privB64 = prefs.getString(AppPreferenceKeys.NativeAdbPrivateKey, null)
|
AppPreferenceKeys.NATIVE_ADB_KEY_PREFS_NAME,
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
)
|
||||||
|
val privB64 = prefs.getString(AppPreferenceKeys.NATIVE_ADB_PRIVATE_KEY, null)
|
||||||
if (privB64 != null) {
|
if (privB64 != null) {
|
||||||
try {
|
try {
|
||||||
val kf = KeyFactory.getInstance("RSA")
|
val kf = KeyFactory.getInstance("RSA")
|
||||||
val priv = kf.generatePrivate(PKCS8EncodedKeySpec(Base64.decode(privB64, Base64.DEFAULT)))
|
val priv =
|
||||||
|
kf.generatePrivate(PKCS8EncodedKeySpec(Base64.decode(privB64, Base64.DEFAULT)))
|
||||||
val pub = derivePublicX509(priv)
|
val pub = derivePublicX509(priv)
|
||||||
Log.i(TAG, "loadOrCreate(): loaded persisted RSA key pair, fp=${fingerprint(pub)}")
|
Log.i(TAG, "loadOrCreate(): loaded persisted RSA key pair, fp=${fingerprint(pub)}")
|
||||||
return Pair(priv, pub)
|
return Pair(priv, pub)
|
||||||
@@ -68,8 +78,16 @@ internal class DirectAdbTransport(private val context: Context) {
|
|||||||
val kpg = KeyPairGenerator.getInstance("RSA")
|
val kpg = KeyPairGenerator.getInstance("RSA")
|
||||||
kpg.initialize(2048)
|
kpg.initialize(2048)
|
||||||
val kp = kpg.generateKeyPair()
|
val kp = kpg.generateKeyPair()
|
||||||
prefs.edit { putString(AppPreferenceKeys.NativeAdbPrivateKey, Base64.encodeToString(kp.private.encoded, Base64.NO_WRAP)) }
|
prefs.edit {
|
||||||
Log.i(TAG, "loadOrCreate(): generated new RSA key pair, fp=${fingerprint(kp.public.encoded)}")
|
putString(
|
||||||
|
AppPreferenceKeys.NATIVE_ADB_PRIVATE_KEY,
|
||||||
|
Base64.encodeToString(kp.private.encoded, Base64.NO_WRAP)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
"loadOrCreate(): generated new RSA key pair, fp=${fingerprint(kp.public.encoded)}"
|
||||||
|
)
|
||||||
return Pair(kp.private, kp.public.encoded)
|
return Pair(kp.private, kp.public.encoded)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +114,7 @@ internal class DirectAdbConnection(
|
|||||||
val port: Int,
|
val port: Int,
|
||||||
private val privateKey: PrivateKey,
|
private val privateKey: PrivateKey,
|
||||||
private val publicKeyX509: ByteArray,
|
private val publicKeyX509: ByteArray,
|
||||||
private val keyName: String = AppDefaults.DefaultAdbKeyName,
|
private val keyName: String = AppDefaults.ADB_KEY_NAME,
|
||||||
) : AutoCloseable {
|
) : AutoCloseable {
|
||||||
|
|
||||||
private val sha1DigestInfoPrefix = byteArrayOf(
|
private val sha1DigestInfoPrefix = byteArrayOf(
|
||||||
@@ -122,7 +140,9 @@ internal class DirectAdbConnection(
|
|||||||
private lateinit var rawOut: OutputStream
|
private lateinit var rawOut: OutputStream
|
||||||
private val nextLocalId = AtomicInteger(1)
|
private val nextLocalId = AtomicInteger(1)
|
||||||
private val streams = ConcurrentHashMap<Int, AdbSocketStream>()
|
private val streams = ConcurrentHashMap<Int, AdbSocketStream>()
|
||||||
@Volatile private var closed = false
|
|
||||||
|
@Volatile
|
||||||
|
private var closed = false
|
||||||
private var readerThread: Thread? = null
|
private var readerThread: Thread? = null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -171,9 +191,17 @@ internal class DirectAdbConnection(
|
|||||||
throw IOException("ADB: connection rejected. Please accept the authorisation dialog on the target device.")
|
throw IOException("ADB: connection rejected. Please accept the authorisation dialog on the target device.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> throw IOException("ADB: unexpected message 0x${afterSign.command.toString(16)} after AUTH_SIGNATURE")
|
|
||||||
|
else -> throw IOException(
|
||||||
|
"ADB: unexpected message 0x${
|
||||||
|
afterSign.command.toString(
|
||||||
|
16
|
||||||
|
)
|
||||||
|
} after AUTH_SIGNATURE"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> throw IOException("ADB: unexpected initial message 0x${first.command.toString(16)}")
|
else -> throw IOException("ADB: unexpected initial message 0x${first.command.toString(16)}")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,6 +290,7 @@ internal class DirectAdbConnection(
|
|||||||
sendMsg(A_CLSE, 0, msg.arg0)
|
sendMsg(A_CLSE, 0, msg.arg0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
A_CLSE -> streams.remove(msg.arg1)?.forceClose()
|
A_CLSE -> streams.remove(msg.arg1)?.forceClose()
|
||||||
A_OPEN -> sendMsg(A_CLSE, 0, msg.arg0)
|
A_OPEN -> sendMsg(A_CLSE, 0, msg.arg0)
|
||||||
}
|
}
|
||||||
@@ -275,7 +304,12 @@ internal class DirectAdbConnection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
private fun sendMsg(command: Int, arg0: Int = 0, arg1: Int = 0, data: ByteArray = ByteArray(0)) {
|
private fun sendMsg(
|
||||||
|
command: Int,
|
||||||
|
arg0: Int = 0,
|
||||||
|
arg1: Int = 0,
|
||||||
|
data: ByteArray = ByteArray(0)
|
||||||
|
) {
|
||||||
val crc = data.fold(0L) { acc, b -> acc + (b.toLong() and 0xFF) }.toInt()
|
val crc = data.fold(0L) { acc, b -> acc + (b.toLong() and 0xFF) }.toInt()
|
||||||
val header = ByteBuffer.allocate(24).order(ByteOrder.LITTLE_ENDIAN)
|
val header = ByteBuffer.allocate(24).order(ByteOrder.LITTLE_ENDIAN)
|
||||||
.putInt(command).putInt(arg0).putInt(arg1)
|
.putInt(command).putInt(arg0).putInt(arg1)
|
||||||
@@ -296,7 +330,8 @@ internal class DirectAdbConnection(
|
|||||||
val dataLen = buf.int
|
val dataLen = buf.int
|
||||||
buf.int
|
buf.int
|
||||||
buf.int
|
buf.int
|
||||||
val data = if (dataLen > 0) ByteArray(dataLen).also { rawIn.readExact(it) } else ByteArray(0)
|
val data =
|
||||||
|
if (dataLen > 0) ByteArray(dataLen).also { rawIn.readExact(it) } else ByteArray(0)
|
||||||
return AdbMsg(command, arg0, arg1, data)
|
return AdbMsg(command, arg0, arg1, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,12 +426,16 @@ internal class AdbSocketStream(
|
|||||||
private const val A_CLSE = 0x45534c43
|
private const val A_CLSE = 0x45534c43
|
||||||
}
|
}
|
||||||
|
|
||||||
@Volatile var remoteId: Int = 0
|
@Volatile
|
||||||
@Volatile var closed: Boolean = false
|
var remoteId: Int = 0
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
var closed: Boolean = false
|
||||||
|
|
||||||
private val latch = CountDownLatch(1)
|
private val latch = CountDownLatch(1)
|
||||||
private val latchOk = AtomicBoolean(false)
|
private val latchOk = AtomicBoolean(false)
|
||||||
private val queue = LinkedBlockingQueue<Any>()
|
private val queue = LinkedBlockingQueue<Any>()
|
||||||
|
|
||||||
private object EndOfStreamMarker
|
private object EndOfStreamMarker
|
||||||
|
|
||||||
val inputStream: InputStream = InStream()
|
val inputStream: InputStream = InStream()
|
||||||
@@ -476,6 +515,7 @@ internal class AdbSocketStream(
|
|||||||
if (len == 0) return
|
if (len == 0) return
|
||||||
sender(A_WRTE, localId, remoteId, b.copyOfRange(off, off + len))
|
sender(A_WRTE, localId, remoteId, b.copyOfRange(off, off + len))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun flush() {}
|
override fun flush() {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,18 @@ import java.nio.file.Path
|
|||||||
class NativeAdbService(appContext: Context) {
|
class NativeAdbService(appContext: Context) {
|
||||||
|
|
||||||
private val transport = DirectAdbTransport(appContext)
|
private val transport = DirectAdbTransport(appContext)
|
||||||
@Volatile private var connection: DirectAdbConnection? = null
|
@Volatile
|
||||||
@Volatile private var connectedHost: String? = null
|
private var connection: DirectAdbConnection? = null
|
||||||
@Volatile private var connectedPort: Int? = null
|
@Volatile
|
||||||
|
private var connectedHost: String? = null
|
||||||
|
@Volatile
|
||||||
|
private var connectedPort: Int? = null
|
||||||
|
|
||||||
var keyName: String
|
var keyName: String
|
||||||
get() = transport.keyName
|
get() = transport.keyName
|
||||||
set(value) { transport.keyName = value }
|
set(value) {
|
||||||
|
transport.keyName = value
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun pair(host: String, port: Int, pairingCode: String): Boolean {
|
fun pair(host: String, port: Int, pairingCode: String): Boolean {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.media.AudioAttributes
|
|||||||
import android.media.AudioFormat
|
import android.media.AudioFormat
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.media.AudioTrack
|
import android.media.AudioTrack
|
||||||
import android.os.Build
|
|
||||||
import android.media.MediaCodec
|
import android.media.MediaCodec
|
||||||
import android.media.MediaFormat
|
import android.media.MediaFormat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -23,15 +22,22 @@ class ScrcpyAudioPlayer(private val codecId: Int) {
|
|||||||
private var audioTrack: AudioTrack? = null
|
private var audioTrack: AudioTrack? = null
|
||||||
private val bufferInfo = MediaCodec.BufferInfo()
|
private val bufferInfo = MediaCodec.BufferInfo()
|
||||||
|
|
||||||
@Volatile private var prepared = false
|
@Volatile
|
||||||
@Volatile private var released = false
|
private var prepared = false
|
||||||
|
@Volatile
|
||||||
|
private var released = false
|
||||||
private var packetCount = 0L
|
private var packetCount = 0L
|
||||||
|
|
||||||
fun feedPacket(data: ByteArray, ptsUs: Long, isConfig: Boolean) {
|
fun feedPacket(data: ByteArray, ptsUs: Long, isConfig: Boolean) {
|
||||||
if (released) return
|
if (released) return
|
||||||
|
|
||||||
if (isConfig) {
|
if (isConfig) {
|
||||||
Log.i(TAG, "feedPacket(): config packet size=${data.size} codec=0x${codecId.toUInt().toString(16)}")
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
"feedPacket(): config packet size=${data.size} codec=0x${
|
||||||
|
codecId.toUInt().toString(16)
|
||||||
|
}"
|
||||||
|
)
|
||||||
when (codecId) {
|
when (codecId) {
|
||||||
AUDIO_CODEC_OPUS -> prepareOpus(data)
|
AUDIO_CODEC_OPUS -> prepareOpus(data)
|
||||||
AUDIO_CODEC_AAC -> prepareAac(data)
|
AUDIO_CODEC_AAC -> prepareAac(data)
|
||||||
@@ -47,13 +53,7 @@ class ScrcpyAudioPlayer(private val codecId: Int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (codecId == AUDIO_CODEC_RAW) {
|
if (codecId == AUDIO_CODEC_RAW) {
|
||||||
ensureRawAudioTrack()?.let { track ->
|
ensureRawAudioTrack()?.write(data, 0, data.size, AudioTrack.WRITE_NON_BLOCKING)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
track.write(data, 0, data.size, AudioTrack.WRITE_NON_BLOCKING)
|
|
||||||
} else {
|
|
||||||
track.write(data, 0, data.size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,11 +74,16 @@ class ScrcpyAudioPlayer(private val codecId: Int) {
|
|||||||
private fun prepareOpus(opusHead: ByteArray) {
|
private fun prepareOpus(opusHead: ByteArray) {
|
||||||
if (prepared || released) return
|
if (prepared || released) return
|
||||||
runCatching {
|
runCatching {
|
||||||
val format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_OPUS, SAMPLE_RATE, CHANNELS)
|
val format = MediaFormat.createAudioFormat(
|
||||||
|
MediaFormat.MIMETYPE_AUDIO_OPUS,
|
||||||
|
SAMPLE_RATE,
|
||||||
|
CHANNELS
|
||||||
|
)
|
||||||
format.setByteBuffer("csd-0", ByteBuffer.wrap(opusHead))
|
format.setByteBuffer("csd-0", ByteBuffer.wrap(opusHead))
|
||||||
// pre-skip field: 2 bytes LE at offset 10 of the OpusHead
|
// pre-skip field: 2 bytes LE at offset 10 of the OpusHead
|
||||||
if (opusHead.size >= 12) {
|
if (opusHead.size >= 12) {
|
||||||
val preSkip = ((opusHead[11].toInt() and 0xFF) shl 8) or (opusHead[10].toInt() and 0xFF)
|
val preSkip =
|
||||||
|
((opusHead[11].toInt() and 0xFF) shl 8) or (opusHead[10].toInt() and 0xFF)
|
||||||
val codecDelayNs = preSkip.toLong() * 1_000_000_000L / SAMPLE_RATE
|
val codecDelayNs = preSkip.toLong() * 1_000_000_000L / SAMPLE_RATE
|
||||||
format.setByteBuffer("csd-1", longBuffer(codecDelayNs))
|
format.setByteBuffer("csd-1", longBuffer(codecDelayNs))
|
||||||
format.setByteBuffer("csd-2", longBuffer(OPUS_SEEK_PREROLL_NS))
|
format.setByteBuffer("csd-2", longBuffer(OPUS_SEEK_PREROLL_NS))
|
||||||
@@ -90,7 +95,8 @@ class ScrcpyAudioPlayer(private val codecId: Int) {
|
|||||||
private fun prepareAac(aacConfig: ByteArray) {
|
private fun prepareAac(aacConfig: ByteArray) {
|
||||||
if (prepared || released) return
|
if (prepared || released) return
|
||||||
runCatching {
|
runCatching {
|
||||||
val format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, SAMPLE_RATE, CHANNELS)
|
val format =
|
||||||
|
MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, SAMPLE_RATE, CHANNELS)
|
||||||
format.setByteBuffer("csd-0", ByteBuffer.wrap(aacConfig))
|
format.setByteBuffer("csd-0", ByteBuffer.wrap(aacConfig))
|
||||||
startCodecAndTrack(format)
|
startCodecAndTrack(format)
|
||||||
}.onFailure { Log.w(TAG, "prepareAac failed", it) }
|
}.onFailure { Log.w(TAG, "prepareAac failed", it) }
|
||||||
@@ -99,7 +105,11 @@ class ScrcpyAudioPlayer(private val codecId: Int) {
|
|||||||
private fun prepareFlac(flacConfig: ByteArray) {
|
private fun prepareFlac(flacConfig: ByteArray) {
|
||||||
if (prepared || released) return
|
if (prepared || released) return
|
||||||
runCatching {
|
runCatching {
|
||||||
val format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_FLAC, SAMPLE_RATE, CHANNELS)
|
val format = MediaFormat.createAudioFormat(
|
||||||
|
MediaFormat.MIMETYPE_AUDIO_FLAC,
|
||||||
|
SAMPLE_RATE,
|
||||||
|
CHANNELS
|
||||||
|
)
|
||||||
if (flacConfig.isNotEmpty()) {
|
if (flacConfig.isNotEmpty()) {
|
||||||
format.setByteBuffer("csd-0", ByteBuffer.wrap(flacConfig))
|
format.setByteBuffer("csd-0", ByteBuffer.wrap(flacConfig))
|
||||||
}
|
}
|
||||||
@@ -161,11 +171,7 @@ class ScrcpyAudioPlayer(private val codecId: Int) {
|
|||||||
val pcm = ByteArray(size)
|
val pcm = ByteArray(size)
|
||||||
outBuf.position(bufferInfo.offset)
|
outBuf.position(bufferInfo.offset)
|
||||||
outBuf.get(pcm)
|
outBuf.get(pcm)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
track.write(pcm, 0, size, AudioTrack.WRITE_NON_BLOCKING)
|
||||||
track.write(pcm, 0, size, AudioTrack.WRITE_NON_BLOCKING)
|
|
||||||
} else {
|
|
||||||
track.write(pcm, 0, size)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
codec.releaseOutputBuffer(idx, false)
|
codec.releaseOutputBuffer(idx, false)
|
||||||
idx = codec.dequeueOutputBuffer(bufferInfo, 0L)
|
idx = codec.dequeueOutputBuffer(bufferInfo, 0L)
|
||||||
@@ -190,9 +196,9 @@ class ScrcpyAudioPlayer(private val codecId: Int) {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "ScrcpyAudioPlayer"
|
private const val TAG = "ScrcpyAudioPlayer"
|
||||||
const val AUDIO_CODEC_OPUS = 0x6f707573
|
const val AUDIO_CODEC_OPUS = 0x6f707573
|
||||||
const val AUDIO_CODEC_AAC = 0x00616163
|
const val AUDIO_CODEC_AAC = 0x00616163
|
||||||
const val AUDIO_CODEC_FLAC = 0x666c6163
|
const val AUDIO_CODEC_FLAC = 0x666c6163
|
||||||
const val AUDIO_CODEC_RAW = 0x00726177
|
const val AUDIO_CODEC_RAW = 0x00726177
|
||||||
private const val SAMPLE_RATE = 48000
|
private const val SAMPLE_RATE = 48000
|
||||||
private const val CHANNELS = 2
|
private const val CHANNELS = 2
|
||||||
private const val CODEC_TIMEOUT_US = 10_000L
|
private const val CODEC_TIMEOUT_US = 10_000L
|
||||||
|
|||||||
@@ -11,21 +11,25 @@ import java.io.InputStreamReader
|
|||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.ArrayDeque
|
import java.util.ArrayDeque
|
||||||
import java.util.LinkedHashSet
|
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
||||||
@Volatile
|
@Volatile
|
||||||
private var activeSession: ActiveSession? = null
|
private var activeSession: ActiveSession? = null
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var videoConsumer: ((VideoPacket) -> Unit)? = null
|
private var videoConsumer: ((VideoPacket) -> Unit)? = null
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var videoReaderThread: Thread? = null
|
private var videoReaderThread: Thread? = null
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var audioConsumer: ((AudioPacket) -> Unit)? = null
|
private var audioConsumer: ((AudioPacket) -> Unit)? = null
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var audioReaderThread: Thread? = null
|
private var audioReaderThread: Thread? = null
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var lastServerCommand: String? = null
|
private var lastServerCommand: String? = null
|
||||||
private val serverLogBuffer = ArrayDeque<String>()
|
private val serverLogBuffer = ArrayDeque<String>()
|
||||||
@@ -44,7 +48,10 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
|||||||
adbService.push(serverJarPath, targetPath)
|
adbService.push(serverJarPath, targetPath)
|
||||||
val cmd = buildServerCommand(targetPath, scid, options)
|
val cmd = buildServerCommand(targetPath, scid, options)
|
||||||
lastServerCommand = cmd
|
lastServerCommand = cmd
|
||||||
Log.i(TAG, "start(): socket=$socketName codec=${options.videoCodec} audio=${options.audio} audioCodec=${options.audioCodec}")
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
"start(): socket=$socketName codec=${options.videoCodec} audio=${options.audio} audioCodec=${options.audioCodec}"
|
||||||
|
)
|
||||||
val serverStream = adbService.openShellStream(cmd)
|
val serverStream = adbService.openShellStream(cmd)
|
||||||
val serverLogThread = startServerLogThread(serverStream, socketName)
|
val serverLogThread = startServerLogThread(serverStream, socketName)
|
||||||
Thread.sleep(SERVER_BOOT_DELAY_MS)
|
Thread.sleep(SERVER_BOOT_DELAY_MS)
|
||||||
@@ -64,13 +71,16 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
|||||||
videoStream = firstStream
|
videoStream = firstStream
|
||||||
videoInput = firstInput
|
videoInput = firstInput
|
||||||
}
|
}
|
||||||
|
|
||||||
options.audio -> {
|
options.audio -> {
|
||||||
audioStream = firstStream
|
audioStream = firstStream
|
||||||
audioInput = firstInput
|
audioInput = firstInput
|
||||||
}
|
}
|
||||||
|
|
||||||
options.control -> {
|
options.control -> {
|
||||||
controlStream = firstStream
|
controlStream = firstStream
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
throw IllegalArgumentException("At least one of video/audio/control must be enabled")
|
throw IllegalArgumentException("At least one of video/audio/control must be enabled")
|
||||||
}
|
}
|
||||||
@@ -128,11 +138,15 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
|||||||
audioStream = audioStream,
|
audioStream = audioStream,
|
||||||
audioInput = audioInput,
|
audioInput = audioInput,
|
||||||
controlStream = controlStream,
|
controlStream = controlStream,
|
||||||
controlWriter = controlStream?.outputStream?.let { ScrcpyControlWriter(DataOutputStream(it)) },
|
controlWriter = controlStream?.outputStream?.let {
|
||||||
|
ScrcpyControlWriter(
|
||||||
|
DataOutputStream(it)
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
return sessionInfo
|
return sessionInfo
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
val tail = snapshotServerLogs(maxLines = 120)
|
val tail = snapshotServerLogs()
|
||||||
val detail = if (tail.isBlank()) "" else " | server_log_tail=\n$tail"
|
val detail = if (tail.isBlank()) "" else " | server_log_tail=\n$tail"
|
||||||
throw IllegalStateException("scrcpy start failed: ${t.message}$detail", t)
|
throw IllegalStateException("scrcpy start failed: ${t.message}$detail", t)
|
||||||
}
|
}
|
||||||
@@ -173,7 +187,7 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
|||||||
)
|
)
|
||||||
} catch (_: EOFException) {
|
} catch (_: EOFException) {
|
||||||
break
|
break
|
||||||
} catch (e: InterruptedException) {
|
} catch (_: InterruptedException) {
|
||||||
// Ignore transient interrupts while session remains active.
|
// Ignore transient interrupts while session remains active.
|
||||||
if (activeSession !== session || vStream.closed) {
|
if (activeSession !== session || vStream.closed) {
|
||||||
break
|
break
|
||||||
@@ -212,10 +226,12 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
|||||||
Log.w(TAG, "audio disabled by server")
|
Log.w(TAG, "audio disabled by server")
|
||||||
return@thread
|
return@thread
|
||||||
}
|
}
|
||||||
|
|
||||||
AUDIO_ERROR -> {
|
AUDIO_ERROR -> {
|
||||||
Log.e(TAG, "audio stream configuration error from server")
|
Log.e(TAG, "audio stream configuration error from server")
|
||||||
return@thread
|
return@thread
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Log.i(TAG, "audio stream codec=0x${streamCodecId.toUInt().toString(16)}")
|
Log.i(TAG, "audio stream codec=0x${streamCodecId.toUInt().toString(16)}")
|
||||||
}
|
}
|
||||||
@@ -223,7 +239,9 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
|||||||
if (session.info.audioCodecId != 0 && streamCodecId != session.info.audioCodecId) {
|
if (session.info.audioCodecId != 0 && streamCodecId != session.info.audioCodecId) {
|
||||||
Log.w(
|
Log.w(
|
||||||
TAG,
|
TAG,
|
||||||
"audio codec mismatch: requested=0x${session.info.audioCodecId.toUInt().toString(16)} stream=0x${streamCodecId.toUInt().toString(16)}",
|
"audio codec mismatch: requested=0x${
|
||||||
|
session.info.audioCodecId.toUInt().toString(16)
|
||||||
|
} stream=0x${streamCodecId.toUInt().toString(16)}",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,10 +256,16 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
|||||||
|
|
||||||
val isConfig = (ptsAndFlags and PACKET_FLAG_CONFIG) != 0L
|
val isConfig = (ptsAndFlags and PACKET_FLAG_CONFIG) != 0L
|
||||||
val ptsUs = ptsAndFlags and PACKET_PTS_MASK
|
val ptsUs = ptsAndFlags and PACKET_PTS_MASK
|
||||||
audioConsumer?.invoke(AudioPacket(data = payload, ptsUs = ptsUs, isConfig = isConfig))
|
audioConsumer?.invoke(
|
||||||
|
AudioPacket(
|
||||||
|
data = payload,
|
||||||
|
ptsUs = ptsUs,
|
||||||
|
isConfig = isConfig
|
||||||
|
)
|
||||||
|
)
|
||||||
} catch (_: EOFException) {
|
} catch (_: EOFException) {
|
||||||
break
|
break
|
||||||
} catch (e: InterruptedException) {
|
} catch (_: InterruptedException) {
|
||||||
if (activeSession !== session || aStream.closed) break
|
if (activeSession !== session || aStream.closed) break
|
||||||
Thread.interrupted()
|
Thread.interrupted()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -279,12 +303,38 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
|||||||
actionButton: Int,
|
actionButton: Int,
|
||||||
buttons: Int,
|
buttons: Int,
|
||||||
) {
|
) {
|
||||||
requireControlWriter().injectTouch(action, pointerId, x, y, screenWidth, screenHeight, pressure, actionButton, buttons)
|
requireControlWriter().injectTouch(
|
||||||
|
action,
|
||||||
|
pointerId,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
screenWidth,
|
||||||
|
screenHeight,
|
||||||
|
pressure,
|
||||||
|
actionButton,
|
||||||
|
buttons
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun injectScroll(x: Int, y: Int, screenWidth: Int, screenHeight: Int, hScroll: Float, vScroll: Float, buttons: Int) {
|
fun injectScroll(
|
||||||
requireControlWriter().injectScroll(x, y, screenWidth, screenHeight, hScroll, vScroll, buttons)
|
x: Int,
|
||||||
|
y: Int,
|
||||||
|
screenWidth: Int,
|
||||||
|
screenHeight: Int,
|
||||||
|
hScroll: Float,
|
||||||
|
vScroll: Float,
|
||||||
|
buttons: Int
|
||||||
|
) {
|
||||||
|
requireControlWriter().injectScroll(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
screenWidth,
|
||||||
|
screenHeight,
|
||||||
|
hScroll,
|
||||||
|
vScroll,
|
||||||
|
buttons
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@@ -384,10 +434,15 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun requireControlWriter(): ScrcpyControlWriter {
|
private fun requireControlWriter(): ScrcpyControlWriter {
|
||||||
return activeSession?.controlWriter ?: throw IllegalStateException("scrcpy control channel not available")
|
return activeSession?.controlWriter
|
||||||
|
?: throw IllegalStateException("scrcpy control channel not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildServerCommand(targetPath: String, scid: Int, options: ScrcpyStartOptions): String {
|
private fun buildServerCommand(
|
||||||
|
targetPath: String,
|
||||||
|
scid: Int,
|
||||||
|
options: ScrcpyStartOptions
|
||||||
|
): String {
|
||||||
val serverArgs = buildServerArgs(scid, options)
|
val serverArgs = buildServerArgs(scid, options)
|
||||||
return "CLASSPATH=$targetPath app_process / com.genymobile.scrcpy.Server ${options.serverVersion} $serverArgs"
|
return "CLASSPATH=$targetPath app_process / com.genymobile.scrcpy.Server ${options.serverVersion} $serverArgs"
|
||||||
}
|
}
|
||||||
@@ -474,7 +529,7 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
|||||||
ServerArg(
|
ServerArg(
|
||||||
type = ServerArgType.NUMBER,
|
type = ServerArgType.NUMBER,
|
||||||
key = "video_bit_rate",
|
key = "video_bit_rate",
|
||||||
value = options.videoBitRate, zeroValueBehavior = ZeroValueBehavior.KEEP,
|
value = options.videoBitRate,
|
||||||
),
|
),
|
||||||
ServerArg(
|
ServerArg(
|
||||||
type = ServerArgType.NUMBER,
|
type = ServerArgType.NUMBER,
|
||||||
@@ -528,7 +583,6 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
|||||||
key = "audio_bit_rate",
|
key = "audio_bit_rate",
|
||||||
value = options.audioBitRate,
|
value = options.audioBitRate,
|
||||||
includeWhen = options.audio,
|
includeWhen = options.audio,
|
||||||
zeroValueBehavior = ZeroValueBehavior.KEEP,
|
|
||||||
),
|
),
|
||||||
ServerArg(
|
ServerArg(
|
||||||
type = ServerArgType.STRING,
|
type = ServerArgType.STRING,
|
||||||
@@ -539,7 +593,8 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
|||||||
ServerArg(
|
ServerArg(
|
||||||
type = ServerArgType.BOOLEAN,
|
type = ServerArgType.BOOLEAN,
|
||||||
key = "audio_dup",
|
key = "audio_dup",
|
||||||
value = options.audioDup, includeWhen = options.audioDup,
|
value = options.audioDup,
|
||||||
|
includeWhen = options.audioDup,
|
||||||
),
|
),
|
||||||
ServerArg(
|
ServerArg(
|
||||||
type = ServerArgType.STRING,
|
type = ServerArgType.STRING,
|
||||||
@@ -579,15 +634,22 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
|||||||
ServerArg(
|
ServerArg(
|
||||||
type = ServerArgType.BOOLEAN,
|
type = ServerArgType.BOOLEAN,
|
||||||
key = "list_encoders",
|
key = "list_encoders",
|
||||||
value = options.listEncoders, includeWhen = options.listEncoders,
|
value = options.listEncoders,
|
||||||
|
includeWhen = options.listEncoders,
|
||||||
),
|
),
|
||||||
ServerArg(
|
ServerArg(
|
||||||
type = ServerArgType.BOOLEAN,
|
type = ServerArgType.BOOLEAN,
|
||||||
key = "list_camera_sizes",
|
key = "list_camera_sizes",
|
||||||
value = options.listCameraSizes, includeWhen = options.listCameraSizes,
|
value = options.listCameraSizes,
|
||||||
|
includeWhen = options.listCameraSizes,
|
||||||
),
|
),
|
||||||
// Reserved for future args that require repeated/list values.
|
// Reserved for future args that require repeated/list values.
|
||||||
// ServerArg(type = ServerArgType.LIST, key = "_unused_list", value = emptyList<String>(), includeWhen = false),
|
// ServerArg(
|
||||||
|
// type = ServerArgType.LIST,
|
||||||
|
// key = "_unused_list",
|
||||||
|
// value = emptyList<String>(),
|
||||||
|
// includeWhen = false,
|
||||||
|
// ),
|
||||||
)
|
)
|
||||||
|
|
||||||
val rendered = args.mapNotNull { it.render() }
|
val rendered = args.mapNotNull { it.render() }
|
||||||
@@ -608,8 +670,8 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private enum class ZeroValueBehavior {
|
private enum class ZeroValueBehavior {
|
||||||
KEEP,
|
|
||||||
OMIT,
|
OMIT,
|
||||||
|
KEEP,
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class ServerArg<T>(
|
private data class ServerArg<T>(
|
||||||
@@ -716,7 +778,12 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
|||||||
private fun startServerLogThread(serverStream: AdbSocketStream, socketName: String): Thread {
|
private fun startServerLogThread(serverStream: AdbSocketStream, socketName: String): Thread {
|
||||||
return thread(start = true, name = "scrcpy-server-log") {
|
return thread(start = true, name = "scrcpy-server-log") {
|
||||||
try {
|
try {
|
||||||
BufferedReader(InputStreamReader(serverStream.inputStream, Charsets.UTF_8)).use { reader ->
|
BufferedReader(
|
||||||
|
InputStreamReader(
|
||||||
|
serverStream.inputStream,
|
||||||
|
Charsets.UTF_8
|
||||||
|
)
|
||||||
|
).use { reader ->
|
||||||
while (true) {
|
while (true) {
|
||||||
val line = reader.readLine() ?: break
|
val line = reader.readLine() ?: break
|
||||||
appendServerLog(line)
|
appendServerLog(line)
|
||||||
@@ -740,7 +807,7 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
private fun snapshotServerLogs(maxLines: Int): String {
|
private fun snapshotServerLogs(maxLines: Int = 120): String {
|
||||||
if (serverLogBuffer.isEmpty()) {
|
if (serverLogBuffer.isEmpty()) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -748,7 +815,10 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
|||||||
return serverLogBuffer.toList().takeLast(take).joinToString("\n")
|
return serverLogBuffer.toList().takeLast(take).joinToString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openAbstractSocketWithRetry(socketName: String, expectDummyByte: Boolean): AdbSocketStream {
|
private fun openAbstractSocketWithRetry(
|
||||||
|
socketName: String,
|
||||||
|
expectDummyByte: Boolean
|
||||||
|
): AdbSocketStream {
|
||||||
var lastEx: Exception? = null
|
var lastEx: Exception? = null
|
||||||
repeat(CONNECT_RETRY_COUNT) { attempt ->
|
repeat(CONNECT_RETRY_COUNT) { attempt ->
|
||||||
try {
|
try {
|
||||||
@@ -963,7 +1033,15 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun injectScroll(x: Int, y: Int, screenWidth: Int, screenHeight: Int, hScroll: Float, vScroll: Float, buttons: Int) {
|
fun injectScroll(
|
||||||
|
x: Int,
|
||||||
|
y: Int,
|
||||||
|
screenWidth: Int,
|
||||||
|
screenHeight: Int,
|
||||||
|
hScroll: Float,
|
||||||
|
vScroll: Float,
|
||||||
|
buttons: Int
|
||||||
|
) {
|
||||||
output.writeByte(TYPE_INJECT_SCROLL_EVENT)
|
output.writeByte(TYPE_INJECT_SCROLL_EVENT)
|
||||||
writePosition(x, y, screenWidth, screenHeight)
|
writePosition(x, y, screenWidth, screenHeight)
|
||||||
output.writeShort(encodeSignedFixedPoint16(hScroll / 16f))
|
output.writeShort(encodeSignedFixedPoint16(hScroll / 16f))
|
||||||
@@ -1033,6 +1111,7 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
|||||||
private const val AUDIO_CODEC_AAC = 0x00616163
|
private const val AUDIO_CODEC_AAC = 0x00616163
|
||||||
private const val AUDIO_CODEC_FLAC = 0x666c6163
|
private const val AUDIO_CODEC_FLAC = 0x666c6163
|
||||||
private const val AUDIO_CODEC_RAW = 0x00726177
|
private const val AUDIO_CODEC_RAW = 0x00726177
|
||||||
|
|
||||||
// Audio stream disable codes from server (writeDisableStream)
|
// Audio stream disable codes from server (writeDisableStream)
|
||||||
private const val AUDIO_DISABLED = 0
|
private const val AUDIO_DISABLED = 0
|
||||||
private const val AUDIO_ERROR = 1
|
private const val AUDIO_ERROR = 1
|
||||||
@@ -1044,10 +1123,12 @@ class ScrcpySessionManager(private val adbService: NativeAdbService) {
|
|||||||
private const val TYPE_SET_DISPLAY_POWER = 10
|
private const val TYPE_SET_DISPLAY_POWER = 10
|
||||||
private val VIDEO_ENCODER_REGEX = Regex("--video-encoder=([\\w.\\-]+)")
|
private val VIDEO_ENCODER_REGEX = Regex("--video-encoder=([\\w.\\-]+)")
|
||||||
private val AUDIO_ENCODER_REGEX = Regex("--audio-encoder=([\\w.\\-]+)")
|
private val AUDIO_ENCODER_REGEX = Regex("--audio-encoder=([\\w.\\-]+)")
|
||||||
private val VIDEO_ENCODER_FALLBACK_REGEX = Regex("""--video-encoder=['\"]?([^'\"\s]+)""")
|
private val VIDEO_ENCODER_FALLBACK_REGEX = Regex("""--video-encoder=['"]?([^'"\s]+)""")
|
||||||
private val AUDIO_ENCODER_FALLBACK_REGEX = Regex("""--audio-encoder=['\"]?([^'\"\s]+)""")
|
private val AUDIO_ENCODER_FALLBACK_REGEX = Regex("""--audio-encoder=['"]?([^'"\s]+)""")
|
||||||
private val VIDEO_ENCODER_TYPE_REGEX = Regex("""--video-codec=[^\s]+\s+--video-encoder=([^\s]+).*?\((hw|sw)\)""")
|
private val VIDEO_ENCODER_TYPE_REGEX =
|
||||||
private val AUDIO_ENCODER_TYPE_REGEX = Regex("""--audio-codec=[^\s]+\s+--audio-encoder=([^\s]+).*?\((hw|sw)\)""")
|
Regex("""--video-codec=\S+\s+--video-encoder=(\S+).*?\((hw|sw)\)""")
|
||||||
|
private val AUDIO_ENCODER_TYPE_REGEX =
|
||||||
|
Regex("""--audio-codec=\S+\s+--audio-encoder=(\S+).*?\((hw|sw)\)""")
|
||||||
private val CAMERA_SIZE_REGEX = Regex("--camera-size=([0-9]+x[0-9]+)")
|
private val CAMERA_SIZE_REGEX = Regex("--camera-size=([0-9]+x[0-9]+)")
|
||||||
private val CAMERA_SIZE_FALLBACK_REGEX = Regex("\\b([1-9][0-9]{1,4}x[1-9][0-9]{1,4})\\b")
|
private val CAMERA_SIZE_FALLBACK_REGEX = Regex("\\b([1-9][0-9]{1,4}x[1-9][0-9]{1,4})\\b")
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package io.github.miuzarte.scrcpyforandroid.pages
|
package io.github.miuzarte.scrcpyforandroid.pages
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -12,6 +11,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusDirection
|
import androidx.compose.ui.focus.FocusDirection
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
@@ -22,11 +22,10 @@ import io.github.miuzarte.scrcpyforandroid.constants.ScrcpyPresets
|
|||||||
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
||||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn
|
import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn
|
||||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlide
|
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlide
|
||||||
import kotlin.math.roundToInt
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import top.yukonga.miuix.kmp.basic.Card
|
import top.yukonga.miuix.kmp.basic.Card
|
||||||
import top.yukonga.miuix.kmp.basic.SnackbarHostState
|
|
||||||
import top.yukonga.miuix.kmp.basic.ScrollBehavior
|
import top.yukonga.miuix.kmp.basic.ScrollBehavior
|
||||||
|
import top.yukonga.miuix.kmp.basic.SnackbarHostState
|
||||||
import top.yukonga.miuix.kmp.basic.SpinnerEntry
|
import top.yukonga.miuix.kmp.basic.SpinnerEntry
|
||||||
import top.yukonga.miuix.kmp.basic.Text
|
import top.yukonga.miuix.kmp.basic.Text
|
||||||
import top.yukonga.miuix.kmp.basic.TextField
|
import top.yukonga.miuix.kmp.basic.TextField
|
||||||
@@ -34,6 +33,7 @@ import top.yukonga.miuix.kmp.extra.SuperArrow
|
|||||||
import top.yukonga.miuix.kmp.extra.SuperDropdown
|
import top.yukonga.miuix.kmp.extra.SuperDropdown
|
||||||
import top.yukonga.miuix.kmp.extra.SuperSpinner
|
import top.yukonga.miuix.kmp.extra.SuperSpinner
|
||||||
import top.yukonga.miuix.kmp.extra.SuperSwitch
|
import top.yukonga.miuix.kmp.extra.SuperSwitch
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
private val AUDIO_SOURCE_OPTIONS = listOf(
|
private val AUDIO_SOURCE_OPTIONS = listOf(
|
||||||
"output" to "output",
|
"output" to "output",
|
||||||
@@ -142,15 +142,20 @@ internal fun AdvancedConfigPage(
|
|||||||
) {
|
) {
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val maxSizePresetIndex = presetIndexFromInputForAdvancedPage(maxSizeInput, ScrcpyPresets.MaxSize)
|
val maxSizePresetIndex =
|
||||||
|
presetIndexFromInputForAdvancedPage(maxSizeInput, ScrcpyPresets.MaxSize)
|
||||||
val maxFpsPresetIndex = presetIndexFromInputForAdvancedPage(maxFpsInput, ScrcpyPresets.MaxFPS)
|
val maxFpsPresetIndex = presetIndexFromInputForAdvancedPage(maxFpsInput, ScrcpyPresets.MaxFPS)
|
||||||
val audioSourceItems = AUDIO_SOURCE_OPTIONS.map { it.second }
|
val audioSourceItems = AUDIO_SOURCE_OPTIONS.map { it.second }
|
||||||
val audioSourceIndex = AUDIO_SOURCE_OPTIONS.indexOfFirst { it.first == audioSourcePreset }.let { if (it >= 0) it else 0 }
|
val audioSourceIndex = AUDIO_SOURCE_OPTIONS.indexOfFirst { it.first == audioSourcePreset }
|
||||||
|
.let { if (it >= 0) it else 0 }
|
||||||
val videoSourceItems = VIDEO_SOURCE_OPTIONS.map { it.second }
|
val videoSourceItems = VIDEO_SOURCE_OPTIONS.map { it.second }
|
||||||
val videoSourceIndex = VIDEO_SOURCE_OPTIONS.indexOfFirst { it.first == videoSourcePreset }.let { if (it >= 0) it else 0 }
|
val videoSourceIndex = VIDEO_SOURCE_OPTIONS.indexOfFirst { it.first == videoSourcePreset }
|
||||||
|
.let { if (it >= 0) it else 0 }
|
||||||
val cameraFacingItems = CAMERA_FACING_OPTIONS.map { it.second }
|
val cameraFacingItems = CAMERA_FACING_OPTIONS.map { it.second }
|
||||||
val cameraFacingIndex = CAMERA_FACING_OPTIONS.indexOfFirst { it.first == cameraFacingPreset }.let { if (it >= 0) it else 0 }
|
val cameraFacingIndex = CAMERA_FACING_OPTIONS.indexOfFirst { it.first == cameraFacingPreset }
|
||||||
val cameraFpsPresetIndex = presetIndexFromInputForAdvancedPage(cameraFpsInput, CAMERA_FPS_PRESETS)
|
.let { if (it >= 0) it else 0 }
|
||||||
|
val cameraFpsPresetIndex =
|
||||||
|
presetIndexFromInputForAdvancedPage(cameraFpsInput, CAMERA_FPS_PRESETS)
|
||||||
val videoEncoderEntries = videoEncoderDropdownItems.map { encoderName ->
|
val videoEncoderEntries = videoEncoderDropdownItems.map { encoderName ->
|
||||||
if (encoderName == "默认") {
|
if (encoderName == "默认") {
|
||||||
SpinnerEntry(title = encoderName)
|
SpinnerEntry(title = encoderName)
|
||||||
@@ -269,7 +274,10 @@ internal fun AdvancedConfigPage(
|
|||||||
title = "摄像头分辨率",
|
title = "摄像头分辨率",
|
||||||
summary = "--camera-size",
|
summary = "--camera-size",
|
||||||
items = cameraSizeDropdownItems,
|
items = cameraSizeDropdownItems,
|
||||||
selectedIndex = cameraSizeIndex.coerceIn(0, (cameraSizeDropdownItems.size - 1).coerceAtLeast(0)),
|
selectedIndex = cameraSizeIndex.coerceIn(
|
||||||
|
0,
|
||||||
|
(cameraSizeDropdownItems.size - 1).coerceAtLeast(0)
|
||||||
|
),
|
||||||
onSelectedIndexChange = { index ->
|
onSelectedIndexChange = { index ->
|
||||||
onCameraSizePresetChange(
|
onCameraSizePresetChange(
|
||||||
when (index) {
|
when (index) {
|
||||||
|
|||||||
@@ -5,56 +5,48 @@ import android.app.Activity
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.saveable.listSaver
|
import androidx.compose.runtime.saveable.listSaver
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.toMutableStateList
|
import androidx.compose.runtime.toMutableStateList
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.DragIndicator
|
|
||||||
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade
|
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade
|
||||||
import io.github.miuzarte.scrcpyforandroid.ScrcpySessionInfo
|
import io.github.miuzarte.scrcpyforandroid.ScrcpySessionInfo
|
||||||
import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults
|
import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults
|
||||||
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics
|
||||||
import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget
|
import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget
|
||||||
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
|
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
|
||||||
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics
|
import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.fetchConnectedDeviceInfo
|
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.DevicePageSettings
|
import io.github.miuzarte.scrcpyforandroid.services.DevicePageSettings
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.loadQuickDevices
|
import io.github.miuzarte.scrcpyforandroid.services.fetchConnectedDeviceInfo
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.loadDevicePageSettings
|
import io.github.miuzarte.scrcpyforandroid.services.loadDevicePageSettings
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.services.loadQuickDevices
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.parseQuickTarget
|
import io.github.miuzarte.scrcpyforandroid.services.parseQuickTarget
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.saveQuickDevices
|
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.saveDevicePageSettings
|
import io.github.miuzarte.scrcpyforandroid.services.saveDevicePageSettings
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.services.saveQuickDevices
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.updateQuickDeviceNameIfEmpty
|
import io.github.miuzarte.scrcpyforandroid.services.updateQuickDeviceNameIfEmpty
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.upsertQuickDevice
|
import io.github.miuzarte.scrcpyforandroid.services.upsertQuickDevice
|
||||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn
|
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.ConfigPanel
|
import io.github.miuzarte.scrcpyforandroid.widgets.ConfigPanel
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.PairingCard
|
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.DeviceEditorScreen
|
import io.github.miuzarte.scrcpyforandroid.widgets.DeviceEditorScreen
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.DeviceTile
|
import io.github.miuzarte.scrcpyforandroid.widgets.DeviceTile
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.LogsPanel
|
import io.github.miuzarte.scrcpyforandroid.widgets.LogsPanel
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.widgets.PairingCard
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.PreviewCard
|
import io.github.miuzarte.scrcpyforandroid.widgets.PreviewCard
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.QuickConnectCard
|
import io.github.miuzarte.scrcpyforandroid.widgets.QuickConnectCard
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.SectionSmallTitle
|
import io.github.miuzarte.scrcpyforandroid.widgets.SectionSmallTitle
|
||||||
@@ -66,9 +58,6 @@ import io.github.miuzarte.scrcpyforandroid.widgets.StatusCard
|
|||||||
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction
|
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions
|
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonCard
|
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonCard
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -77,11 +66,10 @@ import kotlinx.coroutines.withContext
|
|||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
import top.yukonga.miuix.kmp.basic.ScrollBehavior
|
import top.yukonga.miuix.kmp.basic.ScrollBehavior
|
||||||
import top.yukonga.miuix.kmp.basic.SnackbarHostState
|
import top.yukonga.miuix.kmp.basic.SnackbarHostState
|
||||||
import top.yukonga.miuix.kmp.basic.Card
|
|
||||||
import top.yukonga.miuix.kmp.basic.Icon
|
|
||||||
import top.yukonga.miuix.kmp.basic.Text
|
|
||||||
import top.yukonga.miuix.kmp.extra.SuperBottomSheet
|
import top.yukonga.miuix.kmp.extra.SuperBottomSheet
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
private const val ADB_CONNECT_TIMEOUT_MS = 3_000L
|
private const val ADB_CONNECT_TIMEOUT_MS = 3_000L
|
||||||
@@ -90,38 +78,40 @@ private const val ADB_KEEPALIVE_TIMEOUT_MS = 2_000L
|
|||||||
private const val DEVICE_SHORTCUT_SEPARATOR = "\u001F"
|
private const val DEVICE_SHORTCUT_SEPARATOR = "\u001F"
|
||||||
private const val LOG_TAG = "DevicePage"
|
private const val LOG_TAG = "DevicePage"
|
||||||
|
|
||||||
private val DeviceShortcutStateListSaver = listSaver<androidx.compose.runtime.snapshots.SnapshotStateList<DeviceShortcut>, String>(
|
private val DeviceShortcutStateListSaver =
|
||||||
save = { list ->
|
listSaver<androidx.compose.runtime.snapshots.SnapshotStateList<DeviceShortcut>, String>(
|
||||||
list.map { item ->
|
save = { list ->
|
||||||
listOf(
|
list.map { item ->
|
||||||
item.id,
|
listOf(
|
||||||
item.name,
|
item.id,
|
||||||
item.host,
|
item.name,
|
||||||
item.port.toString(),
|
item.host,
|
||||||
if (item.online) "1" else "0",
|
item.port.toString(),
|
||||||
).joinToString(DEVICE_SHORTCUT_SEPARATOR)
|
if (item.online) "1" else "0",
|
||||||
}
|
).joinToString(DEVICE_SHORTCUT_SEPARATOR)
|
||||||
},
|
}
|
||||||
restore = { saved ->
|
},
|
||||||
saved.mapNotNull { line ->
|
restore = { saved ->
|
||||||
val parts = line.split(DEVICE_SHORTCUT_SEPARATOR)
|
saved.mapNotNull { line ->
|
||||||
if (parts.size != 5) return@mapNotNull null
|
val parts = line.split(DEVICE_SHORTCUT_SEPARATOR)
|
||||||
val port = parts[3].toIntOrNull() ?: return@mapNotNull null
|
if (parts.size != 5) return@mapNotNull null
|
||||||
DeviceShortcut(
|
val port = parts[3].toIntOrNull() ?: return@mapNotNull null
|
||||||
id = parts[0],
|
DeviceShortcut(
|
||||||
name = parts[1],
|
id = parts[0],
|
||||||
host = parts[2],
|
name = parts[1],
|
||||||
port = port,
|
host = parts[2],
|
||||||
online = parts[4] == "1",
|
port = port,
|
||||||
)
|
online = parts[4] == "1",
|
||||||
}.toMutableStateList()
|
)
|
||||||
},
|
}.toMutableStateList()
|
||||||
)
|
},
|
||||||
|
)
|
||||||
|
|
||||||
private val StringStateListSaver = listSaver<androidx.compose.runtime.snapshots.SnapshotStateList<String>, String>(
|
private val StringStateListSaver =
|
||||||
save = { it.toList() },
|
listSaver<androidx.compose.runtime.snapshots.SnapshotStateList<String>, String>(
|
||||||
restore = { it.toMutableStateList() },
|
save = { it.toList() },
|
||||||
)
|
restore = { it.toMutableStateList() },
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DeviceTabScreen(
|
fun DeviceTabScreen(
|
||||||
@@ -231,7 +221,7 @@ fun DeviceTabScreen(
|
|||||||
var statusLine by rememberSaveable { mutableStateOf("未连接") }
|
var statusLine by rememberSaveable { mutableStateOf("未连接") }
|
||||||
var adbConnected by rememberSaveable { mutableStateOf(false) }
|
var adbConnected by rememberSaveable { mutableStateOf(false) }
|
||||||
var currentTargetHost by rememberSaveable { mutableStateOf("") }
|
var currentTargetHost by rememberSaveable { mutableStateOf("") }
|
||||||
var currentTargetPort by rememberSaveable { mutableIntStateOf(AppDefaults.DefaultAdbPort) }
|
var currentTargetPort by rememberSaveable { mutableIntStateOf(AppDefaults.ADB_PORT) }
|
||||||
var connectedDeviceLabel by rememberSaveable { mutableStateOf("未连接") }
|
var connectedDeviceLabel by rememberSaveable { mutableStateOf("未连接") }
|
||||||
var sessionInfoWidth by rememberSaveable { mutableIntStateOf(0) }
|
var sessionInfoWidth by rememberSaveable { mutableIntStateOf(0) }
|
||||||
var sessionInfoHeight by rememberSaveable { mutableIntStateOf(0) }
|
var sessionInfoHeight by rememberSaveable { mutableIntStateOf(0) }
|
||||||
@@ -241,7 +231,13 @@ fun DeviceTabScreen(
|
|||||||
var sessionInfo by remember {
|
var sessionInfo by remember {
|
||||||
mutableStateOf<ScrcpySessionInfo?>(null)
|
mutableStateOf<ScrcpySessionInfo?>(null)
|
||||||
}
|
}
|
||||||
LaunchedEffect(sessionInfoWidth, sessionInfoHeight, sessionInfoDeviceName, sessionInfoCodec, sessionInfoControlEnabled) {
|
LaunchedEffect(
|
||||||
|
sessionInfoWidth,
|
||||||
|
sessionInfoHeight,
|
||||||
|
sessionInfoDeviceName,
|
||||||
|
sessionInfoCodec,
|
||||||
|
sessionInfoControlEnabled
|
||||||
|
) {
|
||||||
sessionInfo = if (sessionInfoDeviceName.isNotBlank()) {
|
sessionInfo = if (sessionInfoDeviceName.isNotBlank()) {
|
||||||
ScrcpySessionInfo(
|
ScrcpySessionInfo(
|
||||||
width = sessionInfoWidth,
|
width = sessionInfoWidth,
|
||||||
@@ -261,18 +257,22 @@ fun DeviceTabScreen(
|
|||||||
var adbConnecting by rememberSaveable { mutableStateOf(false) }
|
var adbConnecting by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
var connectHost by rememberSaveable { mutableStateOf("") }
|
var connectHost by rememberSaveable { mutableStateOf("") }
|
||||||
var connectPort by rememberSaveable { mutableStateOf(AppDefaults.DefaultAdbPort.toString()) }
|
var connectPort by rememberSaveable { mutableStateOf(AppDefaults.ADB_PORT.toString()) }
|
||||||
var quickConnectInput by rememberSaveable { mutableStateOf(initialSettings.quickConnectInput) }
|
var quickConnectInput by rememberSaveable { mutableStateOf(initialSettings.quickConnectInput) }
|
||||||
var audioForwardingSupported by rememberSaveable { mutableStateOf(true) }
|
var audioForwardingSupported by rememberSaveable { mutableStateOf(true) }
|
||||||
var cameraMirroringSupported by rememberSaveable { mutableStateOf(true) }
|
var cameraMirroringSupported by rememberSaveable { mutableStateOf(true) }
|
||||||
|
|
||||||
var bitRateMbps by rememberSaveable { mutableFloatStateOf(initialSettings.bitRateMbps) }
|
var bitRateMbps by rememberSaveable { mutableFloatStateOf(initialSettings.videoBitRateMbps) }
|
||||||
var bitRateInput by rememberSaveable { mutableStateOf(initialSettings.bitRateInput) }
|
var bitRateInput by rememberSaveable { mutableStateOf(initialSettings.videoBitRateInput) }
|
||||||
var audioBitRateKbps by rememberSaveable { mutableIntStateOf(initialSettings.audioBitRateKbps) }
|
var audioBitRateKbps by rememberSaveable { mutableIntStateOf(initialSettings.audioBitRateKbps) }
|
||||||
val currentTarget = if (currentTargetHost.isNotBlank()) ConnectionTarget(currentTargetHost, currentTargetPort) else null
|
val currentTarget = if (currentTargetHost.isNotBlank()) ConnectionTarget(
|
||||||
|
currentTargetHost,
|
||||||
|
currentTargetPort
|
||||||
|
) else null
|
||||||
|
|
||||||
val eventLog = rememberSaveable(saver = StringStateListSaver) { mutableStateListOf() }
|
val eventLog = rememberSaveable(saver = StringStateListSaver) { mutableStateListOf() }
|
||||||
val quickDevices = rememberSaveable(saver = DeviceShortcutStateListSaver) { mutableStateListOf() }
|
val quickDevices =
|
||||||
|
rememberSaveable(saver = DeviceShortcutStateListSaver) { mutableStateListOf() }
|
||||||
|
|
||||||
LaunchedEffect(eventLog.size) {
|
LaunchedEffect(eventLog.size) {
|
||||||
onCanClearLogsChange(eventLog.isNotEmpty())
|
onCanClearLogsChange(eventLog.isNotEmpty())
|
||||||
@@ -282,13 +282,25 @@ fun DeviceTabScreen(
|
|||||||
val timestamp = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())
|
val timestamp = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())
|
||||||
val line = "[$timestamp] $message"
|
val line = "[$timestamp] $message"
|
||||||
eventLog.add(0, line)
|
eventLog.add(0, line)
|
||||||
if (eventLog.size > AppDefaults.MaxEventLogLines) {
|
if (eventLog.size > AppDefaults.EVENT_LOG_LINES) {
|
||||||
eventLog.removeRange(AppDefaults.MaxEventLogLines, eventLog.size)
|
eventLog.removeRange(AppDefaults.EVENT_LOG_LINES, eventLog.size)
|
||||||
}
|
}
|
||||||
when (level) {
|
when (level) {
|
||||||
Log.ERROR -> if (error != null) Log.e(LOG_TAG, message, error) else Log.e(LOG_TAG, message)
|
Log.ERROR -> if (error != null) Log.e(LOG_TAG, message, error) else Log.e(
|
||||||
Log.WARN -> if (error != null) Log.w(LOG_TAG, message, error) else Log.w(LOG_TAG, message)
|
LOG_TAG,
|
||||||
Log.DEBUG -> if (error != null) Log.d(LOG_TAG, message, error) else Log.d(LOG_TAG, message)
|
message
|
||||||
|
)
|
||||||
|
|
||||||
|
Log.WARN -> if (error != null) Log.w(LOG_TAG, message, error) else Log.w(
|
||||||
|
LOG_TAG,
|
||||||
|
message
|
||||||
|
)
|
||||||
|
|
||||||
|
Log.DEBUG -> if (error != null) Log.d(LOG_TAG, message, error) else Log.d(
|
||||||
|
LOG_TAG,
|
||||||
|
message
|
||||||
|
)
|
||||||
|
|
||||||
else -> if (error != null) Log.i(LOG_TAG, message, error) else Log.i(LOG_TAG, message)
|
else -> if (error != null) Log.i(LOG_TAG, message, error) else Log.i(LOG_TAG, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,13 +310,19 @@ fun DeviceTabScreen(
|
|||||||
audioForwardingSupported = audioSupported
|
audioForwardingSupported = audioSupported
|
||||||
if (!audioSupported && audioEnabled) {
|
if (!audioSupported && audioEnabled) {
|
||||||
onAudioEnabledChange(false)
|
onAudioEnabledChange(false)
|
||||||
logEvent("设备 Android ${release.ifBlank { "?" }} (SDK $sdkInt) 不支持音频转发,已自动关闭", Log.WARN)
|
logEvent(
|
||||||
|
"设备 Android ${release.ifBlank { "?" }} (SDK $sdkInt) 不支持音频转发,已自动关闭",
|
||||||
|
Log.WARN
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val cameraSupported = sdkInt !in 0..<31
|
val cameraSupported = sdkInt !in 0..<31
|
||||||
cameraMirroringSupported = cameraSupported
|
cameraMirroringSupported = cameraSupported
|
||||||
if (!cameraSupported && videoSourcePreset == "camera") {
|
if (!cameraSupported && videoSourcePreset == "camera") {
|
||||||
onVideoSourcePresetChange("display")
|
onVideoSourcePresetChange("display")
|
||||||
logEvent("设备 Android ${release.ifBlank { "?" }} (SDK $sdkInt) 不支持 camera mirroring,已切换为 display", Log.WARN)
|
logEvent(
|
||||||
|
"设备 Android ${release.ifBlank { "?" }} (SDK $sdkInt) 不支持 camera mirroring,已切换为 display",
|
||||||
|
Log.WARN
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +395,7 @@ fun DeviceTabScreen(
|
|||||||
|
|
||||||
fun refreshEncoderLists() {
|
fun refreshEncoderLists() {
|
||||||
if (!adbConnected) return
|
if (!adbConnected) return
|
||||||
val remotePath = serverRemotePath.trim().ifBlank { AppDefaults.DefaultServerRemotePath }
|
val remotePath = serverRemotePath.trim().ifBlank { AppDefaults.SERVER_REMOTE_PATH }
|
||||||
runCatching {
|
runCatching {
|
||||||
nativeCore.scrcpyListEncoders(
|
nativeCore.scrcpyListEncoders(
|
||||||
customServerUri = customServerUri,
|
customServerUri = customServerUri,
|
||||||
@@ -413,7 +431,7 @@ fun DeviceTabScreen(
|
|||||||
|
|
||||||
fun refreshCameraSizeLists() {
|
fun refreshCameraSizeLists() {
|
||||||
if (!adbConnected) return
|
if (!adbConnected) return
|
||||||
val remotePath = serverRemotePath.trim().ifBlank { AppDefaults.DefaultServerRemotePath }
|
val remotePath = serverRemotePath.trim().ifBlank { AppDefaults.SERVER_REMOTE_PATH }
|
||||||
runCatching {
|
runCatching {
|
||||||
nativeCore.scrcpyListCameraSizes(
|
nativeCore.scrcpyListCameraSizes(
|
||||||
customServerUri = customServerUri,
|
customServerUri = customServerUri,
|
||||||
@@ -470,20 +488,14 @@ fun DeviceTabScreen(
|
|||||||
|
|
||||||
LaunchedEffect(
|
LaunchedEffect(
|
||||||
quickConnectInput,
|
quickConnectInput,
|
||||||
|
audioBitRateKbps,
|
||||||
bitRateMbps,
|
bitRateMbps,
|
||||||
bitRateInput,
|
bitRateInput,
|
||||||
audioBitRateKbps,
|
turnScreenOff,
|
||||||
maxSizeInput,
|
|
||||||
maxFpsInput,
|
|
||||||
noControl,
|
noControl,
|
||||||
videoEncoder,
|
noVideo,
|
||||||
videoCodecOptions,
|
|
||||||
audioEncoder,
|
|
||||||
audioCodecOptions,
|
|
||||||
audioDup,
|
|
||||||
audioSourcePreset,
|
|
||||||
audioSourceCustom,
|
|
||||||
videoSourcePreset,
|
videoSourcePreset,
|
||||||
|
displayIdInput,
|
||||||
cameraIdInput,
|
cameraIdInput,
|
||||||
cameraFacingPreset,
|
cameraFacingPreset,
|
||||||
cameraSizePreset,
|
cameraSizePreset,
|
||||||
@@ -491,14 +503,20 @@ fun DeviceTabScreen(
|
|||||||
cameraArInput,
|
cameraArInput,
|
||||||
cameraFpsInput,
|
cameraFpsInput,
|
||||||
cameraHighSpeed,
|
cameraHighSpeed,
|
||||||
|
audioSourcePreset,
|
||||||
|
audioSourceCustom,
|
||||||
|
audioDup,
|
||||||
noAudioPlayback,
|
noAudioPlayback,
|
||||||
noVideo,
|
|
||||||
requireAudio,
|
requireAudio,
|
||||||
turnScreenOff,
|
maxSizeInput,
|
||||||
|
maxFpsInput,
|
||||||
|
videoEncoder,
|
||||||
|
videoCodecOptions,
|
||||||
|
audioEncoder,
|
||||||
|
audioCodecOptions,
|
||||||
newDisplayWidth,
|
newDisplayWidth,
|
||||||
newDisplayHeight,
|
newDisplayHeight,
|
||||||
newDisplayDpi,
|
newDisplayDpi,
|
||||||
displayIdInput,
|
|
||||||
cropWidth,
|
cropWidth,
|
||||||
cropHeight,
|
cropHeight,
|
||||||
cropX,
|
cropX,
|
||||||
@@ -508,35 +526,36 @@ fun DeviceTabScreen(
|
|||||||
context,
|
context,
|
||||||
DevicePageSettings(
|
DevicePageSettings(
|
||||||
quickConnectInput = quickConnectInput,
|
quickConnectInput = quickConnectInput,
|
||||||
bitRateMbps = bitRateMbps,
|
|
||||||
bitRateInput = bitRateInput,
|
|
||||||
audioBitRateKbps = audioBitRateKbps,
|
audioBitRateKbps = audioBitRateKbps,
|
||||||
maxSizeInput = maxSizeInput,
|
audioBitRateInput = audioBitRateKbps.toString(),
|
||||||
maxFpsInput = maxFpsInput,
|
videoBitRateMbps = bitRateMbps,
|
||||||
|
videoBitRateInput = bitRateInput,
|
||||||
|
turnScreenOff = turnScreenOff,
|
||||||
noControl = noControl,
|
noControl = noControl,
|
||||||
videoEncoder = videoEncoder,
|
noVideo = noVideo,
|
||||||
videoCodecOptions = videoCodecOptions,
|
|
||||||
audioEncoder = audioEncoder,
|
|
||||||
audioCodecOptions = audioCodecOptions,
|
|
||||||
audioDup = audioDup,
|
|
||||||
audioSourcePreset = audioSourcePreset,
|
|
||||||
audioSourceCustom = audioSourceCustom,
|
|
||||||
videoSourcePreset = videoSourcePreset,
|
videoSourcePreset = videoSourcePreset,
|
||||||
|
displayIdInput = displayIdInput,
|
||||||
cameraIdInput = cameraIdInput,
|
cameraIdInput = cameraIdInput,
|
||||||
cameraFacingPreset = cameraFacingPreset,
|
cameraFacingPreset = cameraFacingPreset,
|
||||||
cameraSizePreset = cameraSizePreset,
|
cameraSizePreset = cameraSizePreset,
|
||||||
cameraSizeCustom = cameraSizeCustom,
|
cameraSizeCustom = cameraSizeCustom,
|
||||||
cameraArInput = cameraArInput,
|
cameraAr = cameraArInput,
|
||||||
cameraFpsInput = cameraFpsInput,
|
cameraFps = cameraFpsInput,
|
||||||
cameraHighSpeed = cameraHighSpeed,
|
cameraHighSpeed = cameraHighSpeed,
|
||||||
|
audioSourcePreset = audioSourcePreset,
|
||||||
|
audioSourceCustom = audioSourceCustom,
|
||||||
|
audioDup = audioDup,
|
||||||
noAudioPlayback = noAudioPlayback,
|
noAudioPlayback = noAudioPlayback,
|
||||||
noVideo = noVideo,
|
|
||||||
requireAudio = requireAudio,
|
requireAudio = requireAudio,
|
||||||
turnScreenOff = turnScreenOff,
|
maxSizeInput = maxSizeInput,
|
||||||
|
maxFpsInput = maxFpsInput,
|
||||||
|
videoEncoder = videoEncoder,
|
||||||
|
videoCodecOptions = videoCodecOptions,
|
||||||
|
audioEncoder = audioEncoder,
|
||||||
|
audioCodecOptions = audioCodecOptions,
|
||||||
newDisplayWidth = newDisplayWidth,
|
newDisplayWidth = newDisplayWidth,
|
||||||
newDisplayHeight = newDisplayHeight,
|
newDisplayHeight = newDisplayHeight,
|
||||||
newDisplayDpi = newDisplayDpi,
|
newDisplayDpi = newDisplayDpi,
|
||||||
displayIdInput = displayIdInput,
|
|
||||||
cropWidth = cropWidth,
|
cropWidth = cropWidth,
|
||||||
cropHeight = cropHeight,
|
cropHeight = cropHeight,
|
||||||
cropX = cropX,
|
cropX = cropX,
|
||||||
@@ -747,7 +766,8 @@ fun DeviceTabScreen(
|
|||||||
itemsIndexed(quickDevices, key = { _, device -> device.id }) { _, device ->
|
itemsIndexed(quickDevices, key = { _, device -> device.id }) { _, device ->
|
||||||
val host = device.host
|
val host = device.host
|
||||||
val port = device.port
|
val port = device.port
|
||||||
val isConnectedTarget = adbConnected && currentTarget?.host == host && currentTarget.port == port
|
val isConnectedTarget =
|
||||||
|
adbConnected && currentTarget?.host == host && currentTarget.port == port
|
||||||
|
|
||||||
DeviceTile(
|
DeviceTile(
|
||||||
device = device,
|
device = device,
|
||||||
@@ -768,7 +788,7 @@ fun DeviceTabScreen(
|
|||||||
nativeCore.adbDisconnect()
|
nativeCore.adbDisconnect()
|
||||||
adbConnected = false
|
adbConnected = false
|
||||||
currentTargetHost = ""
|
currentTargetHost = ""
|
||||||
currentTargetPort = AppDefaults.DefaultAdbPort
|
currentTargetPort = AppDefaults.ADB_PORT
|
||||||
audioForwardingSupported = true
|
audioForwardingSupported = true
|
||||||
cameraMirroringSupported = true
|
cameraMirroringSupported = true
|
||||||
sessionInfo = null
|
sessionInfo = null
|
||||||
@@ -809,7 +829,13 @@ fun DeviceTabScreen(
|
|||||||
enabled = !adbConnecting,
|
enabled = !adbConnecting,
|
||||||
onAddDevice = {
|
onAddDevice = {
|
||||||
val target = parseQuickTarget(quickConnectInput) ?: return@QuickConnectCard
|
val target = parseQuickTarget(quickConnectInput) ?: return@QuickConnectCard
|
||||||
upsertQuickDevice(context, quickDevices, target.host, target.port, online = false)
|
upsertQuickDevice(
|
||||||
|
context,
|
||||||
|
quickDevices,
|
||||||
|
target.host,
|
||||||
|
target.port,
|
||||||
|
online = false
|
||||||
|
)
|
||||||
scope.launch {
|
scope.launch {
|
||||||
snack.showSnackbar("已添加设备: ${target.host}:${target.port}")
|
snack.showSnackbar("已添加设备: ${target.host}:${target.port}")
|
||||||
}
|
}
|
||||||
@@ -840,10 +866,13 @@ fun DeviceTabScreen(
|
|||||||
runBusy("执行配对") {
|
runBusy("执行配对") {
|
||||||
val ok = nativeCore.adbPair(
|
val ok = nativeCore.adbPair(
|
||||||
host.trim(),
|
host.trim(),
|
||||||
port.toIntOrNull() ?: AppDefaults.DefaultAdbPort,
|
port.toIntOrNull() ?: AppDefaults.ADB_PORT,
|
||||||
code.trim(),
|
code.trim(),
|
||||||
)
|
)
|
||||||
logEvent(if (ok) "配对成功" else "配对失败", if (ok) Log.INFO else Log.ERROR)
|
logEvent(
|
||||||
|
if (ok) "配对成功" else "配对失败",
|
||||||
|
if (ok) Log.INFO else Log.ERROR
|
||||||
|
)
|
||||||
scope.launch {
|
scope.launch {
|
||||||
snack.showSnackbar(if (ok) "配对成功" else "配对失败")
|
snack.showSnackbar(if (ok) "配对成功" else "配对失败")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package io.github.miuzarte.scrcpyforandroid.pages
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -21,8 +21,8 @@ import androidx.core.view.WindowInsetsCompat
|
|||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade
|
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade
|
||||||
import io.github.miuzarte.scrcpyforandroid.ScrcpySessionInfo
|
import io.github.miuzarte.scrcpyforandroid.ScrcpySessionInfo
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.FullscreenControlScreen
|
|
||||||
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics
|
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.widgets.FullscreenControlScreen
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions
|
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonBar
|
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonBar
|
||||||
import top.yukonga.miuix.kmp.basic.Scaffold
|
import top.yukonga.miuix.kmp.basic.Scaffold
|
||||||
@@ -56,7 +56,9 @@ fun FullscreenControlPage(
|
|||||||
moreActions = virtualButtonLayout.second,
|
moreActions = virtualButtonLayout.second,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val initialOrientation = remember(activity) { activity?.requestedOrientation ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED }
|
val initialOrientation = remember(activity) {
|
||||||
|
activity?.requestedOrientation ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||||
|
}
|
||||||
var session by remember(launch) {
|
var session by remember(launch) {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
ScrcpySessionInfo(
|
ScrcpySessionInfo(
|
||||||
@@ -88,12 +90,15 @@ fun FullscreenControlPage(
|
|||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
val controller = WindowInsetsControllerCompat(window, window.decorView)
|
val controller = WindowInsetsControllerCompat(window, window.decorView)
|
||||||
controller.hide(WindowInsetsCompat.Type.systemBars())
|
controller.hide(WindowInsetsCompat.Type.systemBars())
|
||||||
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
controller.systemBarsBehavior =
|
||||||
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
}
|
}
|
||||||
onDispose {
|
onDispose {
|
||||||
val restoreWindow = activity?.window
|
val restoreWindow = activity?.window
|
||||||
if (restoreWindow != null) {
|
if (restoreWindow != null) {
|
||||||
WindowInsetsControllerCompat(restoreWindow, restoreWindow.decorView).show(WindowInsetsCompat.Type.systemBars())
|
WindowInsetsControllerCompat(restoreWindow, restoreWindow.decorView).show(
|
||||||
|
WindowInsetsCompat.Type.systemBars()
|
||||||
|
)
|
||||||
WindowCompat.setDecorFitsSystemWindows(restoreWindow, true)
|
WindowCompat.setDecorFitsSystemWindows(restoreWindow, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import androidx.activity.compose.PredictiveBackHandler
|
|||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.core.spring
|
import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
@@ -34,7 +34,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.navigation3.runtime.NavKey
|
import androidx.navigation3.runtime.NavKey
|
||||||
import androidx.navigation3.runtime.entryProvider
|
import androidx.navigation3.runtime.entryProvider
|
||||||
import androidx.navigation3.runtime.rememberDecoratedNavEntries
|
import androidx.navigation3.runtime.rememberDecoratedNavEntries
|
||||||
@@ -92,14 +91,18 @@ fun MainPage() {
|
|||||||
val initialDeviceSettings = remember(context) { loadDevicePageSettings(context) }
|
val initialDeviceSettings = remember(context) { loadDevicePageSettings(context) }
|
||||||
val snackHostState = remember { SnackbarHostState() }
|
val snackHostState = remember { SnackbarHostState() }
|
||||||
val tabs = remember { MainTabDestination.entries }
|
val tabs = remember { MainTabDestination.entries }
|
||||||
val pagerState = rememberPagerState(initialPage = MainTabDestination.Device.ordinal, pageCount = { tabs.size })
|
val pagerState = rememberPagerState(
|
||||||
|
initialPage = MainTabDestination.Device.ordinal,
|
||||||
|
pageCount = { tabs.size })
|
||||||
val currentTab = tabs[pagerState.currentPage]
|
val currentTab = tabs[pagerState.currentPage]
|
||||||
val saveableStateHolder = rememberSaveableStateHolder()
|
val saveableStateHolder = rememberSaveableStateHolder()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val rootBackStack = remember { mutableStateListOf<NavKey>(RootScreen.Home) }
|
val rootBackStack = remember { mutableStateListOf<NavKey>(RootScreen.Home) }
|
||||||
val currentRootScreen = rootBackStack.lastOrNull() as? RootScreen ?: RootScreen.Home
|
val currentRootScreen = rootBackStack.lastOrNull() as? RootScreen ?: RootScreen.Home
|
||||||
val deviceScrollBehavior = MiuixScrollBehavior(canScroll = { currentTab == MainTabDestination.Device })
|
val deviceScrollBehavior =
|
||||||
val settingsScrollBehavior = MiuixScrollBehavior(canScroll = { currentTab == MainTabDestination.Settings })
|
MiuixScrollBehavior(canScroll = { currentTab == MainTabDestination.Device })
|
||||||
|
val settingsScrollBehavior =
|
||||||
|
MiuixScrollBehavior(canScroll = { currentTab == MainTabDestination.Settings })
|
||||||
val advancedScrollBehavior = MiuixScrollBehavior(
|
val advancedScrollBehavior = MiuixScrollBehavior(
|
||||||
canScroll = {
|
canScroll = {
|
||||||
currentRootScreen is RootScreen.Advanced || currentRootScreen is RootScreen.VirtualButtonOrder
|
currentRootScreen is RootScreen.Advanced || currentRootScreen is RootScreen.VirtualButtonOrder
|
||||||
@@ -110,27 +113,23 @@ fun MainPage() {
|
|||||||
restore = { restored -> restored.toList() },
|
restore = { restored -> restored.toList() },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var audioEnabled by rememberSaveable { mutableStateOf(initialSettings.audioEnabled) }
|
||||||
|
var audioCodec by rememberSaveable { mutableStateOf(initialSettings.audioCodec) }
|
||||||
|
var videoCodec by rememberSaveable { mutableStateOf(initialSettings.videoCodec) }
|
||||||
var themeBaseIndex by rememberSaveable { mutableIntStateOf(initialSettings.themeBaseIndex) }
|
var themeBaseIndex by rememberSaveable { mutableIntStateOf(initialSettings.themeBaseIndex) }
|
||||||
var monetEnabled by rememberSaveable { mutableStateOf(initialSettings.monetEnabled) }
|
var monetEnabled by rememberSaveable { mutableStateOf(initialSettings.monetEnabled) }
|
||||||
var fullscreenDebugInfoEnabled by rememberSaveable { mutableStateOf(initialSettings.fullscreenDebugInfoEnabled) }
|
var fullscreenDebugInfoEnabled by rememberSaveable { mutableStateOf(initialSettings.fullscreenDebugInfoEnabled) }
|
||||||
var showFullscreenVirtualButtons by rememberSaveable {
|
var showFullscreenVirtualButtons by rememberSaveable { mutableStateOf(initialSettings.showFullscreenVirtualButtons) }
|
||||||
mutableStateOf(initialSettings.showFullscreenVirtualButtons)
|
var keepScreenOnWhenStreamingEnabled by rememberSaveable { mutableStateOf(initialSettings.keepScreenOnWhenStreamingEnabled) }
|
||||||
}
|
var devicePreviewCardHeightDp by rememberSaveable { mutableIntStateOf(initialSettings.devicePreviewCardHeightDp) }
|
||||||
var keepScreenOnWhenStreamingEnabled by rememberSaveable {
|
|
||||||
mutableStateOf(initialSettings.keepScreenOnWhenStreamingEnabled)
|
|
||||||
}
|
|
||||||
var virtualButtonsOutside by rememberSaveable(stateSaver = stringListSaver) {
|
var virtualButtonsOutside by rememberSaveable(stateSaver = stringListSaver) {
|
||||||
mutableStateOf(VirtualButtonActions.parseStoredIds(initialSettings.virtualButtonsOutside))
|
mutableStateOf(VirtualButtonActions.parseStoredIds(initialSettings.virtualButtonsOutside))
|
||||||
}
|
}
|
||||||
var virtualButtonsInMore by rememberSaveable(stateSaver = stringListSaver) {
|
var virtualButtonsInMore by rememberSaveable(stateSaver = stringListSaver) {
|
||||||
mutableStateOf(VirtualButtonActions.parseStoredIds(initialSettings.virtualButtonsInMore))
|
mutableStateOf(VirtualButtonActions.parseStoredIds(initialSettings.virtualButtonsInMore))
|
||||||
}
|
}
|
||||||
var devicePreviewCardHeightDp by rememberSaveable { mutableIntStateOf(initialSettings.devicePreviewCardHeightDp) }
|
|
||||||
var customServerUri by rememberSaveable { mutableStateOf(initialSettings.customServerUri) }
|
var customServerUri by rememberSaveable { mutableStateOf(initialSettings.customServerUri) }
|
||||||
var serverRemotePath by rememberSaveable { mutableStateOf(initialSettings.serverRemotePath) }
|
var serverRemotePath by rememberSaveable { mutableStateOf(initialSettings.serverRemotePath) }
|
||||||
var videoCodec by rememberSaveable { mutableStateOf(initialSettings.videoCodec) }
|
|
||||||
var audioEnabled by rememberSaveable { mutableStateOf(initialSettings.audioEnabled) }
|
|
||||||
var audioCodec by rememberSaveable { mutableStateOf(initialSettings.audioCodec) }
|
|
||||||
var adbKeyName by rememberSaveable { mutableStateOf(initialSettings.adbKeyName) }
|
var adbKeyName by rememberSaveable { mutableStateOf(initialSettings.adbKeyName) }
|
||||||
var noControl by rememberSaveable { mutableStateOf(initialDeviceSettings.noControl) }
|
var noControl by rememberSaveable { mutableStateOf(initialDeviceSettings.noControl) }
|
||||||
var videoEncoder by rememberSaveable { mutableStateOf(initialDeviceSettings.videoEncoder) }
|
var videoEncoder by rememberSaveable { mutableStateOf(initialDeviceSettings.videoEncoder) }
|
||||||
@@ -145,8 +144,8 @@ fun MainPage() {
|
|||||||
var cameraFacingPreset by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraFacingPreset) }
|
var cameraFacingPreset by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraFacingPreset) }
|
||||||
var cameraSizePreset by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraSizePreset) }
|
var cameraSizePreset by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraSizePreset) }
|
||||||
var cameraSizeCustom by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraSizeCustom) }
|
var cameraSizeCustom by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraSizeCustom) }
|
||||||
var cameraArInput by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraArInput) }
|
var cameraArInput by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraAr) }
|
||||||
var cameraFpsInput by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraFpsInput) }
|
var cameraFpsInput by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraFps) }
|
||||||
var cameraHighSpeed by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraHighSpeed) }
|
var cameraHighSpeed by rememberSaveable { mutableStateOf(initialDeviceSettings.cameraHighSpeed) }
|
||||||
var noAudioPlayback by rememberSaveable { mutableStateOf(initialDeviceSettings.noAudioPlayback) }
|
var noAudioPlayback by rememberSaveable { mutableStateOf(initialDeviceSettings.noAudioPlayback) }
|
||||||
var noVideo by rememberSaveable { mutableStateOf(initialDeviceSettings.noVideo) }
|
var noVideo by rememberSaveable { mutableStateOf(initialDeviceSettings.noVideo) }
|
||||||
@@ -178,47 +177,48 @@ fun MainPage() {
|
|||||||
val themeController = remember(themeMode) { ThemeController(colorSchemeMode = themeMode) }
|
val themeController = remember(themeMode) { ThemeController(colorSchemeMode = themeMode) }
|
||||||
|
|
||||||
LaunchedEffect(
|
LaunchedEffect(
|
||||||
|
audioEnabled,
|
||||||
|
audioCodec,
|
||||||
|
videoCodec,
|
||||||
themeBaseIndex,
|
themeBaseIndex,
|
||||||
monetEnabled,
|
monetEnabled,
|
||||||
fullscreenDebugInfoEnabled,
|
fullscreenDebugInfoEnabled,
|
||||||
showFullscreenVirtualButtons,
|
showFullscreenVirtualButtons,
|
||||||
keepScreenOnWhenStreamingEnabled,
|
keepScreenOnWhenStreamingEnabled,
|
||||||
|
devicePreviewCardHeightDp,
|
||||||
virtualButtonsOutside,
|
virtualButtonsOutside,
|
||||||
virtualButtonsInMore,
|
virtualButtonsInMore,
|
||||||
devicePreviewCardHeightDp,
|
|
||||||
customServerUri,
|
customServerUri,
|
||||||
serverRemotePath,
|
serverRemotePath,
|
||||||
videoCodec,
|
|
||||||
audioEnabled,
|
|
||||||
audioCodec,
|
|
||||||
adbKeyName,
|
adbKeyName,
|
||||||
) {
|
) {
|
||||||
saveMainSettings(
|
saveMainSettings(
|
||||||
context,
|
context,
|
||||||
MainSettings(
|
MainSettings(
|
||||||
|
audioEnabled = audioEnabled,
|
||||||
|
audioCodec = audioCodec,
|
||||||
|
videoCodec = videoCodec,
|
||||||
themeBaseIndex = themeBaseIndex,
|
themeBaseIndex = themeBaseIndex,
|
||||||
monetEnabled = monetEnabled,
|
monetEnabled = monetEnabled,
|
||||||
fullscreenDebugInfoEnabled = fullscreenDebugInfoEnabled,
|
fullscreenDebugInfoEnabled = fullscreenDebugInfoEnabled,
|
||||||
showFullscreenVirtualButtons = showFullscreenVirtualButtons,
|
showFullscreenVirtualButtons = showFullscreenVirtualButtons,
|
||||||
keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled,
|
keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled,
|
||||||
|
devicePreviewCardHeightDp = devicePreviewCardHeightDp,
|
||||||
virtualButtonsOutside = VirtualButtonActions.encodeStoredIds(virtualButtonsOutside),
|
virtualButtonsOutside = VirtualButtonActions.encodeStoredIds(virtualButtonsOutside),
|
||||||
virtualButtonsInMore = VirtualButtonActions.encodeStoredIds(virtualButtonsInMore),
|
virtualButtonsInMore = VirtualButtonActions.encodeStoredIds(virtualButtonsInMore),
|
||||||
devicePreviewCardHeightDp = devicePreviewCardHeightDp,
|
|
||||||
customServerUri = customServerUri,
|
customServerUri = customServerUri,
|
||||||
serverRemotePath = serverRemotePath,
|
serverRemotePath = serverRemotePath,
|
||||||
videoCodec = videoCodec,
|
|
||||||
audioEnabled = audioEnabled,
|
|
||||||
audioCodec = audioCodec,
|
|
||||||
adbKeyName = adbKeyName,
|
adbKeyName = adbKeyName,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(adbKeyName) {
|
LaunchedEffect(adbKeyName) {
|
||||||
nativeCore.setAdbKeyName(adbKeyName.ifBlank { AppDefaults.DefaultAdbKeyName })
|
nativeCore.setAdbKeyName(adbKeyName.ifBlank { AppDefaults.ADB_KEY_NAME })
|
||||||
}
|
}
|
||||||
|
|
||||||
val canNavigateBack = rootBackStack.size > 1 || pagerState.currentPage != MainTabDestination.Device.ordinal
|
val canNavigateBack =
|
||||||
|
rootBackStack.size > 1 || pagerState.currentPage != MainTabDestination.Device.ordinal
|
||||||
|
|
||||||
fun popRoot() {
|
fun popRoot() {
|
||||||
if (rootBackStack.size > 1) {
|
if (rootBackStack.size > 1) {
|
||||||
@@ -234,8 +234,8 @@ fun MainPage() {
|
|||||||
pagerState.animateScrollToPage(
|
pagerState.animateScrollToPage(
|
||||||
page = MainTabDestination.Device.ordinal,
|
page = MainTabDestination.Device.ordinal,
|
||||||
animationSpec = spring(
|
animationSpec = spring(
|
||||||
dampingRatio = UiMotion.PageSwitchDampingRatio,
|
dampingRatio = UiMotion.PAGE_SWITCH_DAMPING_RATIO,
|
||||||
stiffness = UiMotion.PageSwitchStiffness,
|
stiffness = UiMotion.PAGE_SWITCH_STIFFNESS,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -251,403 +251,424 @@ fun MainPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val picker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? ->
|
val picker =
|
||||||
if (uri == null) return@rememberLauncherForActivityResult
|
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? ->
|
||||||
runCatching {
|
if (uri == null) return@rememberLauncherForActivityResult
|
||||||
context.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
runCatching {
|
||||||
|
context.contentResolver.takePersistableUriPermission(
|
||||||
|
uri,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
customServerUri = uri.toString()
|
||||||
}
|
}
|
||||||
customServerUri = uri.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
val rootEntryProvider = entryProvider<NavKey> {
|
val rootEntryProvider = entryProvider<NavKey> {
|
||||||
entry(RootScreen.Home) {
|
entry(RootScreen.Home) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
NavigationBar {
|
NavigationBar {
|
||||||
tabs.forEach { tab ->
|
tabs.forEach { tab ->
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = currentTab == tab,
|
selected = currentTab == tab,
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
pagerState.animateScrollToPage(
|
pagerState.animateScrollToPage(
|
||||||
page = tab.ordinal,
|
page = tab.ordinal,
|
||||||
animationSpec = spring(
|
animationSpec = spring(
|
||||||
dampingRatio = UiMotion.PageSwitchDampingRatio,
|
dampingRatio = UiMotion.PAGE_SWITCH_DAMPING_RATIO,
|
||||||
stiffness = UiMotion.PageSwitchStiffness,
|
stiffness = UiMotion.PAGE_SWITCH_STIFFNESS,
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon = tab.icon,
|
||||||
|
label = tab.label,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
snackbarHost = { SnackbarHost(snackHostState) },
|
||||||
|
) { contentPadding ->
|
||||||
|
HorizontalPager(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(bottom = contentPadding.calculateBottomPadding()),
|
||||||
|
state = pagerState,
|
||||||
|
beyondViewportPageCount = 1,
|
||||||
|
) { page ->
|
||||||
|
val tab = tabs[page]
|
||||||
|
saveableStateHolder.SaveableStateProvider(tab.name) {
|
||||||
|
when (tab) {
|
||||||
|
MainTabDestination.Device -> Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = tab.title,
|
||||||
|
actions = {
|
||||||
|
IconButton(
|
||||||
|
onClick = { showDeviceMenu = true },
|
||||||
|
holdDownState = showDeviceMenu,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.MoreVert,
|
||||||
|
contentDescription = "更多"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DeviceMenuPopup(
|
||||||
|
show = showDeviceMenu,
|
||||||
|
canClearLogs = canClearLogs,
|
||||||
|
onDismissRequest = { showDeviceMenu = false },
|
||||||
|
onReorderDevices = {
|
||||||
|
openReorderDevicesAction?.invoke()
|
||||||
|
showDeviceMenu = false
|
||||||
|
},
|
||||||
|
onOpenVirtualButtonOrder = {
|
||||||
|
rootBackStack.add(RootScreen.VirtualButtonOrder)
|
||||||
|
showDeviceMenu = false
|
||||||
|
},
|
||||||
|
onClearLogs = {
|
||||||
|
clearLogsAction?.invoke()
|
||||||
|
showDeviceMenu = false
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
scrollBehavior = deviceScrollBehavior,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { pagePadding ->
|
||||||
|
DeviceTabScreen(
|
||||||
|
contentPadding = pagePadding,
|
||||||
|
nativeCore = nativeCore,
|
||||||
|
snack = snackHostState,
|
||||||
|
scrollBehavior = deviceScrollBehavior,
|
||||||
|
keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled,
|
||||||
|
virtualButtonsOutside = virtualButtonsOutside,
|
||||||
|
virtualButtonsInMore = virtualButtonsInMore,
|
||||||
|
customServerUri = customServerUri,
|
||||||
|
serverRemotePath = serverRemotePath,
|
||||||
|
onServerRemotePathChange = { serverRemotePath = it },
|
||||||
|
videoCodec = videoCodec,
|
||||||
|
onVideoCodecChange = { videoCodec = it },
|
||||||
|
audioEnabled = audioEnabled,
|
||||||
|
onAudioEnabledChange = { audioEnabled = it },
|
||||||
|
audioCodec = audioCodec,
|
||||||
|
onAudioCodecChange = { audioCodec = it },
|
||||||
|
noControl = noControl,
|
||||||
|
onNoControlChange = {
|
||||||
|
noControl = it
|
||||||
|
if (it) {
|
||||||
|
turnScreenOff = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon = tab.icon,
|
videoEncoder = videoEncoder,
|
||||||
label = tab.label,
|
onVideoEncoderChange = { videoEncoder = it },
|
||||||
|
videoCodecOptions = videoCodecOptions,
|
||||||
|
onVideoCodecOptionsChange = { videoCodecOptions = it },
|
||||||
|
audioEncoder = audioEncoder,
|
||||||
|
onAudioEncoderChange = { audioEncoder = it },
|
||||||
|
audioCodecOptions = audioCodecOptions,
|
||||||
|
onAudioCodecOptionsChange = { audioCodecOptions = it },
|
||||||
|
audioDup = audioDup,
|
||||||
|
onAudioDupChange = { audioDup = it },
|
||||||
|
audioSourcePreset = audioSourcePreset,
|
||||||
|
onAudioSourcePresetChange = { audioSourcePreset = it },
|
||||||
|
audioSourceCustom = audioSourceCustom,
|
||||||
|
onAudioSourceCustomChange = { audioSourceCustom = it },
|
||||||
|
videoSourcePreset = videoSourcePreset,
|
||||||
|
onVideoSourcePresetChange = { videoSourcePreset = it },
|
||||||
|
cameraIdInput = cameraIdInput,
|
||||||
|
onCameraIdInputChange = { cameraIdInput = it },
|
||||||
|
cameraFacingPreset = cameraFacingPreset,
|
||||||
|
onCameraFacingPresetChange = { cameraFacingPreset = it },
|
||||||
|
cameraSizePreset = cameraSizePreset,
|
||||||
|
onCameraSizePresetChange = { cameraSizePreset = it },
|
||||||
|
cameraSizeCustom = cameraSizeCustom,
|
||||||
|
onCameraSizeCustomChange = { cameraSizeCustom = it },
|
||||||
|
cameraArInput = cameraArInput,
|
||||||
|
onCameraArInputChange = { cameraArInput = it },
|
||||||
|
cameraFpsInput = cameraFpsInput,
|
||||||
|
onCameraFpsInputChange = { cameraFpsInput = it },
|
||||||
|
cameraHighSpeed = cameraHighSpeed,
|
||||||
|
onCameraHighSpeedChange = { cameraHighSpeed = it },
|
||||||
|
noAudioPlayback = noAudioPlayback,
|
||||||
|
onNoAudioPlaybackChange = { noAudioPlayback = it },
|
||||||
|
noVideo = noVideo,
|
||||||
|
requireAudio = requireAudio,
|
||||||
|
onRequireAudioChange = { requireAudio = it },
|
||||||
|
turnScreenOff = turnScreenOff,
|
||||||
|
onTurnScreenOffChange = { turnScreenOff = it },
|
||||||
|
maxSizeInput = maxSizeInput,
|
||||||
|
onMaxSizeInputChange = { maxSizeInput = it },
|
||||||
|
maxFpsInput = maxFpsInput,
|
||||||
|
onMaxFpsInputChange = { maxFpsInput = it },
|
||||||
|
newDisplayWidth = newDisplayWidth,
|
||||||
|
onNewDisplayWidthChange = { newDisplayWidth = it },
|
||||||
|
newDisplayHeight = newDisplayHeight,
|
||||||
|
onNewDisplayHeightChange = { newDisplayHeight = it },
|
||||||
|
newDisplayDpi = newDisplayDpi,
|
||||||
|
onNewDisplayDpiChange = { newDisplayDpi = it },
|
||||||
|
displayIdInput = displayIdInput,
|
||||||
|
onDisplayIdInputChange = { displayIdInput = it },
|
||||||
|
cropWidth = cropWidth,
|
||||||
|
onCropWidthChange = { cropWidth = it },
|
||||||
|
cropHeight = cropHeight,
|
||||||
|
onCropHeightChange = { cropHeight = it },
|
||||||
|
cropX = cropX,
|
||||||
|
onCropXChange = { cropX = it },
|
||||||
|
cropY = cropY,
|
||||||
|
onCropYChange = { cropY = it },
|
||||||
|
videoEncoderOptions = videoEncoderOptions,
|
||||||
|
onVideoEncoderOptionsChange = {
|
||||||
|
videoEncoderOptions.clear()
|
||||||
|
videoEncoderOptions.addAll(it)
|
||||||
|
},
|
||||||
|
onVideoEncoderTypeMapChange = {
|
||||||
|
videoEncoderTypeMap.clear()
|
||||||
|
videoEncoderTypeMap.putAll(it)
|
||||||
|
},
|
||||||
|
audioEncoderOptions = audioEncoderOptions,
|
||||||
|
onAudioEncoderOptionsChange = {
|
||||||
|
audioEncoderOptions.clear()
|
||||||
|
audioEncoderOptions.addAll(it)
|
||||||
|
},
|
||||||
|
onAudioEncoderTypeMapChange = {
|
||||||
|
audioEncoderTypeMap.clear()
|
||||||
|
audioEncoderTypeMap.putAll(it)
|
||||||
|
},
|
||||||
|
cameraSizeOptions = cameraSizeOptions,
|
||||||
|
onCameraSizeOptionsChange = {
|
||||||
|
cameraSizeOptions.clear()
|
||||||
|
cameraSizeOptions.addAll(it)
|
||||||
|
},
|
||||||
|
onSessionStartedChange = { sessionStarted = it },
|
||||||
|
onRefreshEncodersActionChange = { refreshEncodersAction = it },
|
||||||
|
onRefreshCameraSizesActionChange = {
|
||||||
|
refreshCameraSizesAction = it
|
||||||
|
},
|
||||||
|
onClearLogsActionChange = { clearLogsAction = it },
|
||||||
|
onCanClearLogsChange = { canClearLogs = it },
|
||||||
|
onOpenReorderDevicesActionChange = {
|
||||||
|
openReorderDevicesAction = it
|
||||||
|
},
|
||||||
|
onOpenAdvancedPage = { rootBackStack.add(RootScreen.Advanced) },
|
||||||
|
onOpenFullscreenPage = { session ->
|
||||||
|
rootBackStack.add(
|
||||||
|
RootScreen.Fullscreen(
|
||||||
|
launch = FullscreenControlLaunch(
|
||||||
|
deviceName = session.deviceName,
|
||||||
|
width = session.width,
|
||||||
|
height = session.height,
|
||||||
|
codec = session.codec,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
previewCardHeightDp = devicePreviewCardHeightDp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
snackbarHost = { SnackbarHost(snackHostState) },
|
|
||||||
) { contentPadding ->
|
|
||||||
HorizontalPager(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(bottom = contentPadding.calculateBottomPadding()),
|
|
||||||
state = pagerState,
|
|
||||||
beyondViewportPageCount = 1,
|
|
||||||
) { page ->
|
|
||||||
val tab = tabs[page]
|
|
||||||
saveableStateHolder.SaveableStateProvider(tab.name) {
|
|
||||||
when (tab) {
|
|
||||||
MainTabDestination.Device -> Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopAppBar(
|
|
||||||
title = tab.title,
|
|
||||||
actions = {
|
|
||||||
IconButton(
|
|
||||||
onClick = { showDeviceMenu = true },
|
|
||||||
holdDownState = showDeviceMenu,
|
|
||||||
) {
|
|
||||||
Icon(Icons.Default.MoreVert, contentDescription = "更多")
|
|
||||||
}
|
|
||||||
DeviceMenuPopup(
|
|
||||||
show = showDeviceMenu,
|
|
||||||
canClearLogs = canClearLogs,
|
|
||||||
onDismissRequest = { showDeviceMenu = false },
|
|
||||||
onReorderDevices = {
|
|
||||||
openReorderDevicesAction?.invoke()
|
|
||||||
showDeviceMenu = false
|
|
||||||
},
|
|
||||||
onOpenVirtualButtonOrder = {
|
|
||||||
rootBackStack.add(RootScreen.VirtualButtonOrder)
|
|
||||||
showDeviceMenu = false
|
|
||||||
},
|
|
||||||
onClearLogs = {
|
|
||||||
clearLogsAction?.invoke()
|
|
||||||
showDeviceMenu = false
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
scrollBehavior = deviceScrollBehavior,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) { pagePadding ->
|
|
||||||
DeviceTabScreen(
|
|
||||||
contentPadding = pagePadding,
|
|
||||||
nativeCore = nativeCore,
|
|
||||||
snack = snackHostState,
|
|
||||||
scrollBehavior = deviceScrollBehavior,
|
|
||||||
keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled,
|
|
||||||
virtualButtonsOutside = virtualButtonsOutside,
|
|
||||||
virtualButtonsInMore = virtualButtonsInMore,
|
|
||||||
customServerUri = customServerUri,
|
|
||||||
serverRemotePath = serverRemotePath,
|
|
||||||
onServerRemotePathChange = { serverRemotePath = it },
|
|
||||||
videoCodec = videoCodec,
|
|
||||||
onVideoCodecChange = { videoCodec = it },
|
|
||||||
audioEnabled = audioEnabled,
|
|
||||||
onAudioEnabledChange = { audioEnabled = it },
|
|
||||||
audioCodec = audioCodec,
|
|
||||||
onAudioCodecChange = { audioCodec = it },
|
|
||||||
noControl = noControl,
|
|
||||||
onNoControlChange = {
|
|
||||||
noControl = it
|
|
||||||
if (it) {
|
|
||||||
turnScreenOff = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
videoEncoder = videoEncoder,
|
|
||||||
onVideoEncoderChange = { videoEncoder = it },
|
|
||||||
videoCodecOptions = videoCodecOptions,
|
|
||||||
onVideoCodecOptionsChange = { videoCodecOptions = it },
|
|
||||||
audioEncoder = audioEncoder,
|
|
||||||
onAudioEncoderChange = { audioEncoder = it },
|
|
||||||
audioCodecOptions = audioCodecOptions,
|
|
||||||
onAudioCodecOptionsChange = { audioCodecOptions = it },
|
|
||||||
audioDup = audioDup,
|
|
||||||
onAudioDupChange = { audioDup = it },
|
|
||||||
audioSourcePreset = audioSourcePreset,
|
|
||||||
onAudioSourcePresetChange = { audioSourcePreset = it },
|
|
||||||
audioSourceCustom = audioSourceCustom,
|
|
||||||
onAudioSourceCustomChange = { audioSourceCustom = it },
|
|
||||||
videoSourcePreset = videoSourcePreset,
|
|
||||||
onVideoSourcePresetChange = { videoSourcePreset = it },
|
|
||||||
cameraIdInput = cameraIdInput,
|
|
||||||
onCameraIdInputChange = { cameraIdInput = it },
|
|
||||||
cameraFacingPreset = cameraFacingPreset,
|
|
||||||
onCameraFacingPresetChange = { cameraFacingPreset = it },
|
|
||||||
cameraSizePreset = cameraSizePreset,
|
|
||||||
onCameraSizePresetChange = { cameraSizePreset = it },
|
|
||||||
cameraSizeCustom = cameraSizeCustom,
|
|
||||||
onCameraSizeCustomChange = { cameraSizeCustom = it },
|
|
||||||
cameraArInput = cameraArInput,
|
|
||||||
onCameraArInputChange = { cameraArInput = it },
|
|
||||||
cameraFpsInput = cameraFpsInput,
|
|
||||||
onCameraFpsInputChange = { cameraFpsInput = it },
|
|
||||||
cameraHighSpeed = cameraHighSpeed,
|
|
||||||
onCameraHighSpeedChange = { cameraHighSpeed = it },
|
|
||||||
noAudioPlayback = noAudioPlayback,
|
|
||||||
onNoAudioPlaybackChange = { noAudioPlayback = it },
|
|
||||||
noVideo = noVideo,
|
|
||||||
requireAudio = requireAudio,
|
|
||||||
onRequireAudioChange = { requireAudio = it },
|
|
||||||
turnScreenOff = turnScreenOff,
|
|
||||||
onTurnScreenOffChange = { turnScreenOff = it },
|
|
||||||
maxSizeInput = maxSizeInput,
|
|
||||||
onMaxSizeInputChange = { maxSizeInput = it },
|
|
||||||
maxFpsInput = maxFpsInput,
|
|
||||||
onMaxFpsInputChange = { maxFpsInput = it },
|
|
||||||
newDisplayWidth = newDisplayWidth,
|
|
||||||
onNewDisplayWidthChange = { newDisplayWidth = it },
|
|
||||||
newDisplayHeight = newDisplayHeight,
|
|
||||||
onNewDisplayHeightChange = { newDisplayHeight = it },
|
|
||||||
newDisplayDpi = newDisplayDpi,
|
|
||||||
onNewDisplayDpiChange = { newDisplayDpi = it },
|
|
||||||
displayIdInput = displayIdInput,
|
|
||||||
onDisplayIdInputChange = { displayIdInput = it },
|
|
||||||
cropWidth = cropWidth,
|
|
||||||
onCropWidthChange = { cropWidth = it },
|
|
||||||
cropHeight = cropHeight,
|
|
||||||
onCropHeightChange = { cropHeight = it },
|
|
||||||
cropX = cropX,
|
|
||||||
onCropXChange = { cropX = it },
|
|
||||||
cropY = cropY,
|
|
||||||
onCropYChange = { cropY = it },
|
|
||||||
videoEncoderOptions = videoEncoderOptions,
|
|
||||||
onVideoEncoderOptionsChange = {
|
|
||||||
videoEncoderOptions.clear()
|
|
||||||
videoEncoderOptions.addAll(it)
|
|
||||||
},
|
|
||||||
onVideoEncoderTypeMapChange = {
|
|
||||||
videoEncoderTypeMap.clear()
|
|
||||||
videoEncoderTypeMap.putAll(it)
|
|
||||||
},
|
|
||||||
audioEncoderOptions = audioEncoderOptions,
|
|
||||||
onAudioEncoderOptionsChange = {
|
|
||||||
audioEncoderOptions.clear()
|
|
||||||
audioEncoderOptions.addAll(it)
|
|
||||||
},
|
|
||||||
onAudioEncoderTypeMapChange = {
|
|
||||||
audioEncoderTypeMap.clear()
|
|
||||||
audioEncoderTypeMap.putAll(it)
|
|
||||||
},
|
|
||||||
cameraSizeOptions = cameraSizeOptions,
|
|
||||||
onCameraSizeOptionsChange = {
|
|
||||||
cameraSizeOptions.clear()
|
|
||||||
cameraSizeOptions.addAll(it)
|
|
||||||
},
|
|
||||||
onSessionStartedChange = { sessionStarted = it },
|
|
||||||
onRefreshEncodersActionChange = { refreshEncodersAction = it },
|
|
||||||
onRefreshCameraSizesActionChange = { refreshCameraSizesAction = it },
|
|
||||||
onClearLogsActionChange = { clearLogsAction = it },
|
|
||||||
onCanClearLogsChange = { canClearLogs = it },
|
|
||||||
onOpenReorderDevicesActionChange = { openReorderDevicesAction = it },
|
|
||||||
onOpenAdvancedPage = { rootBackStack.add(RootScreen.Advanced) },
|
|
||||||
onOpenFullscreenPage = { session ->
|
|
||||||
rootBackStack.add(
|
|
||||||
RootScreen.Fullscreen(
|
|
||||||
launch = FullscreenControlLaunch(
|
|
||||||
deviceName = session.deviceName,
|
|
||||||
width = session.width,
|
|
||||||
height = session.height,
|
|
||||||
codec = session.codec,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
previewCardHeightDp = devicePreviewCardHeightDp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
MainTabDestination.Settings -> Scaffold(
|
MainTabDestination.Settings -> Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = tab.title,
|
title = tab.title,
|
||||||
scrollBehavior = settingsScrollBehavior,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) { pagePadding ->
|
|
||||||
SettingsScreen(
|
|
||||||
contentPadding = pagePadding,
|
|
||||||
themeBaseIndex = themeBaseIndex,
|
|
||||||
onThemeBaseIndexChange = { themeBaseIndex = it },
|
|
||||||
monetEnabled = monetEnabled,
|
|
||||||
onMonetEnabledChange = { monetEnabled = it },
|
|
||||||
fullscreenDebugInfoEnabled = fullscreenDebugInfoEnabled,
|
|
||||||
onFullscreenDebugInfoEnabledChange = { fullscreenDebugInfoEnabled = it },
|
|
||||||
showFullscreenVirtualButtons = showFullscreenVirtualButtons,
|
|
||||||
onShowFullscreenVirtualButtonsChange = { showFullscreenVirtualButtons = it },
|
|
||||||
keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled,
|
|
||||||
onKeepScreenOnWhenStreamingEnabledChange = {
|
|
||||||
keepScreenOnWhenStreamingEnabled = it
|
|
||||||
},
|
|
||||||
onOpenReorderDevices = { openReorderDevicesAction?.invoke() },
|
|
||||||
onOpenVirtualButtonOrder = { rootBackStack.add(RootScreen.VirtualButtonOrder) },
|
|
||||||
devicePreviewCardHeightDp = devicePreviewCardHeightDp,
|
|
||||||
onDevicePreviewCardHeightDpChange = {
|
|
||||||
devicePreviewCardHeightDp = it.coerceAtLeast(120)
|
|
||||||
},
|
|
||||||
customServerUri = customServerUri,
|
|
||||||
onPickServer = {
|
|
||||||
picker.launch(arrayOf("application/java-archive", "application/octet-stream", "*/*"))
|
|
||||||
},
|
|
||||||
onClearServer = { customServerUri = null },
|
|
||||||
serverRemotePath = serverRemotePath,
|
|
||||||
onServerRemotePathChange = { serverRemotePath = it },
|
|
||||||
adbKeyName = adbKeyName,
|
|
||||||
onAdbKeyNameChange = { adbKeyName = it },
|
|
||||||
scrollBehavior = settingsScrollBehavior,
|
scrollBehavior = settingsScrollBehavior,
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
) { pagePadding ->
|
||||||
|
SettingsScreen(
|
||||||
|
contentPadding = pagePadding,
|
||||||
|
themeBaseIndex = themeBaseIndex,
|
||||||
|
onThemeBaseIndexChange = { themeBaseIndex = it },
|
||||||
|
monetEnabled = monetEnabled,
|
||||||
|
onMonetEnabledChange = { monetEnabled = it },
|
||||||
|
fullscreenDebugInfoEnabled = fullscreenDebugInfoEnabled,
|
||||||
|
onFullscreenDebugInfoEnabledChange = {
|
||||||
|
fullscreenDebugInfoEnabled = it
|
||||||
|
},
|
||||||
|
keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled,
|
||||||
|
onKeepScreenOnWhenStreamingEnabledChange = {
|
||||||
|
keepScreenOnWhenStreamingEnabled = it
|
||||||
|
},
|
||||||
|
devicePreviewCardHeightDp = devicePreviewCardHeightDp,
|
||||||
|
onDevicePreviewCardHeightDpChange = {
|
||||||
|
devicePreviewCardHeightDp = it.coerceAtLeast(120)
|
||||||
|
},
|
||||||
|
customServerUri = customServerUri,
|
||||||
|
onPickServer = {
|
||||||
|
picker.launch(
|
||||||
|
arrayOf(
|
||||||
|
"application/java-archive",
|
||||||
|
"application/octet-stream",
|
||||||
|
"*/*"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClearServer = { customServerUri = null },
|
||||||
|
serverRemotePath = serverRemotePath,
|
||||||
|
onServerRemotePathChange = { serverRemotePath = it },
|
||||||
|
adbKeyName = adbKeyName,
|
||||||
|
onAdbKeyNameChange = { adbKeyName = it },
|
||||||
|
scrollBehavior = settingsScrollBehavior,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
entry(RootScreen.Advanced) {
|
entry(RootScreen.Advanced) {
|
||||||
val videoEncoderDropdownItems = listOf("默认") + videoEncoderOptions
|
val videoEncoderDropdownItems = listOf("默认") + videoEncoderOptions
|
||||||
val audioEncoderDropdownItems = listOf("默认") + audioEncoderOptions
|
val audioEncoderDropdownItems = listOf("默认") + audioEncoderOptions
|
||||||
val videoEncoderIndex = (videoEncoderOptions.indexOf(videoEncoder) + 1).coerceAtLeast(0)
|
val videoEncoderIndex = (videoEncoderOptions.indexOf(videoEncoder) + 1).coerceAtLeast(0)
|
||||||
val audioEncoderIndex = (audioEncoderOptions.indexOf(audioEncoder) + 1).coerceAtLeast(0)
|
val audioEncoderIndex = (audioEncoderOptions.indexOf(audioEncoder) + 1).coerceAtLeast(0)
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = "高级参数",
|
title = "高级参数",
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = { popRoot() }) {
|
IconButton(onClick = { popRoot() }) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "返回")
|
Icon(
|
||||||
}
|
Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
},
|
contentDescription = "返回"
|
||||||
scrollBehavior = advancedScrollBehavior,
|
)
|
||||||
)
|
|
||||||
},
|
|
||||||
snackbarHost = { SnackbarHost(snackHostState) },
|
|
||||||
) { pagePadding ->
|
|
||||||
AdvancedConfigPage(
|
|
||||||
contentPadding = pagePadding,
|
|
||||||
scrollBehavior = advancedScrollBehavior,
|
|
||||||
sessionStarted = sessionStarted,
|
|
||||||
snackbarHostState = snackHostState,
|
|
||||||
audioEnabled = audioEnabled,
|
|
||||||
noControl = noControl,
|
|
||||||
onNoControlChange = {
|
|
||||||
noControl = it
|
|
||||||
if (it) {
|
|
||||||
turnScreenOff = false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
audioDup = audioDup,
|
|
||||||
onAudioDupChange = { audioDup = it },
|
|
||||||
audioSourcePreset = audioSourcePreset,
|
|
||||||
onAudioSourcePresetChange = { audioSourcePreset = it },
|
|
||||||
audioSourceCustom = audioSourceCustom,
|
|
||||||
onAudioSourceCustomChange = { audioSourceCustom = it },
|
|
||||||
videoSourcePreset = videoSourcePreset,
|
|
||||||
onVideoSourcePresetChange = { videoSourcePreset = it },
|
|
||||||
cameraIdInput = cameraIdInput,
|
|
||||||
onCameraIdInputChange = { cameraIdInput = it },
|
|
||||||
cameraFacingPreset = cameraFacingPreset,
|
|
||||||
onCameraFacingPresetChange = { cameraFacingPreset = it },
|
|
||||||
cameraSizePreset = cameraSizePreset,
|
|
||||||
onCameraSizePresetChange = { cameraSizePreset = it },
|
|
||||||
cameraSizeCustom = cameraSizeCustom,
|
|
||||||
onCameraSizeCustomChange = { cameraSizeCustom = it },
|
|
||||||
cameraSizeDropdownItems = listOf("默认") + cameraSizeOptions + listOf("自定义"),
|
|
||||||
cameraSizeIndex = when (cameraSizePreset) {
|
|
||||||
"custom" -> cameraSizeOptions.size + 1
|
|
||||||
in cameraSizeOptions -> cameraSizeOptions.indexOf(cameraSizePreset) + 1
|
|
||||||
else -> 0
|
|
||||||
},
|
|
||||||
cameraArInput = cameraArInput,
|
|
||||||
onCameraArInputChange = { cameraArInput = it },
|
|
||||||
cameraFpsInput = cameraFpsInput,
|
|
||||||
onCameraFpsInputChange = { cameraFpsInput = it },
|
|
||||||
cameraHighSpeed = cameraHighSpeed,
|
|
||||||
onCameraHighSpeedChange = { cameraHighSpeed = it },
|
|
||||||
noAudioPlayback = noAudioPlayback,
|
|
||||||
onNoAudioPlaybackChange = { noAudioPlayback = it },
|
|
||||||
noVideo = noVideo,
|
|
||||||
onNoVideoChange = { noVideo = it },
|
|
||||||
requireAudio = requireAudio,
|
|
||||||
onRequireAudioChange = { requireAudio = it },
|
|
||||||
turnScreenOff = turnScreenOff,
|
|
||||||
onTurnScreenOffChange = { turnScreenOff = it },
|
|
||||||
maxSizeInput = maxSizeInput,
|
|
||||||
onMaxSizeInputChange = { maxSizeInput = it },
|
|
||||||
maxFpsInput = maxFpsInput,
|
|
||||||
onMaxFpsInputChange = { maxFpsInput = it },
|
|
||||||
videoEncoderDropdownItems = videoEncoderDropdownItems,
|
|
||||||
videoEncoderTypeMap = videoEncoderTypeMap,
|
|
||||||
videoEncoderIndex = videoEncoderIndex,
|
|
||||||
onVideoEncoderChange = { videoEncoder = it },
|
|
||||||
videoCodecOptions = videoCodecOptions,
|
|
||||||
onVideoCodecOptionsChange = { videoCodecOptions = it },
|
|
||||||
audioEncoderDropdownItems = audioEncoderDropdownItems,
|
|
||||||
audioEncoderTypeMap = audioEncoderTypeMap,
|
|
||||||
audioEncoderIndex = audioEncoderIndex,
|
|
||||||
onAudioEncoderChange = { audioEncoder = it },
|
|
||||||
audioCodecOptions = audioCodecOptions,
|
|
||||||
onAudioCodecOptionsChange = { audioCodecOptions = it },
|
|
||||||
onRefreshEncoders = { refreshEncodersAction?.invoke() },
|
|
||||||
onRefreshCameraSizes = { refreshCameraSizesAction?.invoke() },
|
|
||||||
newDisplayWidth = newDisplayWidth,
|
|
||||||
onNewDisplayWidthChange = { newDisplayWidth = it },
|
|
||||||
newDisplayHeight = newDisplayHeight,
|
|
||||||
onNewDisplayHeightChange = { newDisplayHeight = it },
|
|
||||||
newDisplayDpi = newDisplayDpi,
|
|
||||||
onNewDisplayDpiChange = { newDisplayDpi = it },
|
|
||||||
displayIdInput = displayIdInput,
|
|
||||||
onDisplayIdInputChange = { displayIdInput = it },
|
|
||||||
cropWidth = cropWidth,
|
|
||||||
onCropWidthChange = { cropWidth = it },
|
|
||||||
cropHeight = cropHeight,
|
|
||||||
onCropHeightChange = { cropHeight = it },
|
|
||||||
cropX = cropX,
|
|
||||||
onCropXChange = { cropX = it },
|
|
||||||
cropY = cropY,
|
|
||||||
onCropYChange = { cropY = it },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entry(RootScreen.VirtualButtonOrder) {
|
|
||||||
Scaffold(
|
|
||||||
modifier = Modifier.nestedScroll(advancedScrollBehavior.nestedScrollConnection),
|
|
||||||
topBar = {
|
|
||||||
TopAppBar(
|
|
||||||
title = "虚拟按钮排序",
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = { popRoot() }) {
|
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "返回")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scrollBehavior = advancedScrollBehavior,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) { pagePadding ->
|
|
||||||
VirtualButtonOrderPage(
|
|
||||||
contentPadding = pagePadding,
|
|
||||||
scrollBehavior = advancedScrollBehavior,
|
scrollBehavior = advancedScrollBehavior,
|
||||||
outsideIds = virtualButtonsOutside,
|
|
||||||
moreIds = virtualButtonsInMore,
|
|
||||||
onLayoutChange = { outside, more ->
|
|
||||||
virtualButtonsOutside = outside
|
|
||||||
virtualButtonsInMore = more
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
}
|
snackbarHost = { SnackbarHost(snackHostState) },
|
||||||
|
) { pagePadding ->
|
||||||
entry<RootScreen.Fullscreen> { screen ->
|
AdvancedConfigPage(
|
||||||
FullscreenControlPage(
|
contentPadding = pagePadding,
|
||||||
launch = screen.launch,
|
scrollBehavior = advancedScrollBehavior,
|
||||||
nativeCore = nativeCore,
|
sessionStarted = sessionStarted,
|
||||||
virtualButtonsOutside = virtualButtonsOutside,
|
snackbarHostState = snackHostState,
|
||||||
virtualButtonsInMore = virtualButtonsInMore,
|
audioEnabled = audioEnabled,
|
||||||
showDebugInfo = fullscreenDebugInfoEnabled,
|
noControl = noControl,
|
||||||
showVirtualButtons = showFullscreenVirtualButtons,
|
onNoControlChange = {
|
||||||
onDismiss = { popRoot() },
|
noControl = it
|
||||||
|
if (it) {
|
||||||
|
turnScreenOff = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
audioDup = audioDup,
|
||||||
|
onAudioDupChange = { audioDup = it },
|
||||||
|
audioSourcePreset = audioSourcePreset,
|
||||||
|
onAudioSourcePresetChange = { audioSourcePreset = it },
|
||||||
|
audioSourceCustom = audioSourceCustom,
|
||||||
|
onAudioSourceCustomChange = { audioSourceCustom = it },
|
||||||
|
videoSourcePreset = videoSourcePreset,
|
||||||
|
onVideoSourcePresetChange = { videoSourcePreset = it },
|
||||||
|
cameraIdInput = cameraIdInput,
|
||||||
|
onCameraIdInputChange = { cameraIdInput = it },
|
||||||
|
cameraFacingPreset = cameraFacingPreset,
|
||||||
|
onCameraFacingPresetChange = { cameraFacingPreset = it },
|
||||||
|
cameraSizePreset = cameraSizePreset,
|
||||||
|
onCameraSizePresetChange = { cameraSizePreset = it },
|
||||||
|
cameraSizeCustom = cameraSizeCustom,
|
||||||
|
onCameraSizeCustomChange = { cameraSizeCustom = it },
|
||||||
|
cameraSizeDropdownItems = listOf("默认") + cameraSizeOptions + listOf("自定义"),
|
||||||
|
cameraSizeIndex = when (cameraSizePreset) {
|
||||||
|
"custom" -> cameraSizeOptions.size + 1
|
||||||
|
in cameraSizeOptions -> cameraSizeOptions.indexOf(cameraSizePreset) + 1
|
||||||
|
else -> 0
|
||||||
|
},
|
||||||
|
cameraArInput = cameraArInput,
|
||||||
|
onCameraArInputChange = { cameraArInput = it },
|
||||||
|
cameraFpsInput = cameraFpsInput,
|
||||||
|
onCameraFpsInputChange = { cameraFpsInput = it },
|
||||||
|
cameraHighSpeed = cameraHighSpeed,
|
||||||
|
onCameraHighSpeedChange = { cameraHighSpeed = it },
|
||||||
|
noAudioPlayback = noAudioPlayback,
|
||||||
|
onNoAudioPlaybackChange = { noAudioPlayback = it },
|
||||||
|
noVideo = noVideo,
|
||||||
|
onNoVideoChange = { noVideo = it },
|
||||||
|
requireAudio = requireAudio,
|
||||||
|
onRequireAudioChange = { requireAudio = it },
|
||||||
|
turnScreenOff = turnScreenOff,
|
||||||
|
onTurnScreenOffChange = { turnScreenOff = it },
|
||||||
|
maxSizeInput = maxSizeInput,
|
||||||
|
onMaxSizeInputChange = { maxSizeInput = it },
|
||||||
|
maxFpsInput = maxFpsInput,
|
||||||
|
onMaxFpsInputChange = { maxFpsInput = it },
|
||||||
|
videoEncoderDropdownItems = videoEncoderDropdownItems,
|
||||||
|
videoEncoderTypeMap = videoEncoderTypeMap,
|
||||||
|
videoEncoderIndex = videoEncoderIndex,
|
||||||
|
onVideoEncoderChange = { videoEncoder = it },
|
||||||
|
videoCodecOptions = videoCodecOptions,
|
||||||
|
onVideoCodecOptionsChange = { videoCodecOptions = it },
|
||||||
|
audioEncoderDropdownItems = audioEncoderDropdownItems,
|
||||||
|
audioEncoderTypeMap = audioEncoderTypeMap,
|
||||||
|
audioEncoderIndex = audioEncoderIndex,
|
||||||
|
onAudioEncoderChange = { audioEncoder = it },
|
||||||
|
audioCodecOptions = audioCodecOptions,
|
||||||
|
onAudioCodecOptionsChange = { audioCodecOptions = it },
|
||||||
|
onRefreshEncoders = { refreshEncodersAction?.invoke() },
|
||||||
|
onRefreshCameraSizes = { refreshCameraSizesAction?.invoke() },
|
||||||
|
newDisplayWidth = newDisplayWidth,
|
||||||
|
onNewDisplayWidthChange = { newDisplayWidth = it },
|
||||||
|
newDisplayHeight = newDisplayHeight,
|
||||||
|
onNewDisplayHeightChange = { newDisplayHeight = it },
|
||||||
|
newDisplayDpi = newDisplayDpi,
|
||||||
|
onNewDisplayDpiChange = { newDisplayDpi = it },
|
||||||
|
displayIdInput = displayIdInput,
|
||||||
|
onDisplayIdInputChange = { displayIdInput = it },
|
||||||
|
cropWidth = cropWidth,
|
||||||
|
onCropWidthChange = { cropWidth = it },
|
||||||
|
cropHeight = cropHeight,
|
||||||
|
onCropHeightChange = { cropHeight = it },
|
||||||
|
cropX = cropX,
|
||||||
|
onCropXChange = { cropX = it },
|
||||||
|
cropY = cropY,
|
||||||
|
onCropYChange = { cropY = it },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entry(RootScreen.VirtualButtonOrder) {
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.nestedScroll(advancedScrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = "虚拟按钮排序",
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { popRoot() }) {
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = "返回"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollBehavior = advancedScrollBehavior,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { pagePadding ->
|
||||||
|
VirtualButtonOrderPage(
|
||||||
|
contentPadding = pagePadding,
|
||||||
|
scrollBehavior = advancedScrollBehavior,
|
||||||
|
outsideIds = virtualButtonsOutside,
|
||||||
|
moreIds = virtualButtonsInMore,
|
||||||
|
onLayoutChange = { outside, more ->
|
||||||
|
virtualButtonsOutside = outside
|
||||||
|
virtualButtonsInMore = more
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entry<RootScreen.Fullscreen> { screen ->
|
||||||
|
FullscreenControlPage(
|
||||||
|
launch = screen.launch,
|
||||||
|
nativeCore = nativeCore,
|
||||||
|
virtualButtonsOutside = virtualButtonsOutside,
|
||||||
|
virtualButtonsInMore = virtualButtonsInMore,
|
||||||
|
showDebugInfo = fullscreenDebugInfoEnabled,
|
||||||
|
showVirtualButtons = showFullscreenVirtualButtons,
|
||||||
|
onDismiss = { popRoot() },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val rootEntries = rememberDecoratedNavEntries(
|
val rootEntries = rememberDecoratedNavEntries(
|
||||||
@@ -724,7 +745,8 @@ private fun DeviceMenuPopupItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val additionalTopPadding = if (index == 0) UiSpacing.PopupHorizontal else UiSpacing.PageItem
|
val additionalTopPadding = if (index == 0) UiSpacing.PopupHorizontal else UiSpacing.PageItem
|
||||||
val additionalBottomPadding = if (index == optionSize - 1) UiSpacing.PopupHorizontal else UiSpacing.PageItem
|
val additionalBottomPadding =
|
||||||
|
if (index == optionSize - 1) UiSpacing.PopupHorizontal else UiSpacing.PageItem
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
fontSize = MiuixTheme.textStyles.body1.fontSize,
|
fontSize = MiuixTheme.textStyles.body1.fontSize,
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import top.yukonga.miuix.kmp.basic.IconButton
|
|||||||
import top.yukonga.miuix.kmp.basic.ScrollBehavior
|
import top.yukonga.miuix.kmp.basic.ScrollBehavior
|
||||||
import top.yukonga.miuix.kmp.basic.Text
|
import top.yukonga.miuix.kmp.basic.Text
|
||||||
import top.yukonga.miuix.kmp.basic.TextField
|
import top.yukonga.miuix.kmp.basic.TextField
|
||||||
import top.yukonga.miuix.kmp.extra.SuperArrow
|
|
||||||
import top.yukonga.miuix.kmp.extra.SuperDropdown
|
import top.yukonga.miuix.kmp.extra.SuperDropdown
|
||||||
import top.yukonga.miuix.kmp.extra.SuperSwitch
|
import top.yukonga.miuix.kmp.extra.SuperSwitch
|
||||||
import top.yukonga.miuix.kmp.theme.ColorSchemeMode
|
import top.yukonga.miuix.kmp.theme.ColorSchemeMode
|
||||||
@@ -61,12 +60,8 @@ fun SettingsScreen(
|
|||||||
onMonetEnabledChange: (Boolean) -> Unit,
|
onMonetEnabledChange: (Boolean) -> Unit,
|
||||||
fullscreenDebugInfoEnabled: Boolean,
|
fullscreenDebugInfoEnabled: Boolean,
|
||||||
onFullscreenDebugInfoEnabledChange: (Boolean) -> Unit,
|
onFullscreenDebugInfoEnabledChange: (Boolean) -> Unit,
|
||||||
showFullscreenVirtualButtons: Boolean,
|
|
||||||
onShowFullscreenVirtualButtonsChange: (Boolean) -> Unit,
|
|
||||||
keepScreenOnWhenStreamingEnabled: Boolean,
|
keepScreenOnWhenStreamingEnabled: Boolean,
|
||||||
onKeepScreenOnWhenStreamingEnabledChange: (Boolean) -> Unit,
|
onKeepScreenOnWhenStreamingEnabledChange: (Boolean) -> Unit,
|
||||||
onOpenReorderDevices: () -> Unit,
|
|
||||||
onOpenVirtualButtonOrder: () -> Unit,
|
|
||||||
devicePreviewCardHeightDp: Int,
|
devicePreviewCardHeightDp: Int,
|
||||||
onDevicePreviewCardHeightDpChange: (Int) -> Unit,
|
onDevicePreviewCardHeightDpChange: (Int) -> Unit,
|
||||||
customServerUri: String?,
|
customServerUri: String?,
|
||||||
@@ -111,12 +106,6 @@ fun SettingsScreen(
|
|||||||
checked = fullscreenDebugInfoEnabled,
|
checked = fullscreenDebugInfoEnabled,
|
||||||
onCheckedChange = onFullscreenDebugInfoEnabledChange,
|
onCheckedChange = onFullscreenDebugInfoEnabledChange,
|
||||||
)
|
)
|
||||||
SuperSwitch(
|
|
||||||
title = "全屏显示虚拟按钮",
|
|
||||||
summary = "在全屏模式底部显示返回/主页等虚拟按钮",
|
|
||||||
checked = showFullscreenVirtualButtons,
|
|
||||||
onCheckedChange = onShowFullscreenVirtualButtonsChange,
|
|
||||||
)
|
|
||||||
SuperSwitch(
|
SuperSwitch(
|
||||||
title = "投屏时不允许息屏",
|
title = "投屏时不允许息屏",
|
||||||
summary = "Scrcpy 启动后保持本机常亮,避免锁屏导致 ADB 断开",
|
summary = "Scrcpy 启动后保持本机常亮,避免锁屏导致 ADB 断开",
|
||||||
@@ -127,7 +116,11 @@ fun SettingsScreen(
|
|||||||
title = "预览卡高度",
|
title = "预览卡高度",
|
||||||
summary = "设备页预览卡高度",
|
summary = "设备页预览卡高度",
|
||||||
value = devicePreviewCardHeightDp.toFloat(),
|
value = devicePreviewCardHeightDp.toFloat(),
|
||||||
onValueChange = { onDevicePreviewCardHeightDpChange(it.roundToInt().coerceAtLeast(120)) },
|
onValueChange = {
|
||||||
|
onDevicePreviewCardHeightDpChange(
|
||||||
|
it.roundToInt().coerceAtLeast(120)
|
||||||
|
)
|
||||||
|
},
|
||||||
valueRange = 160f..600f,
|
valueRange = 160f..600f,
|
||||||
steps = 439,
|
steps = 439,
|
||||||
unit = "dp",
|
unit = "dp",
|
||||||
@@ -136,19 +129,10 @@ fun SettingsScreen(
|
|||||||
inputFilter = { it.filter(Char::isDigit) },
|
inputFilter = { it.filter(Char::isDigit) },
|
||||||
inputValueRange = 120f..Float.MAX_VALUE,
|
inputValueRange = 120f..Float.MAX_VALUE,
|
||||||
onInputConfirm = { raw ->
|
onInputConfirm = { raw ->
|
||||||
raw.toIntOrNull()?.let { onDevicePreviewCardHeightDpChange(it.coerceAtLeast(120)) }
|
raw.toIntOrNull()
|
||||||
|
?.let { onDevicePreviewCardHeightDpChange(it.coerceAtLeast(120)) }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
SuperArrow(
|
|
||||||
title = "调整设备排序",
|
|
||||||
summary = "调整设备页快捷设备列表顺序",
|
|
||||||
onClick = onOpenReorderDevices,
|
|
||||||
)
|
|
||||||
SuperArrow(
|
|
||||||
title = "虚拟按钮排序",
|
|
||||||
summary = "调整底部虚拟按钮与更多菜单顺序",
|
|
||||||
onClick = onOpenVirtualButtonOrder,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SectionSmallTitle(text = "scrcpy-server")
|
SectionSmallTitle(text = "scrcpy-server")
|
||||||
@@ -192,7 +176,7 @@ fun SettingsScreen(
|
|||||||
TextField(
|
TextField(
|
||||||
value = serverRemotePath,
|
value = serverRemotePath,
|
||||||
onValueChange = onServerRemotePathChange,
|
onValueChange = onServerRemotePathChange,
|
||||||
label = AppDefaults.DefaultServerRemotePath,
|
label = AppDefaults.SERVER_REMOTE_PATH,
|
||||||
useLabelAsPlaceholder = true,
|
useLabelAsPlaceholder = true,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -214,7 +198,7 @@ fun SettingsScreen(
|
|||||||
TextField(
|
TextField(
|
||||||
value = adbKeyName,
|
value = adbKeyName,
|
||||||
onValueChange = onAdbKeyNameChange,
|
onValueChange = onAdbKeyNameChange,
|
||||||
label = AppDefaults.DefaultAdbKeyName,
|
label = AppDefaults.ADB_KEY_NAME,
|
||||||
useLabelAsPlaceholder = true,
|
useLabelAsPlaceholder = true,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.toMutableStateList
|
import androidx.compose.runtime.toMutableStateList
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -54,7 +53,11 @@ internal fun VirtualButtonOrderPage(
|
|||||||
onLayoutChange(outsideState.toList(), moreState.toList())
|
onLayoutChange(outsideState.toList(), moreState.toList())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reorderInside(list: androidx.compose.runtime.snapshots.SnapshotStateList<String>, itemId: String, deltaY: Float) {
|
fun reorderInside(
|
||||||
|
list: androidx.compose.runtime.snapshots.SnapshotStateList<String>,
|
||||||
|
itemId: String,
|
||||||
|
deltaY: Float
|
||||||
|
) {
|
||||||
val fromIndex = list.indexOf(itemId)
|
val fromIndex = list.indexOf(itemId)
|
||||||
if (fromIndex < 0) return
|
if (fromIndex < 0) return
|
||||||
|
|
||||||
@@ -88,7 +91,8 @@ internal fun VirtualButtonOrderPage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun handleOutsideDrop(payload: SortDropPayload) {
|
fun handleOutsideDrop(payload: SortDropPayload) {
|
||||||
val transfer = payload.transferDirection == SortTransferDirection.TO_RIGHT && payload.deltaX >= LIST_TRANSFER_STEP_PX
|
val transfer =
|
||||||
|
payload.transferDirection == SortTransferDirection.TO_RIGHT && payload.deltaX >= LIST_TRANSFER_STEP_PX
|
||||||
if (transfer) {
|
if (transfer) {
|
||||||
if (payload.itemId == VirtualButtonAction.MORE.id) return
|
if (payload.itemId == VirtualButtonAction.MORE.id) return
|
||||||
transferToOther(outsideState, moreState, payload.itemId, payload.deltaY)
|
transferToOther(outsideState, moreState, payload.itemId, payload.deltaY)
|
||||||
@@ -98,7 +102,8 @@ internal fun VirtualButtonOrderPage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun handleMoreDrop(payload: SortDropPayload) {
|
fun handleMoreDrop(payload: SortDropPayload) {
|
||||||
val transfer = payload.transferDirection == SortTransferDirection.TO_LEFT && payload.deltaX <= -LIST_TRANSFER_STEP_PX
|
val transfer =
|
||||||
|
payload.transferDirection == SortTransferDirection.TO_LEFT && payload.deltaX <= -LIST_TRANSFER_STEP_PX
|
||||||
if (transfer) {
|
if (transfer) {
|
||||||
transferToOther(moreState, outsideState, payload.itemId, payload.deltaY)
|
transferToOther(moreState, outsideState, payload.itemId, payload.deltaY)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ fun SuperSlide(
|
|||||||
holdDownState = holdArrow,
|
holdDownState = holdArrow,
|
||||||
endActions = {
|
endActions = {
|
||||||
val isZeroState = value == 0f && zeroStateText != null
|
val isZeroState = value == 0f && zeroStateText != null
|
||||||
val valueText = if (isZeroState) zeroStateText else (displayText ?: displayFormatter(value))
|
val valueText =
|
||||||
|
if (isZeroState) zeroStateText else (displayText ?: displayFormatter(value))
|
||||||
val shouldShowUnit = unit.isNotBlank() && (!isZeroState || showUnitWhenZeroState)
|
val shouldShowUnit = unit.isNotBlank() && (!isZeroState || showUnitWhenZeroState)
|
||||||
val text = if (shouldShowUnit) "$valueText $unit" else valueText
|
val text = if (shouldShowUnit) "$valueText $unit" else valueText
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -6,215 +6,495 @@ import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults
|
|||||||
import io.github.miuzarte.scrcpyforandroid.constants.AppPreferenceKeys
|
import io.github.miuzarte.scrcpyforandroid.constants.AppPreferenceKeys
|
||||||
|
|
||||||
internal data class MainSettings(
|
internal data class MainSettings(
|
||||||
val themeBaseIndex: Int = AppDefaults.DefaultThemeBaseIndex,
|
val audioEnabled: Boolean = AppDefaults.AUDIO_ENABLED,
|
||||||
val monetEnabled: Boolean = AppDefaults.DefaultMonetEnabled,
|
val audioCodec: String = AppDefaults.AUDIO_CODEC,
|
||||||
val fullscreenDebugInfoEnabled: Boolean = AppDefaults.DefaultFullscreenDebugInfoEnabled,
|
val videoCodec: String = AppDefaults.VIDEO_CODEC,
|
||||||
val showFullscreenVirtualButtons: Boolean = AppDefaults.DefaultShowFullscreenVirtualButtons,
|
val themeBaseIndex: Int = AppDefaults.THEME_BASE_INDEX,
|
||||||
val devicePreviewCardHeightDp: Int = AppDefaults.DefaultDevicePreviewCardHeightDp,
|
val monetEnabled: Boolean = AppDefaults.MONET,
|
||||||
val keepScreenOnWhenStreamingEnabled: Boolean = AppDefaults.DefaultKeepScreenOnWhenStreamingEnabled,
|
val fullscreenDebugInfoEnabled: Boolean = AppDefaults.FULLSCREEN_DEBUG_INFO,
|
||||||
val virtualButtonsOutside: String = AppDefaults.DefaultVirtualButtonsOutside,
|
val showFullscreenVirtualButtons: Boolean = AppDefaults.SHOW_FULLSCREEN_VIRTUAL_BUTTONS,
|
||||||
val virtualButtonsInMore: String = AppDefaults.DefaultVirtualButtonsInMore,
|
val keepScreenOnWhenStreamingEnabled: Boolean = AppDefaults.KEEP_SCREEN_ON_WHEN_STREAMING,
|
||||||
val customServerUri: String? = null,
|
val devicePreviewCardHeightDp: Int = AppDefaults.DEVICE_PREVIEW_CARD_HEIGHT_DP,
|
||||||
val serverRemotePath: String = AppDefaults.DefaultServerRemotePathInput,
|
val virtualButtonsOutside: String = AppDefaults.VIRTUAL_BUTTONS_OUTSIDE,
|
||||||
val videoCodec: String = AppDefaults.DefaultVideoCodec,
|
val virtualButtonsInMore: String = AppDefaults.VIRTUAL_BUTTONS_IN_MORE,
|
||||||
val audioEnabled: Boolean = AppDefaults.DefaultAudioEnabled,
|
val customServerUri: String? = AppDefaults.CUSTOM_SERVER_URI,
|
||||||
val audioCodec: String = AppDefaults.DefaultAudioCodec,
|
val serverRemotePath: String = AppDefaults.SERVER_REMOTE_PATH_INPUT,
|
||||||
val adbKeyName: String = AppDefaults.DefaultAdbKeyNameInput,
|
val adbKeyName: String = AppDefaults.ADB_KEY_NAME_INPUT,
|
||||||
)
|
)
|
||||||
|
|
||||||
internal data class DevicePageSettings(
|
internal data class DevicePageSettings(
|
||||||
val pairHost: String = AppDefaults.DefaultPairHost,
|
val quickConnectInput: String = AppDefaults.QUICK_CONNECT_INPUT,
|
||||||
val pairPort: String = AppDefaults.DefaultPairPort,
|
val pairHost: String = AppDefaults.PAIR_HOST,
|
||||||
val pairCode: String = AppDefaults.DefaultPairCode,
|
val pairPort: String = AppDefaults.PAIR_PORT,
|
||||||
val quickConnectInput: String = AppDefaults.DefaultQuickConnectInput,
|
val pairCode: String = AppDefaults.PAIR_CODE,
|
||||||
val bitRateMbps: Float = AppDefaults.DefaultBitRateMbps,
|
val audioBitRateKbps: Int = AppDefaults.AUDIO_BIT_RATE_KBPS,
|
||||||
val bitRateInput: String = AppDefaults.DefaultBitRateInput,
|
val audioBitRateInput: String = AppDefaults.AUDIO_BIT_RATE_INPUT,
|
||||||
val audioBitRateKbps: Int = AppDefaults.DefaultAudioBitRateKbps,
|
val videoBitRateMbps: Float = AppDefaults.VIDEO_BIT_RATE_MBPS,
|
||||||
val maxSizeInput: String = AppDefaults.DefaultMaxSizeInput,
|
val videoBitRateInput: String = AppDefaults.VIDEO_BIT_RATE_INPUT,
|
||||||
val maxFpsInput: String = AppDefaults.DefaultMaxFpsInput,
|
val turnScreenOff: Boolean = AppDefaults.TURN_SCREEN_OFF,
|
||||||
val noControl: Boolean = AppDefaults.DefaultNoControl,
|
val noControl: Boolean = AppDefaults.NO_CONTROL,
|
||||||
val videoEncoder: String = AppDefaults.DefaultVideoEncoder,
|
val noVideo: Boolean = AppDefaults.NO_VIDEO,
|
||||||
val videoCodecOptions: String = AppDefaults.DefaultVideoCodecOptions,
|
val videoSourcePreset: String = AppDefaults.VIDEO_SOURCE_PRESET,
|
||||||
val audioEncoder: String = AppDefaults.DefaultAudioEncoder,
|
val displayIdInput: String = AppDefaults.DISPLAY_ID,
|
||||||
val audioCodecOptions: String = AppDefaults.DefaultAudioCodecOptions,
|
val cameraIdInput: String = AppDefaults.CAMERA_ID,
|
||||||
val audioDup: Boolean = AppDefaults.DefaultAudioDup,
|
val cameraFacingPreset: String = AppDefaults.CAMERA_FACING_PRESET,
|
||||||
val audioSourcePreset: String = AppDefaults.DefaultAudioSourcePreset,
|
val cameraSizePreset: String = AppDefaults.CAMERA_SIZE_PRESET,
|
||||||
val audioSourceCustom: String = AppDefaults.DefaultAudioSourceCustom,
|
val cameraSizeCustom: String = AppDefaults.CAMERA_SIZE_CUSTOM,
|
||||||
val videoSourcePreset: String = AppDefaults.DefaultVideoSourcePreset,
|
val cameraAr: String = AppDefaults.CAMERA_AR,
|
||||||
val cameraIdInput: String = AppDefaults.DefaultCameraIdInput,
|
val cameraFps: String = AppDefaults.CAMERA_FPS,
|
||||||
val cameraFacingPreset: String = AppDefaults.DefaultCameraFacingPreset,
|
val cameraHighSpeed: Boolean = AppDefaults.CAMERA_HIGH_SPEED,
|
||||||
val cameraSizePreset: String = AppDefaults.DefaultCameraSizePreset,
|
val audioSourcePreset: String = AppDefaults.AUDIO_SOURCE_PRESET,
|
||||||
val cameraSizeCustom: String = AppDefaults.DefaultCameraSizeCustom,
|
val audioSourceCustom: String = AppDefaults.AUDIO_SOURCE_CUSTOM,
|
||||||
val cameraArInput: String = AppDefaults.DefaultCameraArInput,
|
val audioDup: Boolean = AppDefaults.AUDIO_DUP,
|
||||||
val cameraFpsInput: String = AppDefaults.DefaultCameraFpsInput,
|
val noAudioPlayback: Boolean = AppDefaults.NO_AUDIO_PLAYBACK,
|
||||||
val cameraHighSpeed: Boolean = AppDefaults.DefaultCameraHighSpeed,
|
val requireAudio: Boolean = AppDefaults.REQUIRE_AUDIO,
|
||||||
val noAudioPlayback: Boolean = AppDefaults.DefaultNoAudioPlayback,
|
val maxSizeInput: String = AppDefaults.MAX_SIZE_INPUT,
|
||||||
val noVideo: Boolean = AppDefaults.DefaultNoVideo,
|
val maxFpsInput: String = AppDefaults.MAX_FPS_INPUT,
|
||||||
val requireAudio: Boolean = AppDefaults.DefaultRequireAudio,
|
val videoEncoder: String = AppDefaults.VIDEO_ENCODER,
|
||||||
val turnScreenOff: Boolean = AppDefaults.DefaultTurnScreenOff,
|
val videoCodecOptions: String = AppDefaults.VIDEO_CODEC_OPTION,
|
||||||
val newDisplayWidth: String = AppDefaults.DefaultNewDisplayWidth,
|
val audioEncoder: String = AppDefaults.AUDIO_ENCODER,
|
||||||
val newDisplayHeight: String = AppDefaults.DefaultNewDisplayHeight,
|
val audioCodecOptions: String = AppDefaults.AUDIO_CODEC_OPTION,
|
||||||
val newDisplayDpi: String = AppDefaults.DefaultNewDisplayDpi,
|
val newDisplayWidth: String = AppDefaults.NEW_DISPLAY_WIDTH,
|
||||||
val displayIdInput: String = AppDefaults.DefaultDisplayIdInput,
|
val newDisplayHeight: String = AppDefaults.NEW_DISPLAY_HEIGHT,
|
||||||
val cropWidth: String = AppDefaults.DefaultCropWidth,
|
val newDisplayDpi: String = AppDefaults.NEW_DISPLAY_DPI,
|
||||||
val cropHeight: String = AppDefaults.DefaultCropHeight,
|
val cropWidth: String = AppDefaults.CROP_WIDTH,
|
||||||
val cropX: String = AppDefaults.DefaultCropX,
|
val cropHeight: String = AppDefaults.CROP_HEIGHT,
|
||||||
val cropY: String = AppDefaults.DefaultCropY,
|
val cropX: String = AppDefaults.CROP_X,
|
||||||
|
val cropY: String = AppDefaults.CROP_Y,
|
||||||
)
|
)
|
||||||
|
|
||||||
internal fun loadMainSettings(context: Context): MainSettings {
|
internal fun loadMainSettings(context: Context): MainSettings {
|
||||||
val prefs = context.getSharedPreferences(AppPreferenceKeys.PrefsName, Context.MODE_PRIVATE)
|
val prefs = context.getSharedPreferences(
|
||||||
|
AppPreferenceKeys.PREFS_NAME,
|
||||||
|
Context.MODE_PRIVATE,
|
||||||
|
)
|
||||||
return MainSettings(
|
return MainSettings(
|
||||||
themeBaseIndex = prefs.getInt(AppPreferenceKeys.ThemeBaseIndex, AppDefaults.DefaultThemeBaseIndex),
|
audioEnabled = prefs.getBoolean(
|
||||||
monetEnabled = prefs.getBoolean(AppPreferenceKeys.MonetEnabled, AppDefaults.DefaultMonetEnabled),
|
AppPreferenceKeys.AUDIO_ENABLED,
|
||||||
|
AppDefaults.AUDIO_ENABLED,
|
||||||
|
),
|
||||||
|
audioCodec = prefs.getString(
|
||||||
|
AppPreferenceKeys.AUDIO_CODEC,
|
||||||
|
AppDefaults.AUDIO_CODEC,
|
||||||
|
).orEmpty().ifBlank { AppDefaults.AUDIO_CODEC },
|
||||||
|
videoCodec = prefs.getString(
|
||||||
|
AppPreferenceKeys.VIDEO_CODEC,
|
||||||
|
AppDefaults.VIDEO_CODEC,
|
||||||
|
).orEmpty().ifBlank { AppDefaults.VIDEO_CODEC },
|
||||||
|
themeBaseIndex = prefs.getInt(
|
||||||
|
AppPreferenceKeys.THEME_BASE_INDEX,
|
||||||
|
AppDefaults.THEME_BASE_INDEX,
|
||||||
|
),
|
||||||
|
monetEnabled = prefs.getBoolean(
|
||||||
|
AppPreferenceKeys.MONET,
|
||||||
|
AppDefaults.MONET,
|
||||||
|
),
|
||||||
fullscreenDebugInfoEnabled = prefs.getBoolean(
|
fullscreenDebugInfoEnabled = prefs.getBoolean(
|
||||||
AppPreferenceKeys.FullscreenDebugInfoEnabled,
|
AppPreferenceKeys.FULLSCREEN_DEBUG_INFO,
|
||||||
AppDefaults.DefaultFullscreenDebugInfoEnabled,
|
AppDefaults.FULLSCREEN_DEBUG_INFO,
|
||||||
),
|
),
|
||||||
showFullscreenVirtualButtons = prefs.getBoolean(
|
showFullscreenVirtualButtons = prefs.getBoolean(
|
||||||
AppPreferenceKeys.ShowFullscreenVirtualButtons,
|
AppPreferenceKeys.SHOW_FULLSCREEN_VIRTUAL_BUTTONS,
|
||||||
AppDefaults.DefaultShowFullscreenVirtualButtons,
|
AppDefaults.SHOW_FULLSCREEN_VIRTUAL_BUTTONS,
|
||||||
|
),
|
||||||
|
keepScreenOnWhenStreamingEnabled = prefs.getBoolean(
|
||||||
|
AppPreferenceKeys.KEEP_SCREEN_ON_WHEN_STREAMING,
|
||||||
|
AppDefaults.KEEP_SCREEN_ON_WHEN_STREAMING,
|
||||||
),
|
),
|
||||||
devicePreviewCardHeightDp = prefs.getInt(
|
devicePreviewCardHeightDp = prefs.getInt(
|
||||||
AppPreferenceKeys.DevicePreviewCardHeightDp,
|
AppPreferenceKeys.DEVICE_PREVIEW_CARD_HEIGHT_DP,
|
||||||
AppDefaults.DefaultDevicePreviewCardHeightDp,
|
AppDefaults.DEVICE_PREVIEW_CARD_HEIGHT_DP,
|
||||||
).coerceAtLeast(120),
|
).coerceAtLeast(120),
|
||||||
keepScreenOnWhenStreamingEnabled = prefs.getBoolean(
|
|
||||||
AppPreferenceKeys.KeepScreenOnWhenStreamingEnabled,
|
|
||||||
AppDefaults.DefaultKeepScreenOnWhenStreamingEnabled,
|
|
||||||
),
|
|
||||||
virtualButtonsOutside = prefs.getString(
|
virtualButtonsOutside = prefs.getString(
|
||||||
AppPreferenceKeys.VirtualButtonsOutside,
|
AppPreferenceKeys.VIRTUAL_BUTTONS_OUTSIDE,
|
||||||
AppDefaults.DefaultVirtualButtonsOutside,
|
AppDefaults.VIRTUAL_BUTTONS_OUTSIDE,
|
||||||
).orEmpty().ifBlank { AppDefaults.DefaultVirtualButtonsOutside },
|
).orEmpty().ifBlank { AppDefaults.VIRTUAL_BUTTONS_OUTSIDE },
|
||||||
virtualButtonsInMore = prefs.getString(
|
virtualButtonsInMore = prefs.getString(
|
||||||
AppPreferenceKeys.VirtualButtonsInMore,
|
AppPreferenceKeys.VIRTUAL_BUTTONS_IN_MORE,
|
||||||
AppDefaults.DefaultVirtualButtonsInMore,
|
AppDefaults.VIRTUAL_BUTTONS_IN_MORE,
|
||||||
).orEmpty().ifBlank { AppDefaults.DefaultVirtualButtonsInMore },
|
).orEmpty().ifBlank { AppDefaults.VIRTUAL_BUTTONS_IN_MORE },
|
||||||
customServerUri = prefs.getString(AppPreferenceKeys.CustomServerUri, null),
|
customServerUri = prefs.getString(
|
||||||
|
AppPreferenceKeys.CUSTOM_SERVER_URI,
|
||||||
|
AppDefaults.CUSTOM_SERVER_URI
|
||||||
|
).orEmpty().ifBlank { null },
|
||||||
serverRemotePath = prefs.getString(
|
serverRemotePath = prefs.getString(
|
||||||
AppPreferenceKeys.ServerRemotePath,
|
AppPreferenceKeys.SERVER_REMOTE_PATH,
|
||||||
AppDefaults.DefaultServerRemotePathInput,
|
AppDefaults.SERVER_REMOTE_PATH_INPUT,
|
||||||
|
).orEmpty(),
|
||||||
|
adbKeyName = prefs.getString(
|
||||||
|
AppPreferenceKeys.ADB_KEY_NAME,
|
||||||
|
AppDefaults.ADB_KEY_NAME_INPUT,
|
||||||
).orEmpty(),
|
).orEmpty(),
|
||||||
videoCodec = prefs.getString(AppPreferenceKeys.VideoCodec, AppDefaults.DefaultVideoCodec)
|
|
||||||
.orEmpty()
|
|
||||||
.ifBlank { AppDefaults.DefaultVideoCodec },
|
|
||||||
audioEnabled = prefs.getBoolean(AppPreferenceKeys.AudioEnabled, AppDefaults.DefaultAudioEnabled),
|
|
||||||
audioCodec = prefs.getString(AppPreferenceKeys.AudioCodec, AppDefaults.DefaultAudioCodec)
|
|
||||||
.orEmpty()
|
|
||||||
.ifBlank { AppDefaults.DefaultAudioCodec },
|
|
||||||
adbKeyName = prefs.getString(AppPreferenceKeys.AdbKeyName, AppDefaults.DefaultAdbKeyNameInput).orEmpty(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun saveMainSettings(context: Context, settings: MainSettings) {
|
internal fun saveMainSettings(context: Context, settings: MainSettings) {
|
||||||
context.getSharedPreferences(AppPreferenceKeys.PrefsName, Context.MODE_PRIVATE)
|
context.getSharedPreferences(
|
||||||
.edit {
|
AppPreferenceKeys.PREFS_NAME,
|
||||||
putInt(AppPreferenceKeys.ThemeBaseIndex, settings.themeBaseIndex)
|
Context.MODE_PRIVATE,
|
||||||
.putBoolean(AppPreferenceKeys.MonetEnabled, settings.monetEnabled)
|
).edit {
|
||||||
.putBoolean(AppPreferenceKeys.FullscreenDebugInfoEnabled, settings.fullscreenDebugInfoEnabled)
|
putBoolean(
|
||||||
.putBoolean(AppPreferenceKeys.ShowFullscreenVirtualButtons, settings.showFullscreenVirtualButtons)
|
AppPreferenceKeys.AUDIO_ENABLED,
|
||||||
.putInt(AppPreferenceKeys.DevicePreviewCardHeightDp, settings.devicePreviewCardHeightDp.coerceAtLeast(120))
|
settings.audioEnabled,
|
||||||
.putBoolean(AppPreferenceKeys.KeepScreenOnWhenStreamingEnabled, settings.keepScreenOnWhenStreamingEnabled)
|
)
|
||||||
.putString(AppPreferenceKeys.VirtualButtonsOutside, settings.virtualButtonsOutside)
|
.putString(
|
||||||
.putString(AppPreferenceKeys.VirtualButtonsInMore, settings.virtualButtonsInMore)
|
AppPreferenceKeys.AUDIO_CODEC,
|
||||||
.putString(AppPreferenceKeys.CustomServerUri, settings.customServerUri)
|
settings.audioCodec,
|
||||||
.putString(AppPreferenceKeys.ServerRemotePath, settings.serverRemotePath)
|
)
|
||||||
.putString(AppPreferenceKeys.VideoCodec, settings.videoCodec)
|
.putString(
|
||||||
.putBoolean(AppPreferenceKeys.AudioEnabled, settings.audioEnabled)
|
AppPreferenceKeys.VIDEO_CODEC,
|
||||||
.putString(AppPreferenceKeys.AudioCodec, settings.audioCodec)
|
settings.videoCodec,
|
||||||
.putString(AppPreferenceKeys.AdbKeyName, settings.adbKeyName)
|
)
|
||||||
}
|
.putInt(
|
||||||
|
AppPreferenceKeys.THEME_BASE_INDEX,
|
||||||
|
settings.themeBaseIndex,
|
||||||
|
)
|
||||||
|
.putBoolean(
|
||||||
|
AppPreferenceKeys.MONET,
|
||||||
|
settings.monetEnabled,
|
||||||
|
)
|
||||||
|
.putBoolean(
|
||||||
|
AppPreferenceKeys.FULLSCREEN_DEBUG_INFO,
|
||||||
|
settings.fullscreenDebugInfoEnabled,
|
||||||
|
)
|
||||||
|
.putBoolean(
|
||||||
|
AppPreferenceKeys.SHOW_FULLSCREEN_VIRTUAL_BUTTONS,
|
||||||
|
settings.showFullscreenVirtualButtons,
|
||||||
|
)
|
||||||
|
.putBoolean(
|
||||||
|
AppPreferenceKeys.KEEP_SCREEN_ON_WHEN_STREAMING,
|
||||||
|
settings.keepScreenOnWhenStreamingEnabled,
|
||||||
|
)
|
||||||
|
.putInt(
|
||||||
|
AppPreferenceKeys.DEVICE_PREVIEW_CARD_HEIGHT_DP,
|
||||||
|
settings.devicePreviewCardHeightDp.coerceAtLeast(120),
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.VIRTUAL_BUTTONS_OUTSIDE,
|
||||||
|
settings.virtualButtonsOutside,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.VIRTUAL_BUTTONS_IN_MORE,
|
||||||
|
settings.virtualButtonsInMore,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.CUSTOM_SERVER_URI,
|
||||||
|
settings.customServerUri,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.SERVER_REMOTE_PATH,
|
||||||
|
settings.serverRemotePath,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.ADB_KEY_NAME,
|
||||||
|
settings.adbKeyName,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun loadDevicePageSettings(context: Context): DevicePageSettings {
|
internal fun loadDevicePageSettings(context: Context): DevicePageSettings {
|
||||||
val prefs = context.getSharedPreferences(AppPreferenceKeys.PrefsName, Context.MODE_PRIVATE)
|
val prefs = context.getSharedPreferences(
|
||||||
|
AppPreferenceKeys.PREFS_NAME,
|
||||||
|
Context.MODE_PRIVATE,
|
||||||
|
)
|
||||||
|
val audioBitRateKbps = prefs.getInt(
|
||||||
|
AppPreferenceKeys.AUDIO_BIT_RATE_KBPS,
|
||||||
|
AppDefaults.AUDIO_BIT_RATE_KBPS,
|
||||||
|
)
|
||||||
return DevicePageSettings(
|
return DevicePageSettings(
|
||||||
pairHost = AppDefaults.DefaultPairHost,
|
quickConnectInput = prefs.getString(
|
||||||
pairPort = AppDefaults.DefaultPairPort,
|
AppPreferenceKeys.QUICK_CONNECT_INPUT,
|
||||||
pairCode = AppDefaults.DefaultPairCode,
|
AppDefaults.QUICK_CONNECT_INPUT,
|
||||||
quickConnectInput = prefs.getString(AppPreferenceKeys.QuickConnectInput, AppDefaults.DefaultQuickConnectInput).orEmpty(),
|
).orEmpty(),
|
||||||
bitRateMbps = prefs.getFloat(AppPreferenceKeys.BitRateMbps, AppDefaults.DefaultBitRateMbps),
|
pairHost = AppDefaults.PAIR_HOST,
|
||||||
bitRateInput = prefs.getString(AppPreferenceKeys.BitRateInput, AppDefaults.DefaultBitRateInput)
|
pairPort = AppDefaults.PAIR_PORT,
|
||||||
.orEmpty()
|
pairCode = AppDefaults.PAIR_CODE,
|
||||||
.ifBlank { AppDefaults.DefaultBitRateInput },
|
audioBitRateKbps = audioBitRateKbps,
|
||||||
maxSizeInput = prefs.getString(AppPreferenceKeys.MaxSizeInput, AppDefaults.DefaultMaxSizeInput).orEmpty(),
|
audioBitRateInput = prefs.getString(
|
||||||
audioBitRateKbps = prefs.getInt(AppPreferenceKeys.AudioBitRateKbps, AppDefaults.DefaultAudioBitRateKbps),
|
AppPreferenceKeys.AUDIO_BIT_RATE_INPUT,
|
||||||
maxFpsInput = prefs.getString(AppPreferenceKeys.MaxFpsInput, AppDefaults.DefaultMaxFpsInput).orEmpty(),
|
AppDefaults.AUDIO_BIT_RATE_INPUT,
|
||||||
noControl = prefs.getBoolean(AppPreferenceKeys.NoControl, AppDefaults.DefaultNoControl),
|
).orEmpty().ifBlank { audioBitRateKbps.toString() },
|
||||||
videoEncoder = prefs.getString(AppPreferenceKeys.VideoEncoder, AppDefaults.DefaultVideoEncoder).orEmpty(),
|
videoBitRateMbps = prefs.getFloat(
|
||||||
videoCodecOptions = prefs.getString(AppPreferenceKeys.VideoCodecOptions, AppDefaults.DefaultVideoCodecOptions).orEmpty(),
|
AppPreferenceKeys.VIDEO_BIT_RATE_MBPS,
|
||||||
audioEncoder = prefs.getString(AppPreferenceKeys.AudioEncoder, AppDefaults.DefaultAudioEncoder).orEmpty(),
|
AppDefaults.VIDEO_BIT_RATE_MBPS,
|
||||||
audioCodecOptions = prefs.getString(AppPreferenceKeys.AudioCodecOptions, AppDefaults.DefaultAudioCodecOptions).orEmpty(),
|
),
|
||||||
audioDup = prefs.getBoolean(AppPreferenceKeys.AudioDup, AppDefaults.DefaultAudioDup),
|
videoBitRateInput = prefs.getString(
|
||||||
audioSourcePreset = prefs.getString(AppPreferenceKeys.AudioSourcePreset, AppDefaults.DefaultAudioSourcePreset)
|
AppPreferenceKeys.VIDEO_BIT_RATE_INPUT,
|
||||||
.orEmpty()
|
AppDefaults.VIDEO_BIT_RATE_INPUT
|
||||||
.ifBlank { AppDefaults.DefaultAudioSourcePreset },
|
).orEmpty().ifBlank { AppDefaults.VIDEO_BIT_RATE_INPUT },
|
||||||
audioSourceCustom = prefs.getString(AppPreferenceKeys.AudioSourceCustom, AppDefaults.DefaultAudioSourceCustom).orEmpty(),
|
turnScreenOff = prefs.getBoolean(
|
||||||
videoSourcePreset = prefs.getString(AppPreferenceKeys.VideoSourcePreset, AppDefaults.DefaultVideoSourcePreset)
|
AppPreferenceKeys.TURN_SCREEN_OFF,
|
||||||
.orEmpty()
|
AppDefaults.TURN_SCREEN_OFF,
|
||||||
.ifBlank { AppDefaults.DefaultVideoSourcePreset },
|
),
|
||||||
cameraIdInput = prefs.getString(AppPreferenceKeys.CameraIdInput, AppDefaults.DefaultCameraIdInput).orEmpty(),
|
noControl = prefs.getBoolean(
|
||||||
cameraFacingPreset = prefs.getString(AppPreferenceKeys.CameraFacingPreset, AppDefaults.DefaultCameraFacingPreset).orEmpty(),
|
AppPreferenceKeys.NO_CONTROL,
|
||||||
cameraSizePreset = prefs.getString(AppPreferenceKeys.CameraSizePreset, AppDefaults.DefaultCameraSizePreset).orEmpty(),
|
AppDefaults.NO_CONTROL,
|
||||||
cameraSizeCustom = prefs.getString(AppPreferenceKeys.CameraSizeCustom, AppDefaults.DefaultCameraSizeCustom).orEmpty(),
|
),
|
||||||
cameraArInput = prefs.getString(AppPreferenceKeys.CameraArInput, AppDefaults.DefaultCameraArInput).orEmpty(),
|
noVideo = prefs.getBoolean(
|
||||||
cameraFpsInput = prefs.getString(AppPreferenceKeys.CameraFpsInput, AppDefaults.DefaultCameraFpsInput).orEmpty(),
|
AppPreferenceKeys.NO_VIDEO,
|
||||||
cameraHighSpeed = prefs.getBoolean(AppPreferenceKeys.CameraHighSpeed, AppDefaults.DefaultCameraHighSpeed),
|
AppDefaults.NO_VIDEO,
|
||||||
noAudioPlayback = prefs.getBoolean(AppPreferenceKeys.NoAudioPlayback, AppDefaults.DefaultNoAudioPlayback),
|
),
|
||||||
noVideo = prefs.getBoolean(AppPreferenceKeys.NoVideo, AppDefaults.DefaultNoVideo),
|
videoSourcePreset = prefs.getString(
|
||||||
requireAudio = prefs.getBoolean(AppPreferenceKeys.RequireAudio, AppDefaults.DefaultRequireAudio),
|
AppPreferenceKeys.VIDEO_SOURCE_PRESET,
|
||||||
turnScreenOff = prefs.getBoolean(AppPreferenceKeys.TurnScreenOff, AppDefaults.DefaultTurnScreenOff),
|
AppDefaults.VIDEO_SOURCE_PRESET,
|
||||||
newDisplayWidth = prefs.getString(AppPreferenceKeys.NewDisplayWidth, AppDefaults.DefaultNewDisplayWidth).orEmpty(),
|
).orEmpty().ifBlank { AppDefaults.VIDEO_SOURCE_PRESET },
|
||||||
newDisplayHeight = prefs.getString(AppPreferenceKeys.NewDisplayHeight, AppDefaults.DefaultNewDisplayHeight).orEmpty(),
|
displayIdInput = prefs.getString(
|
||||||
newDisplayDpi = prefs.getString(AppPreferenceKeys.NewDisplayDpi, AppDefaults.DefaultNewDisplayDpi).orEmpty(),
|
AppPreferenceKeys.DISPLAY_ID,
|
||||||
displayIdInput = prefs.getString(AppPreferenceKeys.DisplayIdInput, AppDefaults.DefaultDisplayIdInput).orEmpty(),
|
AppDefaults.DISPLAY_ID,
|
||||||
cropWidth = prefs.getString(AppPreferenceKeys.CropWidth, AppDefaults.DefaultCropWidth).orEmpty(),
|
)
|
||||||
cropHeight = prefs.getString(AppPreferenceKeys.CropHeight, AppDefaults.DefaultCropHeight).orEmpty(),
|
.orEmpty(),
|
||||||
cropX = prefs.getString(AppPreferenceKeys.CropX, AppDefaults.DefaultCropX).orEmpty(),
|
cameraIdInput = prefs.getString(
|
||||||
cropY = prefs.getString(AppPreferenceKeys.CropY, AppDefaults.DefaultCropY).orEmpty(),
|
AppPreferenceKeys.CAMERA_ID,
|
||||||
|
AppDefaults.CAMERA_ID,
|
||||||
|
)
|
||||||
|
.orEmpty(),
|
||||||
|
cameraFacingPreset = prefs.getString(
|
||||||
|
AppPreferenceKeys.CAMERA_FACING_PRESET,
|
||||||
|
AppDefaults.CAMERA_FACING_PRESET,
|
||||||
|
).orEmpty(),
|
||||||
|
cameraSizePreset = prefs.getString(
|
||||||
|
AppPreferenceKeys.CAMERA_SIZE_PRESET,
|
||||||
|
AppDefaults.CAMERA_SIZE_PRESET,
|
||||||
|
).orEmpty(),
|
||||||
|
cameraSizeCustom = prefs.getString(
|
||||||
|
AppPreferenceKeys.CAMERA_SIZE_CUSTOM,
|
||||||
|
AppDefaults.CAMERA_SIZE_CUSTOM,
|
||||||
|
).orEmpty(),
|
||||||
|
cameraAr = prefs.getString(
|
||||||
|
AppPreferenceKeys.CAMERA_AR,
|
||||||
|
AppDefaults.CAMERA_AR,
|
||||||
|
).orEmpty(),
|
||||||
|
cameraFps = prefs.getString(
|
||||||
|
AppPreferenceKeys.CAMERA_FPS,
|
||||||
|
AppDefaults.CAMERA_FPS,
|
||||||
|
).orEmpty(),
|
||||||
|
cameraHighSpeed = prefs.getBoolean(
|
||||||
|
AppPreferenceKeys.CAMERA_HIGH_SPEED,
|
||||||
|
AppDefaults.CAMERA_HIGH_SPEED,
|
||||||
|
),
|
||||||
|
audioSourcePreset = prefs.getString(
|
||||||
|
AppPreferenceKeys.AUDIO_SOURCE_PRESET,
|
||||||
|
AppDefaults.AUDIO_SOURCE_PRESET,
|
||||||
|
).orEmpty().ifBlank { AppDefaults.AUDIO_SOURCE_PRESET },
|
||||||
|
audioSourceCustom = prefs.getString(
|
||||||
|
AppPreferenceKeys.AUDIO_SOURCE_CUSTOM,
|
||||||
|
AppDefaults.AUDIO_SOURCE_CUSTOM,
|
||||||
|
).orEmpty(),
|
||||||
|
audioDup = prefs.getBoolean(
|
||||||
|
AppPreferenceKeys.AUDIO_DUP,
|
||||||
|
AppDefaults.AUDIO_DUP,
|
||||||
|
),
|
||||||
|
noAudioPlayback = prefs.getBoolean(
|
||||||
|
AppPreferenceKeys.NO_AUDIO_PLAYBACK,
|
||||||
|
AppDefaults.NO_AUDIO_PLAYBACK,
|
||||||
|
),
|
||||||
|
requireAudio = prefs.getBoolean(
|
||||||
|
AppPreferenceKeys.REQUIRE_AUDIO,
|
||||||
|
AppDefaults.REQUIRE_AUDIO,
|
||||||
|
),
|
||||||
|
maxSizeInput = prefs.getString(
|
||||||
|
AppPreferenceKeys.MAX_SIZE_INPUT,
|
||||||
|
AppDefaults.MAX_SIZE_INPUT,
|
||||||
|
)
|
||||||
|
.orEmpty(),
|
||||||
|
maxFpsInput = prefs.getString(
|
||||||
|
AppPreferenceKeys.MAX_FPS_INPUT,
|
||||||
|
AppDefaults.MAX_FPS_INPUT,
|
||||||
|
)
|
||||||
|
.orEmpty(),
|
||||||
|
videoEncoder = prefs.getString(
|
||||||
|
AppPreferenceKeys.VIDEO_ENCODER,
|
||||||
|
AppDefaults.VIDEO_ENCODER,
|
||||||
|
)
|
||||||
|
.orEmpty(),
|
||||||
|
videoCodecOptions = prefs.getString(
|
||||||
|
AppPreferenceKeys.VIDEO_CODEC_OPTION,
|
||||||
|
AppDefaults.VIDEO_CODEC_OPTION,
|
||||||
|
).orEmpty(),
|
||||||
|
audioEncoder = prefs.getString(
|
||||||
|
AppPreferenceKeys.AUDIO_ENCODER,
|
||||||
|
AppDefaults.AUDIO_ENCODER,
|
||||||
|
).orEmpty(),
|
||||||
|
audioCodecOptions = prefs.getString(
|
||||||
|
AppPreferenceKeys.AUDIO_CODEC_OPTION,
|
||||||
|
AppDefaults.AUDIO_CODEC_OPTION,
|
||||||
|
).orEmpty(),
|
||||||
|
newDisplayWidth = prefs.getString(
|
||||||
|
AppPreferenceKeys.NEW_DISPLAY_WIDTH,
|
||||||
|
AppDefaults.NEW_DISPLAY_WIDTH,
|
||||||
|
).orEmpty(),
|
||||||
|
newDisplayHeight = prefs.getString(
|
||||||
|
AppPreferenceKeys.NEW_DISPLAY_HEIGHT,
|
||||||
|
AppDefaults.NEW_DISPLAY_HEIGHT,
|
||||||
|
).orEmpty(),
|
||||||
|
newDisplayDpi = prefs.getString(
|
||||||
|
AppPreferenceKeys.NEW_DISPLAY_DPI,
|
||||||
|
AppDefaults.NEW_DISPLAY_DPI,
|
||||||
|
).orEmpty(),
|
||||||
|
cropWidth = prefs.getString(
|
||||||
|
AppPreferenceKeys.CROP_WIDTH,
|
||||||
|
AppDefaults.CROP_WIDTH,
|
||||||
|
).orEmpty(),
|
||||||
|
cropHeight = prefs.getString(
|
||||||
|
AppPreferenceKeys.CROP_HEIGHT,
|
||||||
|
AppDefaults.CROP_HEIGHT,
|
||||||
|
).orEmpty(),
|
||||||
|
cropX = prefs.getString(
|
||||||
|
AppPreferenceKeys.CROP_X,
|
||||||
|
AppDefaults.CROP_X,
|
||||||
|
).orEmpty(),
|
||||||
|
cropY = prefs.getString(
|
||||||
|
AppPreferenceKeys.CROP_Y,
|
||||||
|
AppDefaults.CROP_Y,
|
||||||
|
).orEmpty(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun saveDevicePageSettings(context: Context, settings: DevicePageSettings) {
|
internal fun saveDevicePageSettings(context: Context, settings: DevicePageSettings) {
|
||||||
context.getSharedPreferences(AppPreferenceKeys.PrefsName, Context.MODE_PRIVATE)
|
context.getSharedPreferences(AppPreferenceKeys.PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
.edit {
|
.edit {
|
||||||
remove(AppPreferenceKeys.PairHost)
|
remove(AppPreferenceKeys.PAIR_HOST)
|
||||||
.remove(AppPreferenceKeys.PairPort)
|
.remove(AppPreferenceKeys.PAIR_PORT)
|
||||||
.remove(AppPreferenceKeys.PairCode)
|
.remove(AppPreferenceKeys.PAIR_CODE)
|
||||||
.putString(AppPreferenceKeys.QuickConnectInput, settings.quickConnectInput)
|
.putString(
|
||||||
.putFloat(AppPreferenceKeys.BitRateMbps, settings.bitRateMbps)
|
AppPreferenceKeys.QUICK_CONNECT_INPUT,
|
||||||
.putString(AppPreferenceKeys.BitRateInput, settings.bitRateInput)
|
settings.quickConnectInput,
|
||||||
.putInt(AppPreferenceKeys.AudioBitRateKbps, settings.audioBitRateKbps)
|
)
|
||||||
.putString(AppPreferenceKeys.MaxSizeInput, settings.maxSizeInput)
|
.putInt(
|
||||||
.putString(AppPreferenceKeys.MaxFpsInput, settings.maxFpsInput)
|
AppPreferenceKeys.AUDIO_BIT_RATE_KBPS,
|
||||||
.putBoolean(AppPreferenceKeys.NoControl, settings.noControl)
|
settings.audioBitRateKbps,
|
||||||
.putString(AppPreferenceKeys.VideoEncoder, settings.videoEncoder)
|
)
|
||||||
.putString(AppPreferenceKeys.VideoCodecOptions, settings.videoCodecOptions)
|
.putString(
|
||||||
.putString(AppPreferenceKeys.AudioEncoder, settings.audioEncoder)
|
AppPreferenceKeys.AUDIO_BIT_RATE_INPUT,
|
||||||
.putString(AppPreferenceKeys.AudioCodecOptions, settings.audioCodecOptions)
|
settings.audioBitRateInput,
|
||||||
.putBoolean(AppPreferenceKeys.AudioDup, settings.audioDup)
|
)
|
||||||
.putString(AppPreferenceKeys.AudioSourcePreset, settings.audioSourcePreset)
|
.putFloat(
|
||||||
.putString(AppPreferenceKeys.AudioSourceCustom, settings.audioSourceCustom)
|
AppPreferenceKeys.VIDEO_BIT_RATE_MBPS,
|
||||||
.putString(AppPreferenceKeys.VideoSourcePreset, settings.videoSourcePreset)
|
settings.videoBitRateMbps,
|
||||||
.putString(AppPreferenceKeys.CameraIdInput, settings.cameraIdInput)
|
)
|
||||||
.putString(AppPreferenceKeys.CameraFacingPreset, settings.cameraFacingPreset)
|
.putString(
|
||||||
.putString(AppPreferenceKeys.CameraSizePreset, settings.cameraSizePreset)
|
AppPreferenceKeys.VIDEO_BIT_RATE_INPUT,
|
||||||
.putString(AppPreferenceKeys.CameraSizeCustom, settings.cameraSizeCustom)
|
settings.videoBitRateInput,
|
||||||
.putString(AppPreferenceKeys.CameraArInput, settings.cameraArInput)
|
)
|
||||||
.putString(AppPreferenceKeys.CameraFpsInput, settings.cameraFpsInput)
|
.putBoolean(
|
||||||
.putBoolean(AppPreferenceKeys.CameraHighSpeed, settings.cameraHighSpeed)
|
AppPreferenceKeys.TURN_SCREEN_OFF,
|
||||||
.putBoolean(AppPreferenceKeys.NoAudioPlayback, settings.noAudioPlayback)
|
settings.turnScreenOff,
|
||||||
.putBoolean(AppPreferenceKeys.NoVideo, settings.noVideo)
|
)
|
||||||
.putBoolean(AppPreferenceKeys.RequireAudio, settings.requireAudio)
|
.putBoolean(
|
||||||
.putBoolean(AppPreferenceKeys.TurnScreenOff, settings.turnScreenOff)
|
AppPreferenceKeys.NO_CONTROL,
|
||||||
.putString(AppPreferenceKeys.NewDisplayWidth, settings.newDisplayWidth)
|
settings.noControl,
|
||||||
.putString(AppPreferenceKeys.NewDisplayHeight, settings.newDisplayHeight)
|
)
|
||||||
.putString(AppPreferenceKeys.NewDisplayDpi, settings.newDisplayDpi)
|
.putBoolean(
|
||||||
.putString(AppPreferenceKeys.DisplayIdInput, settings.displayIdInput)
|
AppPreferenceKeys.NO_VIDEO,
|
||||||
.putString(AppPreferenceKeys.CropWidth, settings.cropWidth)
|
settings.noVideo,
|
||||||
.putString(AppPreferenceKeys.CropHeight, settings.cropHeight)
|
)
|
||||||
.putString(AppPreferenceKeys.CropX, settings.cropX)
|
.putString(
|
||||||
.putString(AppPreferenceKeys.CropY, settings.cropY)
|
AppPreferenceKeys.VIDEO_SOURCE_PRESET,
|
||||||
|
settings.videoSourcePreset,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.DISPLAY_ID,
|
||||||
|
settings.displayIdInput,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.CAMERA_ID,
|
||||||
|
settings.cameraIdInput,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.CAMERA_FACING_PRESET,
|
||||||
|
settings.cameraFacingPreset,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.CAMERA_SIZE_PRESET,
|
||||||
|
settings.cameraSizePreset,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.CAMERA_SIZE_CUSTOM,
|
||||||
|
settings.cameraSizeCustom,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.CAMERA_AR,
|
||||||
|
settings.cameraAr,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.CAMERA_FPS,
|
||||||
|
settings.cameraFps,
|
||||||
|
)
|
||||||
|
.putBoolean(
|
||||||
|
AppPreferenceKeys.CAMERA_HIGH_SPEED,
|
||||||
|
settings.cameraHighSpeed,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.AUDIO_SOURCE_PRESET,
|
||||||
|
settings.audioSourcePreset,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.AUDIO_SOURCE_CUSTOM,
|
||||||
|
settings.audioSourceCustom,
|
||||||
|
)
|
||||||
|
.putBoolean(
|
||||||
|
AppPreferenceKeys.AUDIO_DUP,
|
||||||
|
settings.audioDup,
|
||||||
|
)
|
||||||
|
.putBoolean(
|
||||||
|
AppPreferenceKeys.NO_AUDIO_PLAYBACK,
|
||||||
|
settings.noAudioPlayback,
|
||||||
|
)
|
||||||
|
.putBoolean(
|
||||||
|
AppPreferenceKeys.REQUIRE_AUDIO,
|
||||||
|
settings.requireAudio,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.MAX_SIZE_INPUT,
|
||||||
|
settings.maxSizeInput,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.MAX_FPS_INPUT,
|
||||||
|
settings.maxFpsInput,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.VIDEO_ENCODER,
|
||||||
|
settings.videoEncoder,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.VIDEO_CODEC_OPTION,
|
||||||
|
settings.videoCodecOptions,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.AUDIO_ENCODER,
|
||||||
|
settings.audioEncoder,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.AUDIO_CODEC_OPTION,
|
||||||
|
settings.audioCodecOptions,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.NEW_DISPLAY_WIDTH,
|
||||||
|
settings.newDisplayWidth,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.NEW_DISPLAY_HEIGHT,
|
||||||
|
settings.newDisplayHeight,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.NEW_DISPLAY_DPI,
|
||||||
|
settings.newDisplayDpi,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.CROP_WIDTH,
|
||||||
|
settings.cropWidth,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.CROP_HEIGHT,
|
||||||
|
settings.cropHeight,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.CROP_X,
|
||||||
|
settings.cropX,
|
||||||
|
)
|
||||||
|
.putString(
|
||||||
|
AppPreferenceKeys.CROP_Y,
|
||||||
|
settings.cropY,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,13 @@ internal data class ConnectedDeviceInfo(
|
|||||||
val sdkInt: Int,
|
val sdkInt: Int,
|
||||||
)
|
)
|
||||||
|
|
||||||
internal fun fetchConnectedDeviceInfo(nativeCore: NativeCoreFacade, host: String, port: Int): ConnectedDeviceInfo {
|
internal fun fetchConnectedDeviceInfo(
|
||||||
fun prop(name: String): String = runCatching { nativeCore.adbShell("getprop $name").trim() }.getOrDefault("")
|
nativeCore: NativeCoreFacade,
|
||||||
|
host: String,
|
||||||
|
port: Int
|
||||||
|
): ConnectedDeviceInfo {
|
||||||
|
fun prop(name: String): String =
|
||||||
|
runCatching { nativeCore.adbShell("getprop $name").trim() }.getOrDefault("")
|
||||||
|
|
||||||
val model = prop("ro.product.model")
|
val model = prop("ro.product.model")
|
||||||
val serial = prop("ro.serialno")
|
val serial = prop("ro.serialno")
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget
|
|||||||
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
|
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
|
||||||
|
|
||||||
internal fun loadQuickDevices(context: Context): List<DeviceShortcut> {
|
internal fun loadQuickDevices(context: Context): List<DeviceShortcut> {
|
||||||
val raw = context.getSharedPreferences(AppPreferenceKeys.PrefsName, Context.MODE_PRIVATE)
|
val raw = context.getSharedPreferences(AppPreferenceKeys.PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
.getString(AppPreferenceKeys.QuickDevices, "")
|
.getString(AppPreferenceKeys.QUICK_DEVICES, "")
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
|
|
||||||
if (raw.isBlank()) return emptyList()
|
if (raw.isBlank()) return emptyList()
|
||||||
@@ -21,7 +21,7 @@ internal fun loadQuickDevices(context: Context): List<DeviceShortcut> {
|
|||||||
3 -> {
|
3 -> {
|
||||||
val name = parts[0].trim()
|
val name = parts[0].trim()
|
||||||
val host = parts[1].trim()
|
val host = parts[1].trim()
|
||||||
val port = parts[2].trim().toIntOrNull() ?: AppDefaults.DefaultAdbPort
|
val port = parts[2].trim().toIntOrNull() ?: AppDefaults.ADB_PORT
|
||||||
if (host.isNotBlank()) {
|
if (host.isNotBlank()) {
|
||||||
result.add(
|
result.add(
|
||||||
DeviceShortcut(
|
DeviceShortcut(
|
||||||
@@ -39,7 +39,8 @@ internal fun loadQuickDevices(context: Context): List<DeviceShortcut> {
|
|||||||
// Backward compatibility with old format: name|host:port
|
// Backward compatibility with old format: name|host:port
|
||||||
val name = parts[0].trim()
|
val name = parts[0].trim()
|
||||||
val host = parts[1].substringBefore(":").trim()
|
val host = parts[1].substringBefore(":").trim()
|
||||||
val port = parts[1].substringAfter(":", AppDefaults.DefaultAdbPort.toString()).trim().toIntOrNull() ?: AppDefaults.DefaultAdbPort
|
val port = parts[1].substringAfter(":", AppDefaults.ADB_PORT.toString()).trim()
|
||||||
|
.toIntOrNull() ?: AppDefaults.ADB_PORT
|
||||||
if (host.isNotBlank()) {
|
if (host.isNotBlank()) {
|
||||||
result.add(
|
result.add(
|
||||||
DeviceShortcut(
|
DeviceShortcut(
|
||||||
@@ -59,9 +60,9 @@ internal fun loadQuickDevices(context: Context): List<DeviceShortcut> {
|
|||||||
|
|
||||||
internal fun saveQuickDevices(context: Context, quickDevices: List<DeviceShortcut>) {
|
internal fun saveQuickDevices(context: Context, quickDevices: List<DeviceShortcut>) {
|
||||||
val raw = quickDevices.joinToString("\n") { "${it.name}|${it.host}|${it.port}" }
|
val raw = quickDevices.joinToString("\n") { "${it.name}|${it.host}|${it.port}" }
|
||||||
context.getSharedPreferences(AppPreferenceKeys.PrefsName, Context.MODE_PRIVATE)
|
context.getSharedPreferences(AppPreferenceKeys.PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
.edit {
|
.edit {
|
||||||
putString(AppPreferenceKeys.QuickDevices, raw)
|
putString(AppPreferenceKeys.QUICK_DEVICES, raw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +71,8 @@ internal fun parseQuickTarget(raw: String): ConnectionTarget? {
|
|||||||
if (value.isEmpty()) return null
|
if (value.isEmpty()) return null
|
||||||
val host = value.substringBefore(':').trim()
|
val host = value.substringBefore(':').trim()
|
||||||
if (host.isEmpty()) return null
|
if (host.isEmpty()) return null
|
||||||
val port = value.substringAfter(':', AppDefaults.DefaultAdbPort.toString()).trim().toIntOrNull() ?: AppDefaults.DefaultAdbPort
|
val port = value.substringAfter(':', AppDefaults.ADB_PORT.toString()).trim().toIntOrNull()
|
||||||
|
?: AppDefaults.ADB_PORT
|
||||||
return ConnectionTarget(host, port)
|
return ConnectionTarget(host, port)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import androidx.compose.foundation.gestures.detectTapGestures
|
|||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
@@ -32,11 +31,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.filled.AddLink
|
import androidx.compose.material.icons.filled.AddLink
|
||||||
import androidx.compose.material.icons.filled.Fullscreen
|
import androidx.compose.material.icons.filled.Fullscreen
|
||||||
import androidx.compose.material.icons.filled.Home
|
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
|
||||||
import androidx.compose.material.icons.rounded.CheckCircleOutline
|
import androidx.compose.material.icons.rounded.CheckCircleOutline
|
||||||
import androidx.compose.material.icons.rounded.LinkOff
|
import androidx.compose.material.icons.rounded.LinkOff
|
||||||
import androidx.compose.material.icons.rounded.Wifi
|
import androidx.compose.material.icons.rounded.Wifi
|
||||||
@@ -63,7 +59,6 @@ import androidx.compose.ui.text.input.ImeAction
|
|||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
@@ -96,7 +91,7 @@ import kotlin.math.roundToInt
|
|||||||
private val VIDEO_CODEC_OPTIONS = listOf(
|
private val VIDEO_CODEC_OPTIONS = listOf(
|
||||||
"h264" to "H.264",
|
"h264" to "H.264",
|
||||||
"h265" to "H.265",
|
"h265" to "H.265",
|
||||||
"av1" to "AV1",
|
"av1" to "AV1",
|
||||||
)
|
)
|
||||||
|
|
||||||
private val AUDIO_CODEC_OPTIONS = listOf(
|
private val AUDIO_CODEC_OPTIONS = listOf(
|
||||||
@@ -266,9 +261,13 @@ internal fun PreviewCard(
|
|||||||
}
|
}
|
||||||
val containerAspect = maxWidth.value / maxHeight.value
|
val containerAspect = maxWidth.value / maxHeight.value
|
||||||
val fittedModifier = if (sessionAspect > containerAspect) {
|
val fittedModifier = if (sessionAspect > containerAspect) {
|
||||||
Modifier.fillMaxWidth().aspectRatio(sessionAspect)
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(sessionAspect)
|
||||||
} else {
|
} else {
|
||||||
Modifier.fillMaxHeight().aspectRatio(sessionAspect)
|
Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.aspectRatio(sessionAspect)
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
@@ -358,10 +357,13 @@ internal fun ConfigPanel(
|
|||||||
sessionStarted: Boolean,
|
sessionStarted: Boolean,
|
||||||
) {
|
) {
|
||||||
val videoCodecItems = remember { VIDEO_CODEC_OPTIONS.map { it.second } }
|
val videoCodecItems = remember { VIDEO_CODEC_OPTIONS.map { it.second } }
|
||||||
val videoCodecIndex = VIDEO_CODEC_OPTIONS.indexOfFirst { it.first == videoCodec }.coerceAtLeast(0)
|
val videoCodecIndex =
|
||||||
|
VIDEO_CODEC_OPTIONS.indexOfFirst { it.first == videoCodec }.coerceAtLeast(0)
|
||||||
val audioCodecItems = remember { AUDIO_CODEC_OPTIONS.map { it.second } }
|
val audioCodecItems = remember { AUDIO_CODEC_OPTIONS.map { it.second } }
|
||||||
val audioCodecIndex = AUDIO_CODEC_OPTIONS.indexOfFirst { it.first == audioCodec }.coerceAtLeast(0)
|
val audioCodecIndex =
|
||||||
val audioBitRatePresetIndex = presetIndexFromInput(audioBitRateKbps.toString(), ScrcpyPresets.AudioBitRate)
|
AUDIO_CODEC_OPTIONS.indexOfFirst { it.first == audioCodec }.coerceAtLeast(0)
|
||||||
|
val audioBitRatePresetIndex =
|
||||||
|
presetIndexFromInput(audioBitRateKbps.toString(), ScrcpyPresets.AudioBitRate)
|
||||||
|
|
||||||
SectionSmallTitle(text = "Scrcpy")
|
SectionSmallTitle(text = "Scrcpy")
|
||||||
Card {
|
Card {
|
||||||
@@ -433,6 +435,7 @@ internal fun ConfigPanel(
|
|||||||
dotUsed = true
|
dotUsed = true
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -506,7 +509,9 @@ private fun PairingDialog(
|
|||||||
onValueChange = { host = it },
|
onValueChange = { host = it },
|
||||||
label = "IP 地址",
|
label = "IP 地址",
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth().padding(bottom = UiSpacing.CardContent),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = UiSpacing.CardContent),
|
||||||
)
|
)
|
||||||
TextField(
|
TextField(
|
||||||
value = port,
|
value = port,
|
||||||
@@ -514,14 +519,18 @@ private fun PairingDialog(
|
|||||||
label = "端口",
|
label = "端口",
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
modifier = Modifier.fillMaxWidth().padding(bottom = UiSpacing.CardContent),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = UiSpacing.CardContent),
|
||||||
)
|
)
|
||||||
TextField(
|
TextField(
|
||||||
value = code,
|
value = code,
|
||||||
onValueChange = { code = it },
|
onValueChange = { code = it },
|
||||||
label = "WLAN 配对码",
|
label = "WLAN 配对码",
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth().padding(bottom = UiSpacing.Large),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = UiSpacing.Large),
|
||||||
)
|
)
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(UiSpacing.PopupHorizontal)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(UiSpacing.PopupHorizontal)) {
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -599,8 +608,10 @@ fun FullscreenControlScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun mapToDevice(rawX: Float, rawY: Float): Pair<Int, Int> {
|
fun mapToDevice(rawX: Float, rawY: Float): Pair<Int, Int> {
|
||||||
val x = ((rawX / touchAreaSize.width) * session.width).roundToInt().coerceIn(0, (session.width - 1).coerceAtLeast(0))
|
val x = ((rawX / touchAreaSize.width) * session.width).roundToInt()
|
||||||
val y = ((rawY / touchAreaSize.height) * session.height).roundToInt().coerceIn(0, (session.height - 1).coerceAtLeast(0))
|
.coerceIn(0, (session.width - 1).coerceAtLeast(0))
|
||||||
|
val y = ((rawY / touchAreaSize.height) * session.height).roundToInt()
|
||||||
|
.coerceIn(0, (session.height - 1).coerceAtLeast(0))
|
||||||
return x to y
|
return x to y
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,7 +623,14 @@ fun FullscreenControlScreen(
|
|||||||
val py = event.getY(i)
|
val py = event.getY(i)
|
||||||
activePointerPositions[pointerId] = Offset(px, py)
|
activePointerPositions[pointerId] = Offset(px, py)
|
||||||
val (x, y) = mapToDevice(px, py)
|
val (x, y) = mapToDevice(px, py)
|
||||||
onInjectTouch(UiMotionActions.MOVE, pointerId.toLong(), x, y, event.getPressure(i).coerceIn(0f, 1f), 1)
|
onInjectTouch(
|
||||||
|
UiMotionActions.MOVE,
|
||||||
|
pointerId.toLong(),
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
event.getPressure(i).coerceIn(0f, 1f),
|
||||||
|
1
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -626,7 +644,14 @@ fun FullscreenControlScreen(
|
|||||||
activePointerIds += pointerId
|
activePointerIds += pointerId
|
||||||
activePointerPositions[pointerId] = Offset(event.x, event.y)
|
activePointerPositions[pointerId] = Offset(event.x, event.y)
|
||||||
activeTouchCount = activePointerIds.size
|
activeTouchCount = activePointerIds.size
|
||||||
onInjectTouch(UiMotionActions.DOWN, pointerId.toLong(), x, y, event.getPressure(0).coerceIn(0f, 1f), 1)
|
onInjectTouch(
|
||||||
|
UiMotionActions.DOWN,
|
||||||
|
pointerId.toLong(),
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
event.getPressure(0).coerceIn(0f, 1f),
|
||||||
|
1
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
MotionEvent.ACTION_POINTER_DOWN -> {
|
MotionEvent.ACTION_POINTER_DOWN -> {
|
||||||
@@ -638,7 +663,14 @@ fun FullscreenControlScreen(
|
|||||||
activePointerIds += pointerId
|
activePointerIds += pointerId
|
||||||
activePointerPositions[pointerId] = Offset(px, py)
|
activePointerPositions[pointerId] = Offset(px, py)
|
||||||
activeTouchCount = activePointerIds.size
|
activeTouchCount = activePointerIds.size
|
||||||
onInjectTouch(UiMotionActions.POINTER_DOWN, pointerId.toLong(), x, y, event.getPressure(index).coerceIn(0f, 1f), 1)
|
onInjectTouch(
|
||||||
|
UiMotionActions.POINTER_DOWN,
|
||||||
|
pointerId.toLong(),
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
event.getPressure(index).coerceIn(0f, 1f),
|
||||||
|
1
|
||||||
|
)
|
||||||
syncActivePointersFromEvent()
|
syncActivePointersFromEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,7 +682,14 @@ fun FullscreenControlScreen(
|
|||||||
val py = event.getY(i)
|
val py = event.getY(i)
|
||||||
activePointerPositions[pointerId] = Offset(px, py)
|
activePointerPositions[pointerId] = Offset(px, py)
|
||||||
val (x, y) = mapToDevice(px, py)
|
val (x, y) = mapToDevice(px, py)
|
||||||
onInjectTouch(UiMotionActions.MOVE, pointerId.toLong(), x, y, event.getPressure(i).coerceIn(0f, 1f), 1)
|
onInjectTouch(
|
||||||
|
UiMotionActions.MOVE,
|
||||||
|
pointerId.toLong(),
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
event.getPressure(i).coerceIn(0f, 1f),
|
||||||
|
1
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -698,9 +737,13 @@ fun FullscreenControlScreen(
|
|||||||
}
|
}
|
||||||
val containerAspect = maxWidth.value / maxHeight.value
|
val containerAspect = maxWidth.value / maxHeight.value
|
||||||
val fittedModifier = if (sessionAspect > containerAspect) {
|
val fittedModifier = if (sessionAspect > containerAspect) {
|
||||||
Modifier.fillMaxWidth().aspectRatio(sessionAspect)
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(sessionAspect)
|
||||||
} else {
|
} else {
|
||||||
Modifier.fillMaxHeight().aspectRatio(sessionAspect)
|
Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.aspectRatio(sessionAspect)
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
@@ -780,13 +823,21 @@ private fun ScrcpyVideoSurface(
|
|||||||
factory = { context ->
|
factory = { context ->
|
||||||
TextureView(context).apply {
|
TextureView(context).apply {
|
||||||
surfaceTextureListener = object : TextureView.SurfaceTextureListener {
|
surfaceTextureListener = object : TextureView.SurfaceTextureListener {
|
||||||
override fun onSurfaceTextureAvailable(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
|
override fun onSurfaceTextureAvailable(
|
||||||
|
surfaceTexture: SurfaceTexture,
|
||||||
|
width: Int,
|
||||||
|
height: Int
|
||||||
|
) {
|
||||||
currentSurface?.release() // Release stale surface if any
|
currentSurface?.release() // Release stale surface if any
|
||||||
@SuppressLint("Recycle")
|
@SuppressLint("Recycle")
|
||||||
currentSurface = Surface(surfaceTexture)
|
currentSurface = Surface(surfaceTexture)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSurfaceTextureSizeChanged(surfaceTexture: SurfaceTexture, width: Int, height: Int) = Unit
|
override fun onSurfaceTextureSizeChanged(
|
||||||
|
surfaceTexture: SurfaceTexture,
|
||||||
|
width: Int,
|
||||||
|
height: Int
|
||||||
|
) = Unit
|
||||||
|
|
||||||
override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean {
|
override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean {
|
||||||
val released = currentSurface
|
val released = currentSurface
|
||||||
@@ -820,7 +871,7 @@ internal fun DeviceTile(
|
|||||||
Card(
|
Card(
|
||||||
colors = CardDefaults.defaultColors(
|
colors = CardDefaults.defaultColors(
|
||||||
color = if (device.online) MiuixTheme.colorScheme.surfaceContainer
|
color = if (device.online) MiuixTheme.colorScheme.surfaceContainer
|
||||||
else MiuixTheme.colorScheme.surfaceContainer.copy(alpha = 0.6f),
|
else MiuixTheme.colorScheme.surfaceContainer.copy(alpha = 0.6f),
|
||||||
),
|
),
|
||||||
pressFeedbackType = PressFeedbackType.Sink,
|
pressFeedbackType = PressFeedbackType.Sink,
|
||||||
onClick = haptics.press,
|
onClick = haptics.press,
|
||||||
@@ -1016,7 +1067,9 @@ internal fun DeviceEditorScreen(
|
|||||||
.padding(top = UiSpacing.CardContent),
|
.padding(top = UiSpacing.CardContent),
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(UiSpacing.CardContent),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(UiSpacing.CardContent),
|
||||||
horizontalArrangement = Arrangement.spacedBy(UiSpacing.Medium),
|
horizontalArrangement = Arrangement.spacedBy(UiSpacing.Medium),
|
||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -1032,7 +1085,7 @@ internal fun DeviceEditorScreen(
|
|||||||
TextButton(
|
TextButton(
|
||||||
text = "保存",
|
text = "保存",
|
||||||
onClick = {
|
onClick = {
|
||||||
val p = port.toIntOrNull() ?: AppDefaults.DefaultAdbPort
|
val p = port.toIntOrNull() ?: AppDefaults.ADB_PORT
|
||||||
val h = host.trim()
|
val h = host.trim()
|
||||||
if (h.isNotBlank()) {
|
if (h.isNotBlank()) {
|
||||||
onSave(
|
onSave(
|
||||||
|
|||||||
@@ -110,7 +110,10 @@ fun SortableCardList(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = UiSpacing.CardContent, vertical = UiSpacing.FieldLabelBottom),
|
.padding(
|
||||||
|
horizontal = UiSpacing.CardContent,
|
||||||
|
vertical = UiSpacing.FieldLabelBottom
|
||||||
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
|||||||
@@ -69,12 +69,16 @@ internal fun StatusCardLayout(
|
|||||||
) {
|
) {
|
||||||
val haptics = rememberAppHaptics()
|
val haptics = rememberAppHaptics()
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(IntrinsicSize.Min),
|
||||||
horizontalArrangement = Arrangement.spacedBy(UiSpacing.PageItem),
|
horizontalArrangement = Arrangement.spacedBy(UiSpacing.PageItem),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.weight(1f).fillMaxHeight(),
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxHeight(),
|
||||||
colors = defaultColors(color = spec.big.containerColor),
|
colors = defaultColors(color = spec.big.containerColor),
|
||||||
pressFeedbackType = PressFeedbackType.Tilt,
|
pressFeedbackType = PressFeedbackType.Tilt,
|
||||||
onClick = haptics.press,
|
onClick = haptics.press,
|
||||||
@@ -125,15 +129,21 @@ internal fun StatusCardLayout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
|
Column(modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxHeight()) {
|
||||||
StatusMetricCard(
|
StatusMetricCard(
|
||||||
spec = spec.firstSmall,
|
spec = spec.firstSmall,
|
||||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(UiSpacing.PageItem))
|
Spacer(Modifier.height(UiSpacing.PageItem))
|
||||||
StatusMetricCard(
|
StatusMetricCard(
|
||||||
spec = spec.secondSmall,
|
spec = spec.secondSmall,
|
||||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,6 @@ import androidx.compose.material.icons.filled.MoreVert
|
|||||||
import androidx.compose.material.icons.filled.Notifications
|
import androidx.compose.material.icons.filled.Notifications
|
||||||
import androidx.compose.material.icons.filled.PhotoCamera
|
import androidx.compose.material.icons.filled.PhotoCamera
|
||||||
import androidx.compose.material.icons.filled.PowerSettingsNew
|
import androidx.compose.material.icons.filled.PowerSettingsNew
|
||||||
import androidx.compose.material.icons.filled.VolumeDown
|
|
||||||
import androidx.compose.material.icons.filled.VolumeOff
|
|
||||||
import androidx.compose.material.icons.filled.VolumeUp
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -50,36 +47,76 @@ enum class VirtualButtonAction(
|
|||||||
val icon: ImageVector,
|
val icon: ImageVector,
|
||||||
val keycode: Int?,
|
val keycode: Int?,
|
||||||
) {
|
) {
|
||||||
MORE("more", "更多", Icons.Default.MoreVert, null),
|
MORE(
|
||||||
HOME("home", "主页", Icons.Default.Home, UiAndroidKeycodes.HOME),
|
"more",
|
||||||
BACK("back", "返回", Icons.AutoMirrored.Filled.ArrowBack, UiAndroidKeycodes.BACK),
|
"更多",
|
||||||
APP_SWITCH("app_switch", "多任务", Icons.Default.Apps, UiAndroidKeycodes.APP_SWITCH),
|
Icons.Default.MoreVert,
|
||||||
MENU("menu", "菜单", Icons.Default.Menu, UiAndroidKeycodes.MENU),
|
null
|
||||||
NOTIFICATION("notification", "通知栏", Icons.Default.Notifications, UiAndroidKeycodes.NOTIFICATION),
|
),
|
||||||
VOLUME_UP("volume_up", "音量+", Icons.AutoMirrored.Filled.VolumeUp, UiAndroidKeycodes.VOLUME_UP),
|
HOME(
|
||||||
VOLUME_DOWN("volume_down", "音量-", Icons.AutoMirrored.Filled.VolumeDown, UiAndroidKeycodes.VOLUME_DOWN),
|
"home",
|
||||||
VOLUME_MUTE("volume_mute", "静音", Icons.AutoMirrored.Filled.VolumeOff, UiAndroidKeycodes.VOLUME_MUTE),
|
"主页",
|
||||||
POWER("power", "锁屏", Icons.Default.PowerSettingsNew, UiAndroidKeycodes.POWER),
|
Icons.Default.Home,
|
||||||
SCREENSHOT("screenshot", "截图", Icons.Default.PhotoCamera, UiAndroidKeycodes.SYSRQ),
|
UiAndroidKeycodes.HOME
|
||||||
|
),
|
||||||
|
BACK(
|
||||||
|
"back",
|
||||||
|
"返回",
|
||||||
|
Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
UiAndroidKeycodes.BACK
|
||||||
|
),
|
||||||
|
APP_SWITCH(
|
||||||
|
"app_switch",
|
||||||
|
"多任务",
|
||||||
|
Icons.Default.Apps,
|
||||||
|
UiAndroidKeycodes.APP_SWITCH
|
||||||
|
),
|
||||||
|
MENU(
|
||||||
|
"menu",
|
||||||
|
"菜单",
|
||||||
|
Icons.Default.Menu,
|
||||||
|
UiAndroidKeycodes.MENU
|
||||||
|
),
|
||||||
|
NOTIFICATION(
|
||||||
|
"notification",
|
||||||
|
"通知栏",
|
||||||
|
Icons.Default.Notifications,
|
||||||
|
UiAndroidKeycodes.NOTIFICATION
|
||||||
|
),
|
||||||
|
VOLUME_UP(
|
||||||
|
"volume_up",
|
||||||
|
"音量+",
|
||||||
|
Icons.AutoMirrored.Filled.VolumeUp,
|
||||||
|
UiAndroidKeycodes.VOLUME_UP
|
||||||
|
),
|
||||||
|
VOLUME_DOWN(
|
||||||
|
"volume_down",
|
||||||
|
"音量-",
|
||||||
|
Icons.AutoMirrored.Filled.VolumeDown,
|
||||||
|
UiAndroidKeycodes.VOLUME_DOWN
|
||||||
|
),
|
||||||
|
VOLUME_MUTE(
|
||||||
|
"volume_mute",
|
||||||
|
"静音",
|
||||||
|
Icons.AutoMirrored.Filled.VolumeOff,
|
||||||
|
UiAndroidKeycodes.VOLUME_MUTE
|
||||||
|
),
|
||||||
|
POWER(
|
||||||
|
"power",
|
||||||
|
"锁屏",
|
||||||
|
Icons.Default.PowerSettingsNew,
|
||||||
|
UiAndroidKeycodes.POWER
|
||||||
|
),
|
||||||
|
SCREENSHOT(
|
||||||
|
"screenshot",
|
||||||
|
"截图",
|
||||||
|
Icons.Default.PhotoCamera,
|
||||||
|
UiAndroidKeycodes.SYSRQ
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
object VirtualButtonActions {
|
object VirtualButtonActions {
|
||||||
val all = VirtualButtonAction.entries
|
val all = VirtualButtonAction.entries
|
||||||
val defaultOutsideIds = listOf(
|
|
||||||
VirtualButtonAction.MORE.id,
|
|
||||||
VirtualButtonAction.HOME.id,
|
|
||||||
VirtualButtonAction.BACK.id,
|
|
||||||
)
|
|
||||||
val defaultMoreIds = listOf(
|
|
||||||
VirtualButtonAction.APP_SWITCH.id,
|
|
||||||
VirtualButtonAction.MENU.id,
|
|
||||||
VirtualButtonAction.NOTIFICATION.id,
|
|
||||||
VirtualButtonAction.VOLUME_UP.id,
|
|
||||||
VirtualButtonAction.VOLUME_DOWN.id,
|
|
||||||
VirtualButtonAction.VOLUME_MUTE.id,
|
|
||||||
VirtualButtonAction.POWER.id,
|
|
||||||
VirtualButtonAction.SCREENSHOT.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val byId = all.associateBy { it.id }
|
private val byId = all.associateBy { it.id }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user