refactor: organize codes; add README.md

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

45
README.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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