chore: format codes

This commit is contained in:
Miuzarte
2026-05-31 17:48:05 +08:00
parent 7f256882a4
commit 56c724d282
66 changed files with 422 additions and 821 deletions

View File

@@ -21,7 +21,7 @@ import kotlinx.coroutines.runBlocking
import java.util.Locale import java.util.Locale
// 生物认证需要 FragmentActivity // 生物认证需要 FragmentActivity
class MainActivity : FragmentActivity() { class MainActivity: FragmentActivity() {
override fun attachBaseContext(newBase: Context) { override fun attachBaseContext(newBase: Context) {
val languageTag = getAppLanguageTag(newBase) val languageTag = getAppLanguageTag(newBase)

View File

@@ -11,7 +11,6 @@ import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@@ -89,7 +88,10 @@ object NativeCoreFacade {
} }
} }
if (session.width <= 0 || session.height <= 0) { if (session.width <= 0 || session.height <= 0) {
Log.i(TAG, "attachVideoSurface(): defer decoder, session size not yet known (${session.width}x${session.height})") Log.i(
TAG,
"attachVideoSurface(): defer decoder, session size not yet known (${session.width}x${session.height})",
)
return return
} }
createOrReplaceDecoder(session) createOrReplaceDecoder(session)
@@ -112,13 +114,13 @@ object NativeCoreFacade {
if (requestId != null && currentId != null && requestId != currentId) { if (requestId != null && currentId != null && requestId != currentId) {
Log.i( Log.i(
TAG, TAG,
"detachVideoSurface(): skip stale request requestSurfaceId=$requestId currentSurfaceId=$currentId" "detachVideoSurface(): skip stale request requestSurfaceId=$requestId currentSurfaceId=$currentId",
) )
return return
} }
Log.i( Log.i(
TAG, TAG,
"detachVideoSurface(): surfaceId=$requestId releaseDecoder=$releaseDecoder" "detachVideoSurface(): surfaceId=$requestId releaseDecoder=$releaseDecoder",
) )
activeSurfaceId = null activeSurfaceId = null
renderer.detachDisplaySurface(surface, releaseSurface = false) renderer.detachDisplaySurface(surface, releaseSurface = false)
@@ -197,7 +199,7 @@ object NativeCoreFacade {
if (packetCount == 1L || packetCount % 120L == 0L) { if (packetCount == 1L || packetCount % 120L == 0L) {
Log.i( Log.i(
TAG, TAG,
"videoFeed(): packets=$packetCount key=${packet.isKeyFrame} cfg=${packet.isConfig} decoder=${decoder != null}" "videoFeed(): packets=$packetCount key=${packet.isKeyFrame} cfg=${packet.isConfig} decoder=${decoder != null}",
) )
} }
@@ -207,7 +209,7 @@ object NativeCoreFacade {
packet.data, packet.data,
packet.ptsUs, packet.ptsUs,
packet.isKeyFrame, packet.isKeyFrame,
packet.isConfig packet.isConfig,
) )
} }
} }
@@ -233,7 +235,10 @@ object NativeCoreFacade {
(info.width != currentSessionInfo!!.width || info.height != currentSessionInfo!!.height) (info.width != currentSessionInfo!!.width || info.height != currentSessionInfo!!.height)
) { ) {
// Flex display: rebuild decoder on size change // Flex display: rebuild decoder on size change
Log.i(TAG, "onVideoSizeChanged(): rebuild decoder ${currentSessionInfo!!.width}x${currentSessionInfo!!.height}${info.width}x${info.height}") Log.i(
TAG,
"onVideoSizeChanged(): rebuild decoder ${currentSessionInfo!!.width}x${currentSessionInfo!!.height}${info.width}x${info.height}",
)
currentSessionInfo = info currentSessionInfo = info
createOrReplaceDecoder(info) createOrReplaceDecoder(info)
} }
@@ -306,7 +311,7 @@ object NativeCoreFacade {
"createOrReplaceDecoder(): " + "createOrReplaceDecoder(): " +
"codec=${session.codec?.string ?: "null"}, " + "codec=${session.codec?.string ?: "null"}, " +
"size=${session.width}x${session.height}, " + "size=${session.width}x${session.height}, " +
"persistent=true" "persistent=true",
) )
val newDecoder = AnnexBDecoder( val newDecoder = AnnexBDecoder(
width = session.width, width = session.width,
@@ -325,7 +330,7 @@ object NativeCoreFacade {
} }
Log.i( Log.i(
TAG, TAG,
"videoSizeChanged(): ${current.width}x${current.height} -> ${width}x${height}" "videoSizeChanged(): ${current.width}x${current.height} -> ${width}x${height}",
) )
currentSessionInfo = current.copy(width = width, height = height) currentSessionInfo = current.copy(width = width, height = height)
mainHandler.post { mainHandler.post {

View File

@@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
class StreamActivity : FragmentActivity() { class StreamActivity: FragmentActivity() {
private val basicPip = BasicPictureInPicture(this) private val basicPip = BasicPictureInPicture(this)
private val pipActionReceiver = PictureInPictureActionReceiver() private val pipActionReceiver = PictureInPictureActionReceiver()

View File

@@ -7,7 +7,7 @@ package io.github.miuzarte.scrcpyforandroid.constants
* @param T The type of preset values (typically Int) * @param T The type of preset values (typically Int)
* @param values The list of preset values * @param values The list of preset values
*/ */
class Preset<T : Comparable<T>>(val values: List<T>) { class Preset<T: Comparable<T>>(val values: List<T>) {
val lastIndex: Int get() = values.lastIndex val lastIndex: Int get() = values.lastIndex
val size: Int get() = values.size val size: Int get() = values.size
val indices: IntRange get() = values.indices val indices: IntRange get() = values.indices

View File

@@ -3,12 +3,7 @@ package io.github.miuzarte.scrcpyforandroid.miuix
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -18,15 +13,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import top.yukonga.miuix.kmp.basic.BasicComponent import top.yukonga.miuix.kmp.basic.*
import top.yukonga.miuix.kmp.basic.BasicComponentColors
import top.yukonga.miuix.kmp.basic.BasicComponentDefaults
import top.yukonga.miuix.kmp.basic.DropdownArrowEndAction
import top.yukonga.miuix.kmp.basic.DropdownColors
import top.yukonga.miuix.kmp.basic.DropdownDefaults
import top.yukonga.miuix.kmp.basic.ListPopupColumn
import top.yukonga.miuix.kmp.basic.PopupPositionProvider
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.overlay.OverlayListPopup import top.yukonga.miuix.kmp.overlay.OverlayListPopup
import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.theme.MiuixTheme

View File

@@ -2,15 +2,7 @@ package io.github.miuzarte.scrcpyforandroid.miuix
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.widthIn
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState

View File

@@ -6,7 +6,7 @@ import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
// Composable 用, 不可变 List // Composable 用, 不可变 List
class DeviceShortcuts(val devices: List<DeviceShortcut>) : List<DeviceShortcut> by devices { class DeviceShortcuts(val devices: List<DeviceShortcut>): List<DeviceShortcut> by devices {
fun marshalToString( fun marshalToString(
separator: String = DEFAULT_SEPARATOR, separator: String = DEFAULT_SEPARATOR,
): String = joinToString(separator) { it.marshalToString() } ): String = joinToString(separator) { it.marshalToString() }
@@ -97,7 +97,7 @@ class DeviceShortcuts(val devices: List<DeviceShortcut>) : List<DeviceShortcut>
|| (newPort != null && newPort != old.port) || (newPort != null && newPort != old.port)
) )
newList.distinctBy { it.id } newList.distinctBy { it.id }
else newList else newList,
) )
} }
@@ -176,7 +176,7 @@ data class DeviceShortcut(
if (openFullscreenOnStart) "1" else "0", if (openFullscreenOnStart) "1" else "0",
scrcpyProfileId.trim(), scrcpyProfileId.trim(),
).joinToString( ).joinToString(
separator = separator separator = separator,
) )
companion object { companion object {
@@ -259,7 +259,7 @@ data class DeviceShortcut(
data class ConnectionTarget( data class ConnectionTarget(
val host: String, val host: String,
val port: Int = Defaults.ADB_PORT, val port: Int = Defaults.ADB_PORT,
) : Parcelable { ): Parcelable {
override fun toString(): String = override fun toString(): String =
if (':' in host) "[$host]:$port" if (':' in host) "[$host]:$port"
else "$host:$port" else "$host:$port"

View File

@@ -2,9 +2,9 @@ package io.github.miuzarte.scrcpyforandroid.nativecore
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Build
import android.net.nsd.NsdManager import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo import android.net.nsd.NsdServiceInfo
import android.os.Build
import android.util.Log import android.util.Log
import java.io.IOException import java.io.IOException
import java.net.InetSocketAddress import java.net.InetSocketAddress
@@ -56,7 +56,7 @@ internal object AdbMdnsDiscoverer {
val discoveryFinished = AtomicBoolean(false) val discoveryFinished = AtomicBoolean(false)
val latch = CountDownLatch(1) val latch = CountDownLatch(1)
val discoveryListener = object : NsdManager.DiscoveryListener { val discoveryListener = object: NsdManager.DiscoveryListener {
override fun onDiscoveryStarted(serviceType: String) { override fun onDiscoveryStarted(serviceType: String) {
Log.v(TAG, "discovery started: $serviceType") Log.v(TAG, "discovery started: $serviceType")
} }
@@ -79,7 +79,7 @@ internal object AdbMdnsDiscoverer {
override fun onServiceFound(serviceInfo: NsdServiceInfo) { override fun onServiceFound(serviceInfo: NsdServiceInfo) {
if (discoveryFinished.get()) return if (discoveryFinished.get()) return
Log.v(TAG, "service found: ${serviceInfo.serviceName}") Log.v(TAG, "service found: ${serviceInfo.serviceName}")
val resolveListener = object : NsdManager.ResolveListener { val resolveListener = object: NsdManager.ResolveListener {
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
Log.v(TAG, "resolve failed: ${serviceInfo.serviceName}, error=$errorCode") Log.v(TAG, "resolve failed: ${serviceInfo.serviceName}, error=$errorCode")
} }

View File

@@ -11,11 +11,7 @@ import java.math.BigInteger
import java.net.Socket import java.net.Socket
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
import java.security.KeyFactory import java.security.*
import java.security.PrivateKey
import java.security.Provider
import java.security.SecureRandom
import java.security.Security
import java.security.cert.CertificateFactory import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPrivateKey
@@ -73,7 +69,7 @@ internal class AdbPairingKey(
} }
private val keyManager: X509ExtendedKeyManager private val keyManager: X509ExtendedKeyManager
get() = object : X509ExtendedKeyManager() { get() = object: X509ExtendedKeyManager() {
private val keyAlias = "adbkey" private val keyAlias = "adbkey"
override fun chooseClientAlias( override fun chooseClientAlias(
@@ -110,7 +106,7 @@ internal class AdbPairingKey(
@get:SuppressLint("CustomX509TrustManager") @get:SuppressLint("CustomX509TrustManager")
@get:Suppress("TrustAllX509TrustManager") @get:Suppress("TrustAllX509TrustManager")
private val trustManager: X509ExtendedTrustManager private val trustManager: X509ExtendedTrustManager
get() = object : X509ExtendedTrustManager() { get() = object: X509ExtendedTrustManager() {
// ADB pairing uses SPAKE2 + exported keying material to authenticate the peer. // ADB pairing uses SPAKE2 + exported keying material to authenticate the peer.
// The peer cert is ephemeral/self-signed, so PKIX validation is intentionally bypassed here. // The peer cert is ephemeral/self-signed, so PKIX validation is intentionally bypassed here.
private fun acceptForPairing( private fun acceptForPairing(
@@ -213,4 +209,4 @@ private fun RSAPublicKey.adbEncoded(name: String): ByteArray {
/** /**
* Thrown when the supplied pairing code is invalid during the pairing flow. * Thrown when the supplied pairing code is invalid during the pairing flow.
*/ */
internal class AdbInvalidPairingCodeException : Exception() internal class AdbInvalidPairingCodeException: Exception()

View File

@@ -94,14 +94,14 @@ class AnnexBDecoder(
if (inCount == 1L || inCount % 180L == 0L || isConfig) { if (inCount == 1L || inCount % 180L == 0L || isConfig) {
Log.i( Log.i(
TAG, TAG,
"feed(): mime=$decoderMime in=$inCount size=${data.size} key=$isKeyFrame cfg=$isConfig pts=$ptsUs" "feed(): mime=$decoderMime in=$inCount size=${data.size} key=$isKeyFrame cfg=$isConfig pts=$ptsUs",
) )
} }
} else { } else {
if (isConfig || isKeyFrame) { if (isConfig || isKeyFrame) {
Log.w( Log.w(
TAG, TAG,
"drop critical packet: mime=$decoderMime size=${data.size} key=$isKeyFrame cfg=$isConfig" "drop critical packet: mime=$decoderMime size=${data.size} key=$isKeyFrame cfg=$isConfig",
) )
} }
} }
@@ -110,7 +110,7 @@ class AnnexBDecoder(
Log.w( Log.w(
TAG, TAG,
"feed failed: mime=$decoderMime size=${data.size} key=$isKeyFrame cfg=$isConfig", "feed failed: mime=$decoderMime size=${data.size} key=$isKeyFrame cfg=$isConfig",
it it,
) )
} }
} }

View File

@@ -7,24 +7,13 @@ import io.github.miuzarte.scrcpyforandroid.storage.AdbClientData
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
import io.github.miuzarte.scrcpyforandroid.storage.Storage import io.github.miuzarte.scrcpyforandroid.storage.Storage
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.io.BufferedInputStream import java.io.*
import java.io.ByteArrayOutputStream
import java.io.Closeable
import java.io.EOFException
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.math.BigInteger import java.math.BigInteger
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.Socket import java.net.Socket
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
import java.security.KeyFactory import java.security.*
import java.security.KeyPairGenerator
import java.security.MessageDigest
import java.security.PrivateKey
import java.security.PublicKey
import java.security.Signature
import java.security.interfaces.RSAPrivateCrtKey import java.security.interfaces.RSAPrivateCrtKey
import java.security.interfaces.RSAPublicKey import java.security.interfaces.RSAPublicKey
import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.PKCS8EncodedKeySpec
@@ -72,7 +61,8 @@ internal object DirectAdbTransport {
port, port,
privateKey, privateKey,
publicKeyX509, publicKeyX509,
keyName.ifBlank { AppSettings.ADB_KEY_NAME.defaultValue }) keyName.ifBlank { AppSettings.ADB_KEY_NAME.defaultValue },
)
conn.handshake() conn.handshake()
Log.i(TAG, "connect(): handshake success for $host:$port") Log.i(TAG, "connect(): handshake success for $host:$port")
return conn return conn
@@ -186,7 +176,7 @@ internal object DirectAdbTransport {
importedPrivateKeyFileName = "", importedPrivateKeyFileName = "",
importedPublicKeyX509 = "", importedPublicKeyX509 = "",
importedPublicKeyFileName = "", importedPublicKeyFileName = "",
) ),
) )
loadOrCreate() loadOrCreate()
}.also { cachedKeys = it } }.also { cachedKeys = it }
@@ -232,21 +222,21 @@ internal object DirectAdbTransport {
current.copy( current.copy(
rsaPublicKeyX509 = encoded, rsaPublicKeyX509 = encoded,
) )
} },
) )
} }
Log.i( Log.i(
TAG, TAG,
"loadOrCreate(): loaded persisted RSA key pair from DataStore, " + "loadOrCreate(): loaded persisted RSA key pair from DataStore, " +
"fp=${fingerprint(pub)}" "fp=${fingerprint(pub)}",
) )
return Pair(priv, pub) return Pair(priv, pub)
} catch (e: Exception) { } catch (e: Exception) {
Log.w( Log.w(
TAG, TAG,
"loadOrCreate(): failed to load persisted key from DataStore, regenerating", "loadOrCreate(): failed to load persisted key from DataStore, regenerating",
e e,
) )
} }
} }
@@ -264,12 +254,12 @@ internal object DirectAdbTransport {
importedPrivateKeyFileName = "", importedPrivateKeyFileName = "",
importedPublicKeyX509 = "", importedPublicKeyX509 = "",
importedPublicKeyFileName = "", importedPublicKeyFileName = "",
) ),
) )
Log.i( Log.i(
TAG, TAG,
"loadOrCreate(): generated new RSA key pair, fp=${fingerprint(kp.public.encoded)}" "loadOrCreate(): generated new RSA key pair, fp=${fingerprint(kp.public.encoded)}",
) )
return Pair(kp.private, kp.public.encoded) return Pair(kp.private, kp.public.encoded)
} }
@@ -427,7 +417,7 @@ internal object DirectAdbTransport {
val exponent = buf.int.toLong() and 0xffffffffL val exponent = buf.int.toLong() and 0xffffffffL
val modulus = BigInteger(1, modulusLE.reversedArray()) val modulus = BigInteger(1, modulusLE.reversedArray())
return KeyFactory.getInstance("RSA").generatePublic( return KeyFactory.getInstance("RSA").generatePublic(
RSAPublicKeySpec(modulus, BigInteger.valueOf(exponent)) RSAPublicKeySpec(modulus, BigInteger.valueOf(exponent)),
) )
} }
@@ -538,7 +528,7 @@ internal class DirectAdbConnection(
private val privateKey: PrivateKey, private val privateKey: PrivateKey,
private val publicKeyX509: ByteArray, private val publicKeyX509: ByteArray,
private val keyName: String = AppSettings.ADB_KEY_NAME.defaultValue, private val keyName: String = AppSettings.ADB_KEY_NAME.defaultValue,
) : AutoCloseable { ): AutoCloseable {
private val sha1DigestInfoPrefix = byteArrayOf( private val sha1DigestInfoPrefix = byteArrayOf(
0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2B, 0x0E, 0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2B, 0x0E,
@@ -623,9 +613,9 @@ internal class DirectAdbConnection(
else -> throw IOException( else -> throw IOException(
"ADB: unexpected message 0x${ "ADB: unexpected message 0x${
afterSign.command.toString( afterSign.command.toString(
16 16,
) )
} after AUTH_SIGNATURE" } after AUTH_SIGNATURE",
) )
} }
} }
@@ -837,7 +827,7 @@ internal class DirectAdbConnection(
command: Int, command: Int,
arg0: Int = 0, arg0: Int = 0,
arg1: Int = 0, arg1: Int = 0,
data: ByteArray = ByteArray(0) data: ByteArray = ByteArray(0),
) { ) {
val crc = data.fold(0L) { acc, b -> acc + (b.toLong() and 0xFF) }.toInt() val crc = data.fold(0L) { acc, b -> acc + (b.toLong() and 0xFF) }.toInt()
val header = ByteBuffer.allocate(24).order(ByteOrder.LITTLE_ENDIAN) val header = ByteBuffer.allocate(24).order(ByteOrder.LITTLE_ENDIAN)
@@ -957,7 +947,7 @@ internal class DirectAdbConnection(
class AdbSocketStream( class AdbSocketStream(
val localId: Int, val localId: Int,
private val sender: (cmd: Int, arg0: Int, arg1: Int, `data`: ByteArray) -> Unit, private val sender: (cmd: Int, arg0: Int, arg1: Int, `data`: ByteArray) -> Unit,
) : Closeable { ): Closeable {
companion object { companion object {
private const val A_WRTE = 0x45545257 private const val A_WRTE = 0x45545257
@@ -1016,7 +1006,7 @@ class AdbSocketStream(
queue.offer(EndOfStreamMarker) queue.offer(EndOfStreamMarker)
} }
private inner class InStream : InputStream() { private inner class InStream: InputStream() {
private var chunk: ByteArray? = null private var chunk: ByteArray? = null
private var off = 0 private var off = 0
@@ -1047,7 +1037,7 @@ class AdbSocketStream(
override fun available(): Int = chunk?.let { it.size - off } ?: 0 override fun available(): Int = chunk?.let { it.size - off } ?: 0
} }
private inner class OutStream : OutputStream() { private inner class OutStream: OutputStream() {
override fun write(b: Int) = write(byteArrayOf(b.toByte())) override fun write(b: Int) = write(byteArrayOf(b.toByte()))
override fun write(b: ByteArray, off: Int, len: Int) { override fun write(b: ByteArray, off: Int, len: Int) {
if (closed) throw IOException("ADB stream closed") if (closed) throw IOException("ADB stream closed")

View File

@@ -134,7 +134,7 @@ internal class DirectAdbPairingClient(
private val port: Int, private val port: Int,
private val pairingCode: String, private val pairingCode: String,
private val key: AdbPairingKey, private val key: AdbPairingKey,
) : Closeable { ): Closeable {
private enum class State { private enum class State {
READY, READY,

View File

@@ -190,7 +190,7 @@ object NativeAdbService {
.map(String::trim) .map(String::trim)
.lastOrNull { '/' in it } .lastOrNull { '/' in it }
?: throw IllegalStateException( ?: throw IllegalStateException(
"Cannot resolve launch activity for $normalizedPackageName" "Cannot resolve launch activity for $normalizedPackageName",
) )
val displayArg = displayId val displayArg = displayId

View File

@@ -1,15 +1,7 @@
package io.github.miuzarte.scrcpyforandroid.nativecore package io.github.miuzarte.scrcpyforandroid.nativecore
import android.graphics.SurfaceTexture import android.graphics.SurfaceTexture
import android.opengl.EGL14 import android.opengl.*
import android.opengl.EGLConfig
import android.opengl.EGLContext
import android.opengl.EGLDisplay
import android.opengl.EGLExt
import android.opengl.EGLSurface
import android.opengl.GLES11Ext
import android.opengl.GLES20
import android.opengl.Matrix
import android.os.Handler import android.os.Handler
import android.os.HandlerThread import android.os.HandlerThread
import android.util.Log import android.util.Log
@@ -85,7 +77,7 @@ class PersistentVideoRenderer {
eglConfig, eglConfig,
surface, surface,
intArrayOf(EGL14.EGL_NONE), intArrayOf(EGL14.EGL_NONE),
0 0,
) )
Log.i(tag, "attachDisplaySurface(): attached surfaceId=$newId") Log.i(tag, "attachDisplaySurface(): attached surfaceId=$newId")
drawFrame() drawFrame()
@@ -96,7 +88,7 @@ class PersistentVideoRenderer {
val requestId = surface?.let { System.identityHashCode(it) } val requestId = surface?.let { System.identityHashCode(it) }
Log.i( Log.i(
tag, tag,
"detachDisplaySurface(): request surfaceId=$requestId releaseSurface=$releaseSurface current=${displaySurfaceId}" "detachDisplaySurface(): request surfaceId=$requestId releaseSurface=$releaseSurface current=${displaySurfaceId}",
) )
handler.post { handler.post {
if (released) return@post if (released) return@post
@@ -137,7 +129,7 @@ class PersistentVideoRenderer {
eglConfig, eglConfig,
surface, surface,
intArrayOf(EGL14.EGL_NONE), intArrayOf(EGL14.EGL_NONE),
0 0,
) )
Log.i(tag, "attachRecordSurface(): attached surfaceId=$newId") Log.i(tag, "attachRecordSurface(): attached surfaceId=$newId")
drawFrame() drawFrame()
@@ -148,7 +140,7 @@ class PersistentVideoRenderer {
val requestId = surface?.let { System.identityHashCode(it) } val requestId = surface?.let { System.identityHashCode(it) }
Log.i( Log.i(
tag, tag,
"detachRecordSurface(): request surfaceId=$requestId releaseSurface=$releaseSurface current=$recordSurfaceId" "detachRecordSurface(): request surfaceId=$requestId releaseSurface=$releaseSurface current=$recordSurfaceId",
) )
handler.post { handler.post {
if (released) return@post if (released) return@post
@@ -183,7 +175,7 @@ class PersistentVideoRenderer {
eglDisplay, eglDisplay,
EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
EGL14.EGL_NO_CONTEXT EGL14.EGL_NO_CONTEXT,
) )
if (eglPbufferSurface != EGL14.EGL_NO_SURFACE) { if (eglPbufferSurface != EGL14.EGL_NO_SURFACE) {
EGL14.eglDestroySurface(eglDisplay, eglPbufferSurface) EGL14.eglDestroySurface(eglDisplay, eglPbufferSurface)
@@ -227,7 +219,7 @@ class PersistentVideoRenderer {
EGL14.EGL_GREEN_SIZE, 8, EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8, EGL14.EGL_BLUE_SIZE, 8,
EGL14.EGL_ALPHA_SIZE, 8, EGL14.EGL_ALPHA_SIZE, 8,
EGL14.EGL_NONE EGL14.EGL_NONE,
) )
check(EGL14.eglChooseConfig(eglDisplay, attribs, 0, configs, 0, 1, numConfigs, 0)) check(EGL14.eglChooseConfig(eglDisplay, attribs, 0, configs, 0, 1, numConfigs, 0))
eglConfig = configs[0] eglConfig = configs[0]
@@ -236,30 +228,33 @@ class PersistentVideoRenderer {
eglConfig, eglConfig,
EGL14.EGL_NO_CONTEXT, EGL14.EGL_NO_CONTEXT,
intArrayOf(0x3098, 2, EGL14.EGL_NONE), intArrayOf(0x3098, 2, EGL14.EGL_NONE),
0 0,
) )
check(eglContext != EGL14.EGL_NO_CONTEXT) check(eglContext != EGL14.EGL_NO_CONTEXT)
eglPbufferSurface = EGL14.eglCreatePbufferSurface( eglPbufferSurface = EGL14.eglCreatePbufferSurface(
eglDisplay, eglDisplay,
eglConfig, eglConfig,
intArrayOf(EGL14.EGL_WIDTH, 1, EGL14.EGL_HEIGHT, 1, EGL14.EGL_NONE), intArrayOf(EGL14.EGL_WIDTH, 1, EGL14.EGL_HEIGHT, 1, EGL14.EGL_NONE),
0 0,
) )
check(eglPbufferSurface != EGL14.EGL_NO_SURFACE) check(eglPbufferSurface != EGL14.EGL_NO_SURFACE)
check(EGL14.eglMakeCurrent(eglDisplay, eglPbufferSurface, eglPbufferSurface, eglContext)) check(EGL14.eglMakeCurrent(eglDisplay, eglPbufferSurface, eglPbufferSurface, eglContext))
oesTextureId = createExternalTexture() oesTextureId = createExternalTexture()
decoderSurfaceTexture = SurfaceTexture(oesTextureId).apply { decoderSurfaceTexture = SurfaceTexture(oesTextureId).apply {
setOnFrameAvailableListener({ setOnFrameAvailableListener(
val n = frameAvailableCount.incrementAndGet() {
if (n == 1L || n % 120L == 0L) { val n = frameAvailableCount.incrementAndGet()
Log.i( if (n == 1L || n % 120L == 0L) {
tag, Log.i(
"onFrameAvailable(): available=$n consumed=${frameConsumedCount.get()} rendered=${frameRenderedCount.get()} display=${displaySurfaceId != null}" tag,
) "onFrameAvailable(): available=$n consumed=${frameConsumedCount.get()} rendered=${frameRenderedCount.get()} display=${displaySurfaceId != null}",
} )
drawFrame() }
}, handler) drawFrame()
},
handler,
)
} }
decoderSurface = Surface(decoderSurfaceTexture) decoderSurface = Surface(decoderSurfaceTexture)
Log.i(tag, "initializeLocked(): decoder surface created") Log.i(tag, "initializeLocked(): decoder surface created")
@@ -290,7 +285,7 @@ class PersistentVideoRenderer {
if (consumed == 1L || consumed % 120L == 0L) { if (consumed == 1L || consumed % 120L == 0L) {
Log.i( Log.i(
tag, tag,
"drawFrame(): consumed=$consumed available=${frameAvailableCount.get()} rendered=${frameRenderedCount.get()} display=${displaySurfaceId != null}" "drawFrame(): consumed=$consumed available=${frameAvailableCount.get()} rendered=${frameRenderedCount.get()} display=${displaySurfaceId != null}",
) )
} }
} }
@@ -322,7 +317,7 @@ class PersistentVideoRenderer {
if (rendered == 1L || rendered % 120L == 0L) { if (rendered == 1L || rendered % 120L == 0L) {
Log.i( Log.i(
tag, tag,
"drawFrame(): rendered=$rendered consumed=${frameConsumedCount.get()} available=${frameAvailableCount.get()} viewport=${width[0]}x${height[0]}" "drawFrame(): rendered=$rendered consumed=${frameConsumedCount.get()} available=${frameAvailableCount.get()} viewport=${width[0]}x${height[0]}",
) )
} }
} }
@@ -391,22 +386,22 @@ class PersistentVideoRenderer {
GLES20.glTexParameteri( GLES20.glTexParameteri(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_TEXTURE_MIN_FILTER,
GLES20.GL_LINEAR GLES20.GL_LINEAR,
) )
GLES20.glTexParameteri( GLES20.glTexParameteri(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_TEXTURE_MAG_FILTER,
GLES20.GL_LINEAR GLES20.GL_LINEAR,
) )
GLES20.glTexParameteri( GLES20.glTexParameteri(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_TEXTURE_WRAP_S,
GLES20.GL_CLAMP_TO_EDGE GLES20.GL_CLAMP_TO_EDGE,
) )
GLES20.glTexParameteri( GLES20.glTexParameteri(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_TEXTURE_WRAP_T,
GLES20.GL_CLAMP_TO_EDGE GLES20.GL_CLAMP_TO_EDGE,
) )
return textures[0] return textures[0]
} }
@@ -439,7 +434,7 @@ class PersistentVideoRenderer {
1f, -1f, 1f, 1f, 1f, -1f, 1f, 1f,
-1f, 1f, 0f, 0f, -1f, 1f, 0f, 0f,
1f, 1f, 1f, 0f, 1f, 1f, 1f, 0f,
) ),
) )
position(0) position(0)
} }

View File

@@ -3,12 +3,7 @@ package io.github.miuzarte.scrcpyforandroid.nativecore
// Go reader note: Audio output helper for scrcpy stream: decodes/plays PCM or codec audio frames. // Go reader note: Audio output helper for scrcpy stream: decodes/plays PCM or codec audio frames.
import android.content.Context import android.content.Context
import android.media.AudioAttributes import android.media.*
import android.media.AudioFormat
import android.media.AudioManager
import android.media.AudioTrack
import android.media.MediaCodec
import android.media.MediaFormat
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec
@@ -54,7 +49,7 @@ class ScrcpyAudioPlayer(
TAG, TAG,
"feedPacket(): config packet size=${data.size} codec=0x${ "feedPacket(): config packet size=${data.size} codec=0x${
codecId.toUInt().toString(16) codecId.toUInt().toString(16)
}" }",
) )
when (codecId) { when (codecId) {
Codec.OPUS.id -> prepareOpus(data) Codec.OPUS.id -> prepareOpus(data)
@@ -101,7 +96,7 @@ class ScrcpyAudioPlayer(
val format = MediaFormat.createAudioFormat( val format = MediaFormat.createAudioFormat(
MediaFormat.MIMETYPE_AUDIO_OPUS, MediaFormat.MIMETYPE_AUDIO_OPUS,
SAMPLE_RATE, SAMPLE_RATE,
CHANNELS CHANNELS,
) )
format.setByteBuffer("csd-0", ByteBuffer.wrap(opusHead)) format.setByteBuffer("csd-0", ByteBuffer.wrap(opusHead))
// pre-skip field: 2 bytes LE at offset 10 of the OpusHead // pre-skip field: 2 bytes LE at offset 10 of the OpusHead
@@ -132,7 +127,7 @@ class ScrcpyAudioPlayer(
val format = MediaFormat.createAudioFormat( val format = MediaFormat.createAudioFormat(
MediaFormat.MIMETYPE_AUDIO_FLAC, MediaFormat.MIMETYPE_AUDIO_FLAC,
SAMPLE_RATE, SAMPLE_RATE,
CHANNELS CHANNELS,
) )
if (flacConfig.isNotEmpty()) { if (flacConfig.isNotEmpty()) {
format.setByteBuffer("csd-0", ByteBuffer.wrap(flacConfig)) format.setByteBuffer("csd-0", ByteBuffer.wrap(flacConfig))
@@ -196,11 +191,11 @@ class ScrcpyAudioPlayer(
val attributesBuilder = AudioAttributes.Builder() val attributesBuilder = AudioAttributes.Builder()
.setUsage( .setUsage(
if (lowLatency) AudioAttributes.USAGE_GAME if (lowLatency) AudioAttributes.USAGE_GAME
else AudioAttributes.USAGE_MEDIA else AudioAttributes.USAGE_MEDIA,
) )
.setContentType( .setContentType(
if (lowLatency) AudioAttributes.CONTENT_TYPE_SONIFICATION if (lowLatency) AudioAttributes.CONTENT_TYPE_SONIFICATION
else AudioAttributes.CONTENT_TYPE_MOVIE else AudioAttributes.CONTENT_TYPE_MOVIE,
) )
if (lowLatency) { if (lowLatency) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@@ -215,7 +210,7 @@ class ScrcpyAudioPlayer(
.setEncoding(AudioFormat.ENCODING_PCM_16BIT) .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(SAMPLE_RATE) .setSampleRate(SAMPLE_RATE)
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO) .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
.build() .build(),
) )
.setBufferSizeInBytes(bufferSize) .setBufferSizeInBytes(bufferSize)
.setTransferMode(AudioTrack.MODE_STREAM) .setTransferMode(AudioTrack.MODE_STREAM)
@@ -229,7 +224,7 @@ class ScrcpyAudioPlayer(
Log.i( Log.i(
TAG, TAG,
"low-latency audio requested: nativeSampleRate=$nativeSampleRate streamSampleRate=$SAMPLE_RATE " + "low-latency audio requested: nativeSampleRate=$nativeSampleRate streamSampleRate=$SAMPLE_RATE " +
"framesPerBurst=$framesPerBurst bufferSize=$bufferSize performanceMode=${track.performanceMode}" "framesPerBurst=$framesPerBurst bufferSize=$bufferSize performanceMode=${track.performanceMode}",
) )
} }
return track return track

View File

@@ -11,13 +11,8 @@ import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcuts import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcuts
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime import io.github.miuzarte.scrcpyforandroid.services.*
import io.github.miuzarte.scrcpyforandroid.services.AppScreenOn
import io.github.miuzarte.scrcpyforandroid.services.DisconnectCause
import io.github.miuzarte.scrcpyforandroid.services.EventLogMessage
import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent
import io.github.miuzarte.scrcpyforandroid.services.LocalInputService
import io.github.miuzarte.scrcpyforandroid.services.render
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions
import io.github.miuzarte.scrcpyforandroid.storage.Settings import io.github.miuzarte.scrcpyforandroid.storage.Settings
@@ -27,25 +22,9 @@ import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions
import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyProfiles import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyProfiles
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
private const val ADB_CONNECT_TIMEOUT_MS = 12_000L private const val ADB_CONNECT_TIMEOUT_MS = 12_000L
private const val ADB_KEEPALIVE_INTERVAL_MS = 3_000L private const val ADB_KEEPALIVE_INTERVAL_MS = 3_000L
@@ -58,7 +37,7 @@ private const val ADB_TCP_PROBE_TIMEOUT_MS = 500
internal class DeviceTabViewModel( internal class DeviceTabViewModel(
internal val scrcpy: Scrcpy, internal val scrcpy: Scrcpy,
connectionServices: DeviceConnectionServices, connectionServices: DeviceConnectionServices,
) : ViewModel() { ): ViewModel() {
val scrcpyListings: Scrcpy.Listings get() = scrcpy.listings val scrcpyListings: Scrcpy.Listings get() = scrcpy.listings
@@ -107,7 +86,7 @@ internal class DeviceTabViewModel(
val quickConnectInput: StateFlow<String> = _quickConnectInput.asStateFlow() val quickConnectInput: StateFlow<String> = _quickConnectInput.asStateFlow()
private val _savedShortcuts = MutableStateFlow( private val _savedShortcuts = MutableStateFlow(
DeviceShortcuts.unmarshalFrom(_qdBundle.value.quickDevicesList) DeviceShortcuts.unmarshalFrom(_qdBundle.value.quickDevicesList),
) )
val savedShortcuts: StateFlow<DeviceShortcuts> = _savedShortcuts.asStateFlow() val savedShortcuts: StateFlow<DeviceShortcuts> = _savedShortcuts.asStateFlow()
@@ -223,13 +202,13 @@ internal class DeviceTabViewModel(
val virtualButtonLayout: StateFlow<Pair<List<VirtualButtonAction>, List<VirtualButtonAction>>> = val virtualButtonLayout: StateFlow<Pair<List<VirtualButtonAction>, List<VirtualButtonAction>>> =
_asBundle.map { _asBundle.map {
VirtualButtonActions.splitLayout( VirtualButtonActions.splitLayout(
VirtualButtonActions.parseStoredLayout(it.virtualButtonsLayout) VirtualButtonActions.parseStoredLayout(it.virtualButtonsLayout),
) )
}.stateIn( }.stateIn(
viewModelScope, viewModelScope,
SharingStarted.Eagerly, SharingStarted.Eagerly,
VirtualButtonActions.splitLayout( VirtualButtonActions.splitLayout(
VirtualButtonActions.parseStoredLayout(_asBundle.value.virtualButtonsLayout) VirtualButtonActions.parseStoredLayout(_asBundle.value.virtualButtonsLayout),
), ),
) )
@@ -397,7 +376,7 @@ internal class DeviceTabViewModel(
EventLogMessage.Resource( EventLogMessage.Resource(
R.string.vm_send_action, R.string.vm_send_action,
listOf(EventLogMessage.Resource(action.titleResId)), listOf(EventLogMessage.Resource(action.titleResId)),
) ),
) { ) {
scrcpy.injectKeycode(0, keycode) scrcpy.injectKeycode(0, keycode)
scrcpy.injectKeycode(1, keycode) scrcpy.injectKeycode(1, keycode)
@@ -432,7 +411,7 @@ internal class DeviceTabViewModel(
private fun runBusy( private fun runBusy(
label: EventLogMessage, label: EventLogMessage,
onFinished: (() -> Unit)? = null, onFinished: (() -> Unit)? = null,
block: suspend () -> Unit block: suspend () -> Unit,
) { ) {
if (_busy.value) return if (_busy.value) return
viewModelScope.launch { viewModelScope.launch {
@@ -492,7 +471,7 @@ internal class DeviceTabViewModel(
val result = connectionController.disconnectAdbConnection( val result = connectionController.disconnectAdbConnection(
clearQuickOnlineForTarget, clearQuickOnlineForTarget,
cause, cause,
statusLine statusLine,
) )
result.clearedTarget?.let { target -> result.clearedTarget?.let { target ->
if (target.host.isNotBlank()) if (target.host.isNotBlank())
@@ -535,14 +514,14 @@ internal class DeviceTabViewModel(
host = host, host = host,
port = port, port = port,
name = fullLabel, name = fullLabel,
updateNameOnlyWhenEmpty = true updateNameOnlyWhenEmpty = true,
) )
} }
logEvent( logEvent(
"ADB connected: model=${info.model}, serial=${info.serial.ifBlank { "unknown" }}, " + "ADB connected: model=${info.model}, serial=${info.serial.ifBlank { "unknown" }}, " +
"manufacturer=${info.manufacturer.ifBlank { "unknown" }}, brand=${info.brand.ifBlank { "unknown" }}, " + "manufacturer=${info.manufacturer.ifBlank { "unknown" }}, brand=${info.brand.ifBlank { "unknown" }}, " +
"device=${info.device.ifBlank { "unknown" }}, android=${info.androidRelease.ifBlank { "unknown" }}, sdk=${info.sdkInt}" "device=${info.device.ifBlank { "unknown" }}, android=${info.androidRelease.ifBlank { "unknown" }}, sdk=${info.sdkInt}",
) )
AppRuntime.snackbar(R.string.vm_adb_connected) AppRuntime.snackbar(R.string.vm_adb_connected)
@@ -607,7 +586,8 @@ internal class DeviceTabViewModel(
if (!resolvedOptions.video) "off" if (!resolvedOptions.video) "off"
else { else {
val codec = session.codec?.string ?: "null" val codec = session.codec?.string ?: "null"
val sizeHint = if (session.width > 0 && session.height > 0) " ${session.width}x${session.height}" else "" val sizeHint =
if (session.width > 0 && session.height > 0) " ${session.width}x${session.height}" else ""
val bitrateSuffix = if (activeBundle.videoBitRate <= 0) " @default" val bitrateSuffix = if (activeBundle.videoBitRate <= 0) " @default"
else " @%.1fMbps".format(activeBundle.videoBitRate / 1_000_000f) else " @%.1fMbps".format(activeBundle.videoBitRate / 1_000_000f)
"$codec$sizeHint$bitrateSuffix" "$codec$sizeHint$bitrateSuffix"
@@ -623,11 +603,11 @@ internal class DeviceTabViewModel(
logEvent( logEvent(
"scrcpy 已启动: device=${session.deviceName}, video=$videoDetail, audio=$audioDetail, " + "scrcpy 已启动: device=${session.deviceName}, video=$videoDetail, audio=$audioDetail, " +
"control=${resolvedOptions.control}, turnScreenOff=${resolvedOptions.turnScreenOff}, " + "control=${resolvedOptions.control}, turnScreenOff=${resolvedOptions.turnScreenOff}, " +
"maxSize=${resolvedOptions.maxSize}, maxFps=${resolvedOptions.maxFps}" "maxSize=${resolvedOptions.maxSize}, maxFps=${resolvedOptions.maxFps}",
) )
AppRuntime.snackbar( AppRuntime.snackbar(
if (resolvedOptions.recordFilename.isNotBlank()) R.string.vm_scrcpy_started_recording if (resolvedOptions.recordFilename.isNotBlank()) R.string.vm_scrcpy_started_recording
else R.string.vm_scrcpy_started else R.string.vm_scrcpy_started,
) )
} }
@@ -699,13 +679,13 @@ internal class DeviceTabViewModel(
} }
logEvent( logEvent(
if (useLegacyPaste) R.string.vm_legacy_paste_injected if (useLegacyPaste) R.string.vm_legacy_paste_injected
else R.string.vm_clipboard_synced_paste else R.string.vm_clipboard_synced_paste,
) )
}.onFailure { error -> }.onFailure { error ->
logEvent(R.string.vm_clipboard_paste_failed, level = Log.WARN, error = error) logEvent(R.string.vm_clipboard_paste_failed, level = Log.WARN, error = error)
AppRuntime.snackbar( AppRuntime.snackbar(
if (useLegacyPaste) R.string.fullscreen_legacy_paste_failed if (useLegacyPaste) R.string.fullscreen_legacy_paste_failed
else R.string.fullscreen_clipboard_sync_failed else R.string.fullscreen_clipboard_sync_failed,
) )
} }
} }
@@ -723,7 +703,7 @@ internal class DeviceTabViewModel(
) )
AppRuntime.snackbar( AppRuntime.snackbar(
if (useClipboardPaste) R.string.fullscreen_paste_non_ascii if (useClipboardPaste) R.string.fullscreen_paste_non_ascii
else R.string.fullscreen_text_input_failed else R.string.fullscreen_text_input_failed,
) )
} }
} }
@@ -747,7 +727,7 @@ internal class DeviceTabViewModel(
port = device.port, port = device.port,
autoStartScrcpy = device.startScrcpyOnConnect, autoStartScrcpy = device.startScrcpyOnConnect,
autoEnterFullScreen = device.startScrcpyOnConnect && device.openFullscreenOnStart, autoEnterFullScreen = device.startScrcpyOnConnect && device.openFullscreenOnStart,
scrcpyProfileId = device.scrcpyProfileId scrcpyProfileId = device.scrcpyProfileId,
) )
connectionController.updateQuickConnected(false) connectionController.updateQuickConnected(false)
} catch (error: Exception) { } catch (error: Exception) {
@@ -818,7 +798,7 @@ internal class DeviceTabViewModel(
) )
AppRuntime.snackbar( AppRuntime.snackbar(
if (ok) R.string.vm_pairing_succeeded if (ok) R.string.vm_pairing_succeeded
else R.string.vm_pairing_failed else R.string.vm_pairing_failed,
) )
} }
} }
@@ -850,7 +830,7 @@ internal class DeviceTabViewModel(
viewModelScope.launch { viewModelScope.launch {
disconnectAdbConnection( disconnectAdbConnection(
cause = DisconnectCause.KeepAliveFailed, cause = DisconnectCause.KeepAliveFailed,
statusLine = "ADB disconnected" statusLine = "ADB disconnected",
) )
} }
logEvent(R.string.vm_auto_reconnect_failed, level = Log.ERROR, error = error) logEvent(R.string.vm_auto_reconnect_failed, level = Log.ERROR, error = error)
@@ -979,7 +959,7 @@ internal class DeviceTabViewModel(
screenHeight, screenHeight,
pressure, pressure,
actionButton, actionButton,
buttons buttons,
) )
} }
@@ -1001,9 +981,9 @@ internal class DeviceTabViewModel(
class Factory( class Factory(
private val scrcpy: Scrcpy, private val scrcpy: Scrcpy,
private val connectionServices: DeviceConnectionServices, private val connectionServices: DeviceConnectionServices,
) : ViewModelProvider.Factory { ): ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T { override fun <T: ViewModel> create(modelClass: Class<T>): T {
return DeviceTabViewModel(scrcpy, connectionServices) as T return DeviceTabViewModel(scrcpy, connectionServices) as T
} }
} }

View File

@@ -6,24 +6,11 @@ import android.net.Uri
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import io.github.miuzarte.scrcpyforandroid.R import io.github.miuzarte.scrcpyforandroid.R
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime import io.github.miuzarte.scrcpyforandroid.services.*
import io.github.miuzarte.scrcpyforandroid.services.DirectoryDownloadSnapshot
import io.github.miuzarte.scrcpyforandroid.services.DirectorySnapshotSession
import io.github.miuzarte.scrcpyforandroid.services.FileManagerService
import io.github.miuzarte.scrcpyforandroid.services.RemoteFileEntry
import io.github.miuzarte.scrcpyforandroid.services.RemoteFileKind
import io.github.miuzarte.scrcpyforandroid.services.RemoteFileStat
import io.github.miuzarte.scrcpyforandroid.storage.BundleSyncDelegate import io.github.miuzarte.scrcpyforandroid.storage.BundleSyncDelegate
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -36,11 +23,11 @@ internal enum class FileManagerSortField { NAME, SIZE, TIME, EXTENSION }
internal data class FileManagerScrollPosition(val index: Int, val offset: Int) internal data class FileManagerScrollPosition(val index: Int, val offset: Int)
internal sealed interface PendingTreeDownload { internal sealed interface PendingTreeDownload {
data class File(val remotePath: String, val fileName: String) : PendingTreeDownload data class File(val remotePath: String, val fileName: String): PendingTreeDownload
data class Directory(val snapshot: DirectoryDownloadSnapshot) : PendingTreeDownload data class Directory(val snapshot: DirectoryDownloadSnapshot): PendingTreeDownload
} }
internal class FileManagerViewModel : ViewModel() { internal class FileManagerViewModel: ViewModel() {
private val asBundleSync = BundleSyncDelegate( private val asBundleSync = BundleSyncDelegate(
sharedFlow = appSettings.bundleState, sharedFlow = appSettings.bundleState,
@@ -82,7 +69,7 @@ internal class FileManagerViewModel : ViewModel() {
val sortField: StateFlow<FileManagerSortField> = asBundle val sortField: StateFlow<FileManagerSortField> = asBundle
.map { .map {
runCatching { FileManagerSortField.valueOf(it.fileManagerSortBy) }.getOrDefault( runCatching { FileManagerSortField.valueOf(it.fileManagerSortBy) }.getOrDefault(
FileManagerSortField.NAME FileManagerSortField.NAME,
) )
} }
.stateIn( .stateIn(
@@ -473,7 +460,7 @@ internal fun sortEntries(
} }
return entries.sortedWith( return entries.sortedWith(
compareByDescending<RemoteFileEntry> { it.isDirectory } compareByDescending<RemoteFileEntry> { it.isDirectory }
.then(if (descending) comparator.reversed() else comparator) .then(if (descending) comparator.reversed() else comparator),
) )
} }

View File

@@ -10,29 +10,9 @@ import android.view.Surface
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalActivity import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box import androidx.compose.runtime.*
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
@@ -65,18 +45,8 @@ import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
import io.github.miuzarte.scrcpyforandroid.storage.Settings import io.github.miuzarte.scrcpyforandroid.storage.Settings
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
import io.github.miuzarte.scrcpyforandroid.util.Debouncer import io.github.miuzarte.scrcpyforandroid.util.Debouncer
import io.github.miuzarte.scrcpyforandroid.widgets.AppListBottomSheet import io.github.miuzarte.scrcpyforandroid.widgets.*
import io.github.miuzarte.scrcpyforandroid.widgets.AppListEntry import kotlinx.coroutines.*
import io.github.miuzarte.scrcpyforandroid.widgets.ScrcpyVideoSurface
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonBar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.SnackbarHost import top.yukonga.miuix.kmp.basic.SnackbarHost
import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.Text
@@ -126,7 +96,7 @@ fun FullscreenControlScreen(
val buttonItems = remember(asBundle.virtualButtonsLayout) { val buttonItems = remember(asBundle.virtualButtonsLayout) {
VirtualButtonActions.splitLayout( VirtualButtonActions.splitLayout(
VirtualButtonActions.parseStoredLayout(asBundle.virtualButtonsLayout) VirtualButtonActions.parseStoredLayout(asBundle.virtualButtonsLayout),
) )
} }
val floatingActions = remember(buttonItems) { val floatingActions = remember(buttonItems) {
@@ -137,7 +107,7 @@ fun FullscreenControlScreen(
val fullscreenVirtualButtonHeight = asBundle.fullscreenVirtualButtonHeightDp.dp val fullscreenVirtualButtonHeight = asBundle.fullscreenVirtualButtonHeightDp.dp
val fullscreenVirtualButtonDockSetting = remember(asBundle.fullscreenVirtualButtonDock) { val fullscreenVirtualButtonDockSetting = remember(asBundle.fullscreenVirtualButtonDock) {
AppSettings.FullscreenVirtualButtonDock.fromStoredValue( AppSettings.FullscreenVirtualButtonDock.fromStoredValue(
asBundle.fullscreenVirtualButtonDock asBundle.fullscreenVirtualButtonDock,
) )
} }
var displayRotation by remember(activity) { var displayRotation by remember(activity) {
@@ -148,7 +118,7 @@ fun FullscreenControlScreen(
if (displayManager == null || activity == null) { if (displayManager == null || activity == null) {
onDispose {} onDispose {}
} else { } else {
val listener = object : DisplayManager.DisplayListener { val listener = object: DisplayManager.DisplayListener {
override fun onDisplayAdded(displayId: Int) = Unit override fun onDisplayAdded(displayId: Int) = Unit
override fun onDisplayRemoved(displayId: Int) = Unit override fun onDisplayRemoved(displayId: Int) = Unit
@@ -171,7 +141,8 @@ fun FullscreenControlScreen(
AppSettings.FullscreenVirtualButtonDock.FOLLOW_TOP, AppSettings.FullscreenVirtualButtonDock.FOLLOW_TOP,
AppSettings.FullscreenVirtualButtonDock.FOLLOW_BOTTOM, AppSettings.FullscreenVirtualButtonDock.FOLLOW_BOTTOM,
AppSettings.FullscreenVirtualButtonDock.FOLLOW_LEFT, AppSettings.FullscreenVirtualButtonDock.FOLLOW_LEFT,
AppSettings.FullscreenVirtualButtonDock.FOLLOW_RIGHT -> null AppSettings.FullscreenVirtualButtonDock.FOLLOW_RIGHT,
-> null
AppSettings.FullscreenVirtualButtonDock.FIXED_TOP -> VirtualButtonBar.FullscreenDock.TOP AppSettings.FullscreenVirtualButtonDock.FIXED_TOP -> VirtualButtonBar.FullscreenDock.TOP
AppSettings.FullscreenVirtualButtonDock.FIXED_BOTTOM -> VirtualButtonBar.FullscreenDock.BOTTOM AppSettings.FullscreenVirtualButtonDock.FIXED_BOTTOM -> VirtualButtonBar.FullscreenDock.BOTTOM
@@ -250,7 +221,7 @@ fun FullscreenControlScreen(
val restoreWindow = activity?.window val restoreWindow = activity?.window
if (restoreWindow != null) { if (restoreWindow != null) {
WindowInsetsControllerCompat(restoreWindow, restoreWindow.decorView).show( WindowInsetsControllerCompat(restoreWindow, restoreWindow.decorView).show(
WindowInsetsCompat.Type.systemBars() WindowInsetsCompat.Type.systemBars(),
) )
WindowCompat.setDecorFitsSystemWindows(restoreWindow, true) WindowCompat.setDecorFitsSystemWindows(restoreWindow, true)
} }
@@ -320,7 +291,7 @@ fun FullscreenControlScreen(
Log.w("FullscreenControlPage", "commitImeText failed", error) Log.w("FullscreenControlPage", "commitImeText failed", error)
AppRuntime.snackbar( AppRuntime.snackbar(
if (useClipboardPaste) R.string.fullscreen_paste_non_ascii if (useClipboardPaste) R.string.fullscreen_paste_non_ascii
else R.string.fullscreen_text_input_failed else R.string.fullscreen_text_input_failed,
) )
} }
} }
@@ -367,7 +338,7 @@ fun FullscreenControlScreen(
Log.w("FullscreenControl", "pasteLocalClipboard failed", error) Log.w("FullscreenControl", "pasteLocalClipboard failed", error)
AppRuntime.snackbar( AppRuntime.snackbar(
if (useLegacyPaste) R.string.fullscreen_legacy_paste_failed if (useLegacyPaste) R.string.fullscreen_legacy_paste_failed
else R.string.fullscreen_clipboard_sync_failed else R.string.fullscreen_clipboard_sync_failed,
) )
} }
} }
@@ -383,7 +354,7 @@ fun FullscreenControlScreen(
Log.w( Log.w(
"FullscreenControlPage", "FullscreenControlPage",
"sendKeycode failed for keycode=$it", "sendKeycode failed for keycode=$it",
e e,
) )
} }
} }
@@ -452,7 +423,7 @@ fun FullscreenControlScreen(
VirtualButtonBar.FullscreenDock.BOTTOM -> Alignment.BottomCenter VirtualButtonBar.FullscreenDock.BOTTOM -> Alignment.BottomCenter
VirtualButtonBar.FullscreenDock.LEFT -> Alignment.CenterStart VirtualButtonBar.FullscreenDock.LEFT -> Alignment.CenterStart
VirtualButtonBar.FullscreenDock.RIGHT -> Alignment.CenterEnd VirtualButtonBar.FullscreenDock.RIGHT -> Alignment.CenterEnd
} },
), ),
dock = fullscreenVirtualButtonDock, dock = fullscreenVirtualButtonDock,
reverseOrder = fullscreenVirtualButtonReverseOrder, reverseOrder = fullscreenVirtualButtonReverseOrder,
@@ -582,10 +553,12 @@ private fun isFixedDockOrderReversed(
) )
return when (visualDock) { return when (visualDock) {
VirtualButtonBar.FullscreenDock.TOP, VirtualButtonBar.FullscreenDock.TOP,
VirtualButtonBar.FullscreenDock.BOTTOM -> visualDirection == DockDirection.RIGHT_TO_LEFT VirtualButtonBar.FullscreenDock.BOTTOM,
-> visualDirection == DockDirection.RIGHT_TO_LEFT
VirtualButtonBar.FullscreenDock.LEFT, VirtualButtonBar.FullscreenDock.LEFT,
VirtualButtonBar.FullscreenDock.RIGHT -> visualDirection == DockDirection.BOTTOM_TO_TOP VirtualButtonBar.FullscreenDock.RIGHT,
-> visualDirection == DockDirection.BOTTOM_TO_TOP
} }
} }
@@ -707,7 +680,7 @@ fun FullscreenControlPage(
Modifier.pointerInteropFilter { event -> Modifier.pointerInteropFilter { event ->
touchEventHandler.handleMotionEvent(event) touchEventHandler.handleMotionEvent(event)
} }
else Modifier else Modifier,
) )
.onSizeChanged { size -> .onSizeChanged { size ->
touchAreaSize = size touchAreaSize = size
@@ -736,7 +709,7 @@ fun FullscreenControlPage(
else else
Modifier Modifier
.fillMaxHeight() .fillMaxHeight()
.aspectRatio(sessionAspect) .aspectRatio(sessionAspect),
), ),
) { ) {
ScrcpyVideoSurface( ScrcpyVideoSurface(
@@ -750,7 +723,7 @@ fun FullscreenControlPage(
bounds.top.toInt(), bounds.top.toInt(),
bounds.right.toInt(), bounds.right.toInt(),
bounds.bottom.toInt(), bounds.bottom.toInt(),
) ),
) )
}, },
session = session, session = session,
@@ -779,7 +752,7 @@ fun FullscreenControlPage(
.align(Alignment.TopStart) .align(Alignment.TopStart)
.padding(start = UiSpacing.ContentVertical, top = UiSpacing.ContentVertical) .padding(start = UiSpacing.ContentVertical, top = UiSpacing.ContentVertical)
.background(Color.Black.copy(alpha = 0.5f)) .background(Color.Black.copy(alpha = 0.5f))
.padding(horizontal = UiSpacing.ContentVertical, vertical = UiSpacing.Medium) .padding(horizontal = UiSpacing.ContentVertical, vertical = UiSpacing.Medium),
) { ) {
Column(verticalArrangement = Arrangement.spacedBy(UiSpacing.Tiny)) { Column(verticalArrangement = Arrangement.spacedBy(UiSpacing.Tiny)) {
Text( Text(

View File

@@ -1,28 +1,14 @@
package io.github.miuzarte.scrcpyforandroid.pages package io.github.miuzarte.scrcpyforandroid.pages
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
@@ -50,22 +36,8 @@ import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
import io.github.miuzarte.scrcpyforandroid.ui.contextClick import io.github.miuzarte.scrcpyforandroid.ui.contextClick
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers import top.yukonga.miuix.kmp.basic.*
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.HorizontalDivider
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.IconButton
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.ScrollBehavior
import top.yukonga.miuix.kmp.basic.SnackbarHost
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.basic.TextField
import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.blur.layerBackdrop import top.yukonga.miuix.kmp.blur.layerBackdrop
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
import top.yukonga.miuix.kmp.theme.MiuixTheme.textStyles import top.yukonga.miuix.kmp.theme.MiuixTheme.textStyles
@@ -168,7 +140,7 @@ private fun RecordPreferencesPage(
TextFieldValue( TextFieldValue(
text = soBundle.recordFilename, text = soBundle.recordFilename,
selection = TextRange(soBundle.recordFilename.length), selection = TextRange(soBundle.recordFilename.length),
) ),
) )
} }
@@ -225,7 +197,7 @@ private fun RecordPreferencesPage(
RecordFilenameTemplate.resolve( RecordFilenameTemplate.resolve(
template = draftTemplate.text, template = draftTemplate.text,
sessionInfo = previewSessionInfo, sessionInfo = previewSessionInfo,
) ),
) )
} }
val listState = rememberSaveable( val listState = rememberSaveable(
@@ -293,7 +265,7 @@ private fun RecordPreferencesPage(
if (index > 0) HorizontalDivider() if (index > 0) HorizontalDivider()
Column( Column(
modifier = Modifier.padding(horizontal = UiSpacing.Large), modifier = Modifier.padding(horizontal = UiSpacing.Large),
verticalArrangement = Arrangement.spacedBy(UiSpacing.Small) verticalArrangement = Arrangement.spacedBy(UiSpacing.Small),
) { ) {
Text( Text(
text = entry.value, text = entry.value,
@@ -302,7 +274,7 @@ private fun RecordPreferencesPage(
) )
Text( Text(
text = stringResource( text = stringResource(
entry.descriptionResId ?: R.string.record_plain_text entry.descriptionResId ?: R.string.record_plain_text,
), ),
color = colorScheme.onSurfaceVariantSummary, color = colorScheme.onSurfaceVariantSummary,
fontSize = textStyles.body2.fontSize, fontSize = textStyles.body2.fontSize,

View File

@@ -2,16 +2,8 @@ package io.github.miuzarte.scrcpyforandroid.pages
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import io.github.miuzarte.scrcpyforandroid.R import io.github.miuzarte.scrcpyforandroid.R
@@ -20,11 +12,7 @@ import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcuts
import io.github.miuzarte.scrcpyforandroid.scaffolds.ReorderableList import io.github.miuzarte.scrcpyforandroid.scaffolds.ReorderableList
import io.github.miuzarte.scrcpyforandroid.storage.Settings import io.github.miuzarte.scrcpyforandroid.storage.Settings
import io.github.miuzarte.scrcpyforandroid.storage.Storage.quickDevices import io.github.miuzarte.scrcpyforandroid.storage.Storage.quickDevices
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import top.yukonga.miuix.kmp.overlay.OverlayBottomSheet import top.yukonga.miuix.kmp.overlay.OverlayBottomSheet
@Composable @Composable

View File

@@ -4,16 +4,7 @@ import android.content.pm.ActivityInfo
import android.graphics.Rect import android.graphics.Rect
import android.util.Rational import android.util.Rational
import androidx.activity.compose.LocalActivity import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner

View File

@@ -7,30 +7,12 @@ import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.ViewConfiguration import android.view.ViewConfiguration
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ContentCopy import androidx.compose.material.icons.rounded.ContentCopy
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -56,14 +38,7 @@ import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
import io.github.miuzarte.scrcpyforandroid.ui.contextClick import io.github.miuzarte.scrcpyforandroid.ui.contextClick
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
import top.yukonga.miuix.kmp.basic.DropdownEntry import top.yukonga.miuix.kmp.basic.*
import top.yukonga.miuix.kmp.basic.DropdownItem
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.IconButton
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.SmallTopAppBar
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextField
import top.yukonga.miuix.kmp.blur.layerBackdrop import top.yukonga.miuix.kmp.blur.layerBackdrop
import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.extended.More import top.yukonga.miuix.kmp.icon.extended.More
@@ -123,9 +98,9 @@ fun TerminalScreen(
onClick = { onClick = {
output = "" output = ""
}, },
) ),
) ),
) ),
) { ) {
Icon( Icon(
imageVector = MiuixIcons.More, imageVector = MiuixIcons.More,
@@ -140,7 +115,7 @@ fun TerminalScreen(
Box( Box(
modifier = modifier =
if (blurActive) Modifier.layerBackdrop(blurBackdrop) if (blurActive) Modifier.layerBackdrop(blurBackdrop)
else Modifier else Modifier,
) { ) {
TerminalPage( TerminalPage(
viewModel = viewModel, viewModel = viewModel,
@@ -298,7 +273,7 @@ private fun TerminalPage(
} }
val terminalViewClient = remember { val terminalViewClient = remember {
object : TerminalViewClient { object: TerminalViewClient {
override fun onScale(scale: Float): Float { override fun onScale(scale: Float): Float {
when { when {
scale >= FONT_SCALE_STEP_THRESHOLD -> { scale >= FONT_SCALE_STEP_THRESHOLD -> {
@@ -360,7 +335,7 @@ private fun TerminalPage(
terminalView?.setTerminalCursorBlinkerRate(500) terminalView?.setTerminalCursorBlinkerRate(500)
terminalView?.setTerminalCursorBlinkerState( terminalView?.setTerminalCursorBlinkerState(
start = true, start = true,
startOnlyIfCursorEnabled = true startOnlyIfCursorEnabled = true,
) )
viewModel.syncOutput { onOutputChange(it) } viewModel.syncOutput { onOutputChange(it) }
} }
@@ -419,7 +394,7 @@ private fun TerminalPage(
// theme changes // theme changes
LaunchedEffect( LaunchedEffect(
colorScheme.surface, colorScheme.surface,
colorScheme.onSurface colorScheme.onSurface,
) { ) {
applyTheme() applyTheme()
terminalView?.onScreenUpdated() terminalView?.onScreenUpdated()
@@ -546,7 +521,7 @@ private fun TerminalPage(
TerminalExtraKeyButton( TerminalExtraKeyButton(
"CTRL", "CTRL",
Modifier.weight(1f), Modifier.weight(1f),
active = viewModel.ctrlLatched active = viewModel.ctrlLatched,
) { ) {
haptic.contextClick() haptic.contextClick()
viewModel.ctrlLatched = !viewModel.ctrlLatched viewModel.ctrlLatched = !viewModel.ctrlLatched
@@ -554,7 +529,7 @@ private fun TerminalPage(
TerminalExtraKeyButton( TerminalExtraKeyButton(
"ALT", "ALT",
Modifier.weight(1f), Modifier.weight(1f),
active = viewModel.altLatched active = viewModel.altLatched,
) { ) {
haptic.contextClick() haptic.contextClick()
viewModel.altLatched = !viewModel.altLatched viewModel.altLatched = !viewModel.altLatched
@@ -639,7 +614,7 @@ private fun TerminalOutputBottomSheet(
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight(2f / 3f) .fillMaxHeight(2f / 3f),
) { ) {
item { item {
TextField( TextField(

View File

@@ -16,15 +16,11 @@ import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
import io.github.miuzarte.scrcpyforandroid.services.LocalInputService import io.github.miuzarte.scrcpyforandroid.services.LocalInputService
import io.github.miuzarte.scrcpyforandroid.storage.BundleSyncDelegate import io.github.miuzarte.scrcpyforandroid.storage.BundleSyncDelegate
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import top.yukonga.miuix.kmp.basic.SnackbarResult import top.yukonga.miuix.kmp.basic.SnackbarResult
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -32,7 +28,7 @@ import kotlin.math.roundToInt
private const val DEFAULT_TERMINAL_FONT_SIZE_SP = 14f private const val DEFAULT_TERMINAL_FONT_SIZE_SP = 14f
private const val LOG_TAG = "TerminalScreen" private const val LOG_TAG = "TerminalScreen"
internal class TerminalViewModel : ViewModel() { internal class TerminalViewModel: ViewModel() {
private val asBundleSync = BundleSyncDelegate( private val asBundleSync = BundleSyncDelegate(
sharedFlow = appSettings.bundleState, sharedFlow = appSettings.bundleState,

View File

@@ -20,7 +20,7 @@ private val OS2_PHONE_LIGHT = BgEffectConfig.Config(
0.95f, 0.95f,
0.14f, 0.14f,
0.27f, 0.27f,
0.8f 0.8f,
), ),
colors1 = OS2_PHONE_LIGHT_COLORS, colors1 = OS2_PHONE_LIGHT_COLORS,
colors2 = OS2_PHONE_LIGHT_COLORS, colors2 = OS2_PHONE_LIGHT_COLORS,
@@ -51,7 +51,7 @@ private val OS2_PHONE_DARK = BgEffectConfig.Config(
0.81f, 0.81f,
0.14f, 0.14f,
0.24f, 0.24f,
0.72f 0.72f,
), ),
colors1 = OS2_PHONE_DARK_COLORS, colors1 = OS2_PHONE_DARK_COLORS,
colors2 = OS2_PHONE_DARK_COLORS, colors2 = OS2_PHONE_DARK_COLORS,
@@ -82,7 +82,7 @@ private val OS2_PAD_LIGHT = BgEffectConfig.Config(
0.68f, 0.68f,
0.28f, 0.28f,
0.26f, 0.26f,
0.62f 0.62f,
), ),
colors1 = OS2_PAD_LIGHT_COLORS, colors1 = OS2_PAD_LIGHT_COLORS,
colors2 = OS2_PAD_LIGHT_COLORS, colors2 = OS2_PAD_LIGHT_COLORS,
@@ -113,7 +113,7 @@ private val OS2_PAD_DARK = BgEffectConfig.Config(
0.71f, 0.71f,
0.43f, 0.43f,
0.09f, 0.09f,
0.75f 0.75f,
), ),
colors1 = OS2_PAD_DARK_COLORS, colors1 = OS2_PAD_DARK_COLORS,
colors2 = OS2_PAD_DARK_COLORS, colors2 = OS2_PAD_DARK_COLORS,

View File

@@ -18,7 +18,7 @@ private val OS3_PHONE_LIGHT = BgEffectConfig.Config(
0.64f, 0.64f,
0.65f, 0.65f,
0.98f, 0.98f,
1.0f 1.0f,
), ),
colors2 = floatArrayOf( colors2 = floatArrayOf(
0.58f, 0.58f,
@@ -36,7 +36,7 @@ private val OS3_PHONE_LIGHT = BgEffectConfig.Config(
0.97f, 0.97f,
0.77f, 0.77f,
0.84f, 0.84f,
1.0f 1.0f,
), ),
colors3 = floatArrayOf( colors3 = floatArrayOf(
0.98f, 0.98f,
@@ -54,7 +54,7 @@ private val OS3_PHONE_LIGHT = BgEffectConfig.Config(
0.56f, 0.56f,
0.69f, 0.69f,
1.0f, 1.0f,
1.0f 1.0f,
), ),
colorInterpPeriod = 5.0f, colorInterpPeriod = 5.0f,
lightOffset = 0.1f, lightOffset = 0.1f,
@@ -80,7 +80,7 @@ private val OS3_PHONE_DARK = BgEffectConfig.Config(
0.11f, 0.11f,
0.16f, 0.16f,
0.83f, 0.83f,
0.4f 0.4f,
), ),
colors2 = floatArrayOf( colors2 = floatArrayOf(
0.07f, 0.07f,
@@ -98,7 +98,7 @@ private val OS3_PHONE_DARK = BgEffectConfig.Config(
0.0f, 0.0f,
0.2f, 0.2f,
0.78f, 0.78f,
0.5f 0.5f,
), ),
colors3 = floatArrayOf( colors3 = floatArrayOf(
0.58f, 0.58f,
@@ -116,7 +116,7 @@ private val OS3_PHONE_DARK = BgEffectConfig.Config(
0.12f, 0.12f,
0.16f, 0.16f,
0.7f, 0.7f,
0.6f 0.6f,
), ),
colorInterpPeriod = 8.0f, colorInterpPeriod = 8.0f,
lightOffset = 0.0f, lightOffset = 0.0f,
@@ -142,7 +142,7 @@ private val OS3_PAD_LIGHT = BgEffectConfig.Config(
0.98f, 0.98f,
0.76f, 0.76f,
0.8f, 0.8f,
1.0f 1.0f,
), ),
colors2 = floatArrayOf( colors2 = floatArrayOf(
0.66f, 0.66f,
@@ -160,7 +160,7 @@ private val OS3_PAD_LIGHT = BgEffectConfig.Config(
0.97f, 0.97f,
0.77f, 0.77f,
0.84f, 0.84f,
1.0f 1.0f,
), ),
colors3 = floatArrayOf( colors3 = floatArrayOf(
0.97f, 0.97f,
@@ -178,7 +178,7 @@ private val OS3_PAD_LIGHT = BgEffectConfig.Config(
0.72f, 0.72f,
0.73f, 0.73f,
0.98f, 0.98f,
1.0f 1.0f,
), ),
colorInterpPeriod = 7.0f, colorInterpPeriod = 7.0f,
lightOffset = 0.1f, lightOffset = 0.1f,
@@ -204,7 +204,7 @@ private val OS3_PAD_DARK = BgEffectConfig.Config(
0.14f, 0.14f,
0.18f, 0.18f,
0.55f, 0.55f,
0.5f 0.5f,
), ),
colors2 = floatArrayOf( colors2 = floatArrayOf(
0.07f, 0.07f,
@@ -222,7 +222,7 @@ private val OS3_PAD_DARK = BgEffectConfig.Config(
0.66f, 0.66f,
0.26f, 0.26f,
0.62f, 0.62f,
0.5f 0.5f,
), ),
colors3 = floatArrayOf( colors3 = floatArrayOf(
0.58f, 0.58f,
@@ -240,7 +240,7 @@ private val OS3_PAD_DARK = BgEffectConfig.Config(
0.27f, 0.27f,
0.18f, 0.18f,
0.6f, 0.6f,
0.6f 0.6f,
), ),
colorInterpPeriod = 7.0f, colorInterpPeriod = 7.0f,
lightOffset = 0.0f, lightOffset = 0.0f,

View File

@@ -1,12 +1,6 @@
package io.github.miuzarte.scrcpyforandroid.pages.effect package io.github.miuzarte.scrcpyforandroid.pages.effect
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
@Composable @Composable
internal fun rememberFrameTimeSeconds( internal fun rememberFrameTimeSeconds(

View File

@@ -61,7 +61,7 @@ object BiometricGate {
prompt = BiometricPrompt( prompt = BiometricPrompt(
activity, activity,
ContextCompat.getMainExecutor(activity), ContextCompat.getMainExecutor(activity),
object : BiometricPrompt.AuthenticationCallback() { object: BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
finish(true) finish(true)
} }
@@ -71,7 +71,7 @@ object BiometricGate {
} }
override fun onAuthenticationFailed() = Unit override fun onAuthenticationFailed() = Unit
} },
) )
prompt.authenticate( prompt.authenticate(
@@ -79,7 +79,7 @@ object BiometricGate {
.setAllowedAuthenticators(ALLOWED_AUTHENTICATORS) .setAllowedAuthenticators(ALLOWED_AUTHENTICATORS)
.setTitle(title) .setTitle(title)
.setSubtitle(subtitle) .setSubtitle(subtitle)
.build() .build(),
) )
continuation.invokeOnCancellation { continuation.invokeOnCancellation {

View File

@@ -6,11 +6,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Block import androidx.compose.material.icons.rounded.Block
import androidx.compose.material.icons.rounded.Password import androidx.compose.material.icons.rounded.Password
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -20,12 +16,7 @@ import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import top.yukonga.miuix.kmp.basic.DropdownDefaults import top.yukonga.miuix.kmp.basic.*
import top.yukonga.miuix.kmp.basic.DropdownItem
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.ListPopupColumn
import top.yukonga.miuix.kmp.basic.SpinnerItemImpl
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
@Composable @Composable

View File

@@ -22,7 +22,7 @@ object PasswordRepository {
private const val PASSWORD_SUFFIX = ".password" private const val PASSWORD_SUFFIX = ".password"
private const val CREATED_WITH_AUTH_SUFFIX = ".created_with_auth" private const val CREATED_WITH_AUTH_SUFFIX = ".created_with_auth"
class PasswordStorageCorruptedException(cause: Throwable) : Exception(cause) class PasswordStorageCorruptedException(cause: Throwable): Exception(cause)
private val _entriesState = MutableStateFlow<List<PasswordEntry>>(emptyList()) private val _entriesState = MutableStateFlow<List<PasswordEntry>>(emptyList())
val entriesState: StateFlow<List<PasswordEntry>> = _entriesState.asStateFlow() val entriesState: StateFlow<List<PasswordEntry>> = _entriesState.asStateFlow()
@@ -191,7 +191,8 @@ object PasswordRepository {
is PasswordStorageCorruptedException -> throwable.cause ?: throwable is PasswordStorageCorruptedException -> throwable.cause ?: throwable
is KeyStoreException, is KeyStoreException,
is GeneralSecurityException, is GeneralSecurityException,
is KeyPermanentlyInvalidatedException -> throwable is KeyPermanentlyInvalidatedException,
-> throwable
else -> throwable else -> throwable
} }

View File

@@ -27,7 +27,7 @@ class PasswordUseCase {
val password = entry.cipherText val password = entry.cipherText
?: return Result.failure( ?: return Result.failure(
IllegalStateException(AppRuntime.stringResource(R.string.password_expired)) IllegalStateException(AppRuntime.stringResource(R.string.password_expired)),
) )
return Result.success(password.copyOf()) return Result.success(password.copyOf())
} }

View File

@@ -1,17 +1,9 @@
package io.github.miuzarte.scrcpyforandroid.scaffolds package io.github.miuzarte.scrcpyforandroid.scaffolds
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource

View File

@@ -1,15 +1,7 @@
package io.github.miuzarte.scrcpyforandroid.scaffolds package io.github.miuzarte.scrcpyforandroid.scaffolds
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
@@ -60,7 +52,7 @@ fun LazyColumn(
Modifier.pointerInput(Unit) { Modifier.pointerInput(Unit) {
detectTapGestures(onTap = { focusManager.clearFocus() }) detectTapGestures(onTap = { focusManager.clearFocus() })
} }
else Modifier else Modifier,
), ),
) { ) {
val contentWidthModifier = val contentWidthModifier =
@@ -78,7 +70,7 @@ fun LazyColumn(
.then( .then(
if (scrollBehavior != null) if (scrollBehavior != null)
Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
else Modifier else Modifier,
), ),
state = state, state = state,
contentPadding = mergedContentPadding, contentPadding = mergedContentPadding,

View File

@@ -9,11 +9,7 @@ import androidx.compose.ui.unit.Dp
import io.github.miuzarte.scrcpyforandroid.R import io.github.miuzarte.scrcpyforandroid.R
import io.github.miuzarte.scrcpyforandroid.miuix.OverlaySpinnerPreference import io.github.miuzarte.scrcpyforandroid.miuix.OverlaySpinnerPreference
import io.github.miuzarte.scrcpyforandroid.miuix.SpinnerEntry import io.github.miuzarte.scrcpyforandroid.miuix.SpinnerEntry
import top.yukonga.miuix.kmp.basic.BasicComponentColors import top.yukonga.miuix.kmp.basic.*
import top.yukonga.miuix.kmp.basic.BasicComponentDefaults
import top.yukonga.miuix.kmp.basic.DropdownColors
import top.yukonga.miuix.kmp.basic.DropdownDefaults
import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator
/** /**
* [OverlaySpinnerPreference] wrapper with fallback value display and loading state. * [OverlaySpinnerPreference] wrapper with fallback value display and loading state.
@@ -57,7 +53,7 @@ fun OverlaySpinnerWithFallback(
SpinnerEntry( SpinnerEntry(
title = overrideEndActionValue, title = overrideEndActionValue,
enabled = true, // 保持启用以显示蓝色而非灰色 enabled = true, // 保持启用以显示蓝色而非灰色
) ),
) )
} }
if (dataLoading) { if (dataLoading) {
@@ -66,7 +62,7 @@ fun OverlaySpinnerWithFallback(
icon = { mod -> InfiniteProgressIndicator(mod) }, icon = { mod -> InfiniteProgressIndicator(mod) },
title = textLoading, title = textLoading,
enabled = false, enabled = false,
) ),
) )
} }
} }

View File

@@ -1,15 +1,7 @@
package io.github.miuzarte.scrcpyforandroid.scaffolds package io.github.miuzarte.scrcpyforandroid.scaffolds
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.DragIndicator import androidx.compose.material.icons.rounded.DragIndicator
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -51,13 +43,13 @@ class ReorderableList(
val icon: ImageVector, val icon: ImageVector,
val contentDescription: String, val contentDescription: String,
val onClick: () -> Unit, val onClick: () -> Unit,
) : EndAction ): EndAction
data class Checkbox( data class Checkbox(
val checked: Boolean, val checked: Boolean,
val enabled: Boolean = true, val enabled: Boolean = true,
val onClick: () -> Unit, val onClick: () -> Unit,
) : EndAction ): EndAction
} }
data class Item( data class Item(
@@ -94,7 +86,7 @@ class ReorderableList(
.height(IntrinsicSize.Min) .height(IntrinsicSize.Min)
.padding( .padding(
horizontal = UiSpacing.CardTitle, horizontal = UiSpacing.CardTitle,
vertical = UiSpacing.FieldLabelBottom vertical = UiSpacing.FieldLabelBottom,
), ),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
@@ -108,14 +100,14 @@ class ReorderableList(
Modifier.clickable(onClick = item.onClick) Modifier.clickable(onClick = item.onClick)
} else { } else {
Modifier Modifier
} },
), ),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(UiSpacing.Small) horizontalArrangement = Arrangement.spacedBy(UiSpacing.Small),
) { ) {
if (item.icon != null) Icon( if (item.icon != null) Icon(
item.icon, item.icon,
contentDescription = item.title contentDescription = item.title,
) )
Column { Column {
Text( Text(
@@ -132,7 +124,7 @@ class ReorderableList(
} }
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(UiSpacing.Small) horizontalArrangement = Arrangement.spacedBy(UiSpacing.Small),
) { ) {
item.endActions.forEach { action -> item.endActions.forEach { action ->
EndActionView( EndActionView(
@@ -181,7 +173,7 @@ class ReorderableList(
modifier = Modifier modifier = Modifier
.padding( .padding(
horizontal = UiSpacing.CardTitle, horizontal = UiSpacing.CardTitle,
vertical = UiSpacing.FieldLabelBottom vertical = UiSpacing.FieldLabelBottom,
), ),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
@@ -191,7 +183,7 @@ class ReorderableList(
Modifier.clickable(onClick = item.onClick) Modifier.clickable(onClick = item.onClick)
} else { } else {
Modifier Modifier
} },
), ),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {

View File

@@ -3,18 +3,12 @@ package io.github.miuzarte.scrcpyforandroid.scaffolds
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation

View File

@@ -1,15 +1,6 @@
package io.github.miuzarte.scrcpyforandroid.scrcpy package io.github.miuzarte.scrcpyforandroid.scrcpy
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.AudioSource import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.*
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.CameraFacing
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.DisplayImePolicy
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.ListOptions
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.LogLevel
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Orientation
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.OrientationLock
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Tick
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.VideoSource
import io.github.miuzarte.scrcpyforandroid.services.NativeRecordingSupport import io.github.miuzarte.scrcpyforandroid.services.NativeRecordingSupport
data class ClientOptions( data class ClientOptions(
@@ -304,7 +295,7 @@ data class ClientOptions(
if (!video && !audio && !control) { if (!video && !audio && !control) {
throw IllegalArgumentException( throw IllegalArgumentException(
"nothing to do" "nothing to do",
) )
} }
@@ -315,13 +306,13 @@ data class ClientOptions(
if (newDisplay.isNotBlank()) { if (newDisplay.isNotBlank()) {
if (videoSource != VideoSource.DISPLAY) { if (videoSource != VideoSource.DISPLAY) {
throw IllegalArgumentException( throw IllegalArgumentException(
"--new-display is only available with --video-source=display" "--new-display is only available with --video-source=display",
) )
} }
if (!video) { if (!video) {
throw IllegalArgumentException( throw IllegalArgumentException(
"--new-display is incompatible with --no-video" "--new-display is incompatible with --no-video",
) )
} }
} }
@@ -329,39 +320,39 @@ data class ClientOptions(
if (videoSource == VideoSource.CAMERA) { if (videoSource == VideoSource.CAMERA) {
if (displayId > 0) { if (displayId > 0) {
throw IllegalArgumentException( throw IllegalArgumentException(
"--display-id is only available with --video-source=display" "--display-id is only available with --video-source=display",
) )
} }
if (displayImePolicy != DisplayImePolicy.UNDEFINED) { if (displayImePolicy != DisplayImePolicy.UNDEFINED) {
throw IllegalArgumentException( throw IllegalArgumentException(
"--display-ime-policy is only available with --video-source=display" "--display-ime-policy is only available with --video-source=display",
) )
} }
if (cameraId.isNotBlank() && cameraFacing != CameraFacing.ANY) { if (cameraId.isNotBlank() && cameraFacing != CameraFacing.ANY) {
throw IllegalArgumentException( throw IllegalArgumentException(
"Cannot specify both --camera-id and --camera-facing" "Cannot specify both --camera-id and --camera-facing",
) )
} }
if (cameraSize.isNotBlank()) { if (cameraSize.isNotBlank()) {
if (maxSize > 0u) { if (maxSize > 0u) {
throw IllegalArgumentException( throw IllegalArgumentException(
"Cannot specify both --camera-size and -m/--max-size" "Cannot specify both --camera-size and -m/--max-size",
) )
} }
if (cameraAr.isNotBlank()) { if (cameraAr.isNotBlank()) {
throw IllegalArgumentException( throw IllegalArgumentException(
"--camera-high-speed requires an explicit --camera-fps value" "--camera-high-speed requires an explicit --camera-fps value",
) )
} }
} }
if (cameraHighSpeed && cameraFps <= 0u) { if (cameraHighSpeed && cameraFps <= 0u) {
throw IllegalArgumentException( throw IllegalArgumentException(
"--camera-high-speed requires an explicit --camera-fps value" "--camera-high-speed requires an explicit --camera-fps value",
) )
} }
@@ -376,13 +367,13 @@ data class ClientOptions(
|| cameraSize.isNotBlank() || cameraSize.isNotBlank()
) { ) {
throw IllegalArgumentException( throw IllegalArgumentException(
"Camera options are only available with --video-source=camera" "Camera options are only available with --video-source=camera",
) )
} }
if (displayId > 0 && newDisplay.isNotBlank()) { if (displayId > 0 && newDisplay.isNotBlank()) {
throw IllegalArgumentException( throw IllegalArgumentException(
"Cannot specify both --display-id and --new-display" "Cannot specify both --display-id and --new-display",
) )
} }
@@ -390,19 +381,19 @@ data class ClientOptions(
if (videoSource != VideoSource.DISPLAY || newDisplay.isBlank()) { if (videoSource != VideoSource.DISPLAY || newDisplay.isBlank()) {
throw IllegalArgumentException( throw IllegalArgumentException(
"-x/--flex-display can only be applied to displays created " + "-x/--flex-display can only be applied to displays created " +
"with --new-display" "with --new-display",
) )
} }
if (!control) { if (!control) {
throw IllegalArgumentException( throw IllegalArgumentException(
"-n/--no-control is not compatible with -x/--flex-display" "-n/--no-control is not compatible with -x/--flex-display",
) )
} }
if (crop.isNotBlank()) { if (crop.isNotBlank()) {
throw IllegalArgumentException( throw IllegalArgumentException(
"--crop is not compatible with -x/--flex-display" "--crop is not compatible with -x/--flex-display",
) )
} }
} }
@@ -411,7 +402,7 @@ data class ClientOptions(
&& displayId == 0 && newDisplay.isBlank() && displayId == 0 && newDisplay.isBlank()
) { ) {
throw IllegalArgumentException( throw IllegalArgumentException(
"--display-ime-policy is only supported on a secondary display" "--display-ime-policy is only supported on a secondary display",
) )
} }
@@ -432,27 +423,27 @@ data class ClientOptions(
if (audioDup) { if (audioDup) {
if (!audio) { if (!audio) {
throw IllegalArgumentException( throw IllegalArgumentException(
"--audio-dup not supported if audio is disabled" "--audio-dup not supported if audio is disabled",
) )
} }
if (audioSource != AudioSource.PLAYBACK) { if (audioSource != AudioSource.PLAYBACK) {
throw IllegalArgumentException( throw IllegalArgumentException(
"--audio-dup is specific to --audio-source=playback" "--audio-dup is specific to --audio-source=playback",
) )
} }
} }
if (recordFormat != RecordFormat.AUTO && recordFilename.isBlank()) { if (recordFormat != RecordFormat.AUTO && recordFilename.isBlank()) {
throw IllegalArgumentException( throw IllegalArgumentException(
"Record format specified without recording" "Record format specified without recording",
) )
} }
if (recordFilename.isNotBlank()) { if (recordFilename.isNotBlank()) {
if (!video && !audio) { if (!video && !audio) {
throw IllegalArgumentException( throw IllegalArgumentException(
"Video and audio disabled, nothing to record" "Video and audio disabled, nothing to record",
) )
} }
@@ -461,14 +452,14 @@ data class ClientOptions(
if (recordFormat == RecordFormat.AUTO) { if (recordFormat == RecordFormat.AUTO) {
throw IllegalArgumentException( throw IllegalArgumentException(
"No format specified for recording file " + "No format specified for recording file " +
"(try with --record-format=mp4)" "(try with --record-format=mp4)",
) )
} }
} }
if (!NativeRecordingSupport.isSupported(recordFormat)) { if (!NativeRecordingSupport.isSupported(recordFormat)) {
throw IllegalArgumentException( throw IllegalArgumentException(
"Android native recording currently supports only MP4/M4A" "Android native recording currently supports only MP4/M4A",
) )
} }
@@ -476,14 +467,14 @@ data class ClientOptions(
if (recordOrientation.isMirror()) { if (recordOrientation.isMirror()) {
throw IllegalArgumentException( throw IllegalArgumentException(
"Record orientation only supports rotation, " + "Record orientation only supports rotation, " +
"not flipping: ${recordOrientation.string}" "not flipping: ${recordOrientation.string}",
) )
} }
} }
if (video && recordFormat.isAudioOnly()) { if (video && recordFormat.isAudioOnly()) {
throw IllegalArgumentException( throw IllegalArgumentException(
"Audio container does not support video stream" "Audio container does not support video stream",
) )
} }
@@ -554,32 +545,32 @@ data class ClientOptions(
if (!control) { if (!control) {
if (turnScreenOff) { if (turnScreenOff) {
throw IllegalArgumentException( throw IllegalArgumentException(
"Cannot request to turn screen off if control is disabled" "Cannot request to turn screen off if control is disabled",
) )
} }
if (stayAwake) { if (stayAwake) {
throw IllegalArgumentException( throw IllegalArgumentException(
"Cannot request to stay awake if control is disabled" "Cannot request to stay awake if control is disabled",
) )
} }
if (showTouches) { if (showTouches) {
throw IllegalArgumentException( throw IllegalArgumentException(
"Cannot request to show touches if control is disabled" "Cannot request to show touches if control is disabled",
) )
} }
if (powerOffOnClose) { if (powerOffOnClose) {
throw IllegalArgumentException( throw IllegalArgumentException(
"Cannot request power off on close if control is disabled" "Cannot request power off on close if control is disabled",
) )
} }
if (startApp.isNotBlank()) { if (startApp.isNotBlank()) {
throw IllegalArgumentException( throw IllegalArgumentException(
"Cannot start an Android app if control is disabled" "Cannot start an Android app if control is disabled",
) )
} }
if (keepActive) { if (keepActive) {
throw IllegalArgumentException( throw IllegalArgumentException(
"Cannot keep device active if control is disabled" "Cannot keep device active if control is disabled",
) )
} }
} }

View File

@@ -12,36 +12,18 @@ import io.github.miuzarte.scrcpyforandroid.constants.Defaults
import io.github.miuzarte.scrcpyforandroid.nativecore.AdbSocketStream import io.github.miuzarte.scrcpyforandroid.nativecore.AdbSocketStream
import io.github.miuzarte.scrcpyforandroid.nativecore.NativeAdbService import io.github.miuzarte.scrcpyforandroid.nativecore.NativeAdbService
import io.github.miuzarte.scrcpyforandroid.nativecore.ScrcpyAudioPlayer import io.github.miuzarte.scrcpyforandroid.nativecore.ScrcpyAudioPlayer
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.CameraFacing import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.*
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec import io.github.miuzarte.scrcpyforandroid.services.*
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.EncoderType
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.ListOptions
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent
import io.github.miuzarte.scrcpyforandroid.services.LocalInputService
import io.github.miuzarte.scrcpyforandroid.services.NativeAacRecorder
import io.github.miuzarte.scrcpyforandroid.services.NativeMp4Recorder
import io.github.miuzarte.scrcpyforandroid.services.NativeWavRecorder
import io.github.miuzarte.scrcpyforandroid.services.RecordingFileResolver
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import java.io.*
import java.io.BufferedInputStream
import java.io.BufferedReader
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.EOFException
import java.io.File
import java.io.InputStreamReader
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
import java.util.ArrayDeque import java.util.ArrayDeque
@@ -228,7 +210,7 @@ class Scrcpy(
TAG, TAG,
"start(): create audio player codecId=0x${ "start(): create audio player codecId=0x${
info.audioCodecId.toUInt().toString(16) info.audioCodecId.toUInt().toString(16)
}" }",
) )
val player = ScrcpyAudioPlayer(appContext, info.audioCodecId, lowLatency) val player = ScrcpyAudioPlayer(appContext, info.audioCodecId, lowLatency)
audioPlayer = player audioPlayer = player
@@ -243,7 +225,8 @@ class Scrcpy(
val recordFile = RecordingFileResolver.resolve(options, info) val recordFile = RecordingFileResolver.resolve(options, info)
when (options.recordFormat) { when (options.recordFormat) {
ClientOptions.RecordFormat.MP4, ClientOptions.RecordFormat.MP4,
ClientOptions.RecordFormat.M4A -> { ClientOptions.RecordFormat.M4A,
-> {
val recorder = NativeMp4Recorder( val recorder = NativeMp4Recorder(
outputFile = recordFile, outputFile = recordFile,
includeVideo = options.video, includeVideo = options.video,
@@ -296,13 +279,16 @@ class Scrcpy(
} }
Log.i( Log.i(
TAG, "start(): Session started successfully - device=${info.deviceName}, " + TAG,
"video=${if (options.video) buildString { "start(): Session started successfully - device=${info.deviceName}, " +
append(info.codec?.string ?: "null") "video=${
if (info.width > 0 && info.height > 0) append(" ${info.width}x${info.height}") if (options.video) buildString {
} else "off"}, " + append(info.codec?.string ?: "null")
if (info.width > 0 && info.height > 0) append(" ${info.width}x${info.height}")
} else "off"
}, " +
"audio=${if (options.audio) options.audioCodec.string else "off"}, " + "audio=${if (options.audio) options.audioCodec.string else "off"}, " +
"control=${options.control}" "control=${options.control}",
) )
return@withContext info return@withContext info
@@ -637,9 +623,11 @@ class Scrcpy(
runTrackedFetch { runTrackedFetch {
val output = executeList(ListOptions.CAMERA_SIZES) val output = executeList(ListOptions.CAMERA_SIZES)
val parsed = parseCameraSizes(output) val parsed = parseCameraSizes(output)
.sortedWith(compareByDescending { size -> .sortedWith(
size.substringBefore('x').toIntOrNull() ?: 0 compareByDescending { size ->
}) size.substringBefore('x').toIntOrNull() ?: 0
},
)
cachedCameraSizes = parsed cachedCameraSizes = parsed
logListPreview( logListPreview(
list = ListOptions.CAMERA_SIZES, list = ListOptions.CAMERA_SIZES,
@@ -789,7 +777,7 @@ class Scrcpy(
id = match.groupValues[1].toInt(), id = match.groupValues[1].toInt(),
width = match.groupValues[2].toInt(), width = match.groupValues[2].toInt(),
height = match.groupValues[3].toInt(), height = match.groupValues[3].toInt(),
) ),
) )
} }
return displays.toList() return displays.toList()
@@ -810,7 +798,7 @@ class Scrcpy(
facing = CameraFacing.fromString(facing), facing = CameraFacing.fromString(facing),
activeSize = activeSize, activeSize = activeSize,
fps = fpsValues.map(Int::toUShort), fps = fpsValues.map(Int::toUShort),
) ),
) )
} }
return cameras.toList() return cameras.toList()
@@ -832,7 +820,7 @@ class Scrcpy(
system = match.groupValues[1] == "*", system = match.groupValues[1] == "*",
label = match.groupValues[2].trim(), label = match.groupValues[2].trim(),
packageName = match.groupValues[3].trim(), packageName = match.groupValues[3].trim(),
) ),
) )
} }
return apps.toList().sortedBy { appSortKey(it) } return apps.toList().sortedBy { appSortKey(it) }
@@ -1079,7 +1067,7 @@ class Scrcpy(
Log.w(TAG, "audio disabled by server") Log.w(TAG, "audio disabled by server")
if (options.requireAudio) { if (options.requireAudio) {
throw IllegalStateException( throw IllegalStateException(
"Audio is required but was disabled by the server" "Audio is required but was disabled by the server",
) )
} }
0 0
@@ -1089,7 +1077,7 @@ class Scrcpy(
Log.e(TAG, "audio stream configuration error from server") Log.e(TAG, "audio stream configuration error from server")
if (options.requireAudio) { if (options.requireAudio) {
throw IllegalStateException( throw IllegalStateException(
"Audio is required but the server failed to configure audio capture" "Audio is required but the server failed to configure audio capture",
) )
} }
0 0
@@ -1098,7 +1086,7 @@ class Scrcpy(
else -> { else -> {
Log.i( Log.i(
TAG, TAG,
"audio stream codec=0x${streamCodecId.toUInt().toString(16)}" "audio stream codec=0x${streamCodecId.toUInt().toString(16)}",
) )
streamCodecId streamCodecId
} }
@@ -1191,7 +1179,7 @@ class Scrcpy(
(headerBuf[11].toInt() and 0xFF) (headerBuf[11].toInt() and 0xFF)
Log.i( Log.i(
TAG, TAG,
"video session packet: ${sw}x${sh} clientResized=$clientResized" "video session packet: ${sw}x${sh} clientResized=$clientResized",
) )
onVideoSessionSize(sw, sh) onVideoSessionSize(sw, sh)
continue continue
@@ -1261,7 +1249,7 @@ class Scrcpy(
val packet = AudioPacket( val packet = AudioPacket(
data = payload, data = payload,
ptsUs = ptsUs, ptsUs = ptsUs,
isConfig = isConfig isConfig = isConfig,
) )
audioConsumers.forEach { it(packet) } audioConsumers.forEach { it(packet) }
} catch (_: EOFException) { } catch (_: EOFException) {
@@ -1349,7 +1337,7 @@ class Scrcpy(
screenHeight, screenHeight,
pressure, pressure,
actionButton, actionButton,
buttons buttons,
) )
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
Log.w(TAG, "injectTouch(): control channel not available", e) Log.w(TAG, "injectTouch(): control channel not available", e)
@@ -1373,7 +1361,7 @@ class Scrcpy(
screenHeight, screenHeight,
hScroll, hScroll,
vScroll, vScroll,
buttons buttons,
) )
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
Log.w(TAG, "injectScroll(): control channel not available", e) Log.w(TAG, "injectScroll(): control channel not available", e)
@@ -1496,15 +1484,15 @@ class Scrcpy(
private fun startServerLogThread( private fun startServerLogThread(
serverStream: AdbSocketStream, serverStream: AdbSocketStream,
socketName: String socketName: String,
): Thread { ): Thread {
return thread(start = true, name = "scrcpy-server-log") { return thread(start = true, name = "scrcpy-server-log") {
try { try {
BufferedReader( BufferedReader(
InputStreamReader( InputStreamReader(
serverStream.inputStream, serverStream.inputStream,
Charsets.UTF_8 Charsets.UTF_8,
) ),
).use { reader -> ).use { reader ->
while (true) { while (true) {
val line = reader.readLine() ?: break val line = reader.readLine() ?: break
@@ -1539,7 +1527,7 @@ class Scrcpy(
private suspend fun openAbstractSocketWithRetry( private suspend fun openAbstractSocketWithRetry(
socketName: String, socketName: String,
expectDummyByte: Boolean expectDummyByte: Boolean,
): AdbSocketStream { ): AdbSocketStream {
var lastEx: Exception? = null var lastEx: Exception? = null
repeat(CONNECT_RETRY_COUNT) { attempt -> repeat(CONNECT_RETRY_COUNT) { attempt ->
@@ -1713,7 +1701,7 @@ class Scrcpy(
screenHeight: Int, screenHeight: Int,
hScroll: Float, hScroll: Float,
vScroll: Float, vScroll: Float,
buttons: Int buttons: Int,
) { ) {
output.writeByte(TYPE_INJECT_SCROLL_EVENT) output.writeByte(TYPE_INJECT_SCROLL_EVENT)
writePosition(x, y, screenWidth, screenHeight) writePosition(x, y, screenWidth, screenHeight)

View File

@@ -1,15 +1,6 @@
package io.github.miuzarte.scrcpyforandroid.scrcpy package io.github.miuzarte.scrcpyforandroid.scrcpy
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.AudioSource import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.*
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.CameraFacing
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.DisplayImePolicy
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.ListOptions
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.LogLevel
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Orientation
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.OrientationLock
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Tick
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.VideoSource
// 启动 scrcpy 前直接继承自 [ClientOptions], // 启动 scrcpy 前直接继承自 [ClientOptions],
// 不需要默认值 // 不需要默认值

View File

@@ -186,7 +186,8 @@ class TouchEventHandler(
val isHoverMotion = when (event.actionMasked) { val isHoverMotion = when (event.actionMasked) {
MotionEvent.ACTION_HOVER_ENTER, MotionEvent.ACTION_HOVER_ENTER,
MotionEvent.ACTION_HOVER_MOVE, MotionEvent.ACTION_HOVER_MOVE,
MotionEvent.ACTION_HOVER_EXIT -> true MotionEvent.ACTION_HOVER_EXIT,
-> true
MotionEvent.ACTION_MOVE -> buttons == 0 MotionEvent.ACTION_MOVE -> buttons == 0
else -> false else -> false
@@ -201,7 +202,8 @@ class TouchEventHandler(
) { ) {
when (event.actionMasked) { when (event.actionMasked) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_BUTTON_PRESS -> { MotionEvent.ACTION_BUTTON_PRESS,
-> {
coroutineScope.launch { coroutineScope.launch {
runCatching { runCatching {
onBackOrScreenOn(0) onBackOrScreenOn(0)
@@ -212,7 +214,8 @@ class TouchEventHandler(
} }
MotionEvent.ACTION_UP, MotionEvent.ACTION_UP,
MotionEvent.ACTION_BUTTON_RELEASE -> { MotionEvent.ACTION_BUTTON_RELEASE,
-> {
coroutineScope.launch { coroutineScope.launch {
runCatching { runCatching {
onBackOrScreenOn(1) onBackOrScreenOn(1)
@@ -234,7 +237,8 @@ class TouchEventHandler(
MotionEvent.ACTION_MOVE -> MotionEvent.ACTION_MOVE MotionEvent.ACTION_MOVE -> MotionEvent.ACTION_MOVE
MotionEvent.ACTION_BUTTON_PRESS, MotionEvent.ACTION_BUTTON_PRESS,
MotionEvent.ACTION_BUTTON_RELEASE -> return true MotionEvent.ACTION_BUTTON_RELEASE,
-> return true
else -> return true else -> return true
} }
@@ -343,7 +347,7 @@ class TouchEventHandler(
Log.w( Log.w(
FULLSCREEN_TOUCH_LOG_TAG, FULLSCREEN_TOUCH_LOG_TAG,
"handlePointerDown failed for pointerId=$pointerId", "handlePointerDown failed for pointerId=$pointerId",
e e,
) )
} }
} }
@@ -373,7 +377,7 @@ class TouchEventHandler(
Log.w( Log.w(
FULLSCREEN_TOUCH_LOG_TAG, FULLSCREEN_TOUCH_LOG_TAG,
"handlePointerMove failed for pointerId=$pointerId", "handlePointerMove failed for pointerId=$pointerId",
e e,
) )
} }
} }

View File

@@ -2,7 +2,6 @@ package io.github.miuzarte.scrcpyforandroid.services
import android.content.Context import android.content.Context
import androidx.annotation.StringRes import androidx.annotation.StringRes
import io.github.miuzarte.scrcpyforandroid.R
import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget
import io.github.miuzarte.scrcpyforandroid.nativecore.AdbMdnsDiscoverer import io.github.miuzarte.scrcpyforandroid.nativecore.AdbMdnsDiscoverer
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy

View File

@@ -32,10 +32,10 @@ object AppUpdateChecker {
) )
sealed interface State { sealed interface State {
data object Idle : State data object Idle: State
data object Checking : State data object Checking: State
data object Error : State data object Error: State
data class Ready(val release: ReleaseInfo) : State data class Ready(val release: ReleaseInfo): State
} }
private val checkMutex = Mutex() private val checkMutex = Mutex()

View File

@@ -7,7 +7,7 @@ internal class DeviceAdbAutoReconnectManager(
private val controller: ConnectionController, private val controller: ConnectionController,
private val stateStore: ConnectionStateStore, private val stateStore: ConnectionStateStore,
private val backgroundRunner: DeviceAdbBackgroundRunner = DeviceAdbBackgroundRunner(), private val backgroundRunner: DeviceAdbBackgroundRunner = DeviceAdbBackgroundRunner(),
) : Closeable { ): Closeable {
suspend fun runKeepAliveLoop( suspend fun runKeepAliveLoop(
isForeground: () -> Boolean, isForeground: () -> Boolean,
intervalMs: Long, intervalMs: Long,
@@ -31,7 +31,7 @@ internal class DeviceAdbAutoReconnectManager(
onReconnectFailure = onReconnectFailure, onReconnectFailure = onReconnectFailure,
shouldAutoReconnect = { shouldAutoReconnect = {
stateStore.state.value.disconnectCause != DisconnectCause.User && stateStore.state.value.disconnectCause != DisconnectCause.User &&
stateStore.state.value.disconnectCause != DisconnectCause.KillAdbOnClose stateStore.state.value.disconnectCause != DisconnectCause.KillAdbOnClose
}, },
) )
} }

View File

@@ -1,15 +1,11 @@
package io.github.miuzarte.scrcpyforandroid.services package io.github.miuzarte.scrcpyforandroid.services
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.ExecutorCoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import java.io.Closeable import java.io.Closeable
import java.util.concurrent.Executors import java.util.concurrent.Executors
internal class DeviceAdbBackgroundRunner : Closeable { internal class DeviceAdbBackgroundRunner: Closeable {
private val executor = Executors.newSingleThreadExecutor { runnable -> private val executor = Executors.newSingleThreadExecutor { runnable ->
Thread(runnable, "device-adb-monitor").apply { isDaemon = true } Thread(runnable, "device-adb-monitor").apply { isDaemon = true }
} }

View File

@@ -22,7 +22,7 @@ internal data class DeviceAdbSessionState(
val connectedScrcpyProfileId: String = ScrcpyOptions.GLOBAL_PROFILE_ID, val connectedScrcpyProfileId: String = ScrcpyOptions.GLOBAL_PROFILE_ID,
val audioForwardingSupported: Boolean = true, val audioForwardingSupported: Boolean = true,
val cameraMirroringSupported: Boolean = true, val cameraMirroringSupported: Boolean = true,
) : Parcelable ): Parcelable
internal class DeviceAdbConnectionCoordinator( internal class DeviceAdbConnectionCoordinator(
private val adbService: NativeAdbService = NativeAdbService, private val adbService: NativeAdbService = NativeAdbService,

View File

@@ -25,7 +25,7 @@ data class ConnectedDeviceInfo(
internal suspend fun fetchConnectedDeviceInfo( internal suspend fun fetchConnectedDeviceInfo(
adbService: NativeAdbService, adbService: NativeAdbService,
host: String, host: String,
port: Int port: Int,
): ConnectedDeviceInfo = withContext(Dispatchers.IO) { ): ConnectedDeviceInfo = withContext(Dispatchers.IO) {
val values = runCatching { val values = runCatching {
adbService.shellBatch { adbService.shellBatch {

View File

@@ -10,11 +10,11 @@ import java.util.Date
import java.util.Locale import java.util.Locale
sealed interface EventLogMessage { sealed interface EventLogMessage {
data class Raw(val text: String) : EventLogMessage data class Raw(val text: String): EventLogMessage
data class Resource( data class Resource(
@param:StringRes val resId: Int, @param:StringRes val resId: Int,
val args: List<Any> = emptyList(), val args: List<Any> = emptyList(),
) : EventLogMessage ): EventLogMessage
} }
data class EventLogEntry( data class EventLogEntry(

View File

@@ -86,7 +86,7 @@ object FileManagerService {
private val listTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") private val listTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
private val displayTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss") private val displayTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss")
private val listLineRegex = Regex( private val listLineRegex = Regex(
"""^\s*(\d+)\s+([\-bcdlps][rwxstST-]{9})\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})\s+(.+)$""" """^\s*(\d+)\s+([\-bcdlps][rwxstST-]{9})\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})\s+(.+)$""",
) )
private val sizeFormatter = DecimalFormat("0.00") private val sizeFormatter = DecimalFormat("0.00")
@@ -101,7 +101,7 @@ object FileManagerService {
.filterNot { it.name == "." || it.name == ".." } .filterNot { it.name == "." || it.name == ".." }
.sortedWith( .sortedWith(
compareByDescending<RemoteFileEntry> { it.isDirectory } compareByDescending<RemoteFileEntry> { it.isDirectory }
.thenBy { it.name.lowercase() } .thenBy { it.name.lowercase() },
) )
.toList() .toList()
} }
@@ -173,7 +173,7 @@ object FileManagerService {
targetFile.outputStream().use { output -> targetFile.outputStream().use { output ->
NativeAdbService.pull( NativeAdbService.pull(
joinRemotePath(snapshot.remoteRootPath, relativePath), joinRemotePath(snapshot.remoteRootPath, relativePath),
output output,
) )
} }
} }
@@ -189,7 +189,7 @@ object FileManagerService {
NativeAdbService.ensureConnectionResponsive() NativeAdbService.ensureConnectionResponsive()
val rootDocument = DocumentsContract.buildDocumentUriUsingTree( val rootDocument = DocumentsContract.buildDocumentUriUsingTree(
treeUri, treeUri,
DocumentsContract.getTreeDocumentId(treeUri) DocumentsContract.getTreeDocumentId(treeUri),
) )
val target = createUniqueDocument(context, rootDocument, fileName, guessMimeType(fileName)) val target = createUniqueDocument(context, rootDocument, fileName, guessMimeType(fileName))
context.contentResolver.openOutputStream(target, "w").use { output -> context.contentResolver.openOutputStream(target, "w").use { output ->
@@ -206,7 +206,7 @@ object FileManagerService {
NativeAdbService.ensureConnectionResponsive() NativeAdbService.ensureConnectionResponsive()
val rootDocument = DocumentsContract.buildDocumentUriUsingTree( val rootDocument = DocumentsContract.buildDocumentUriUsingTree(
treeUri, treeUri,
DocumentsContract.getTreeDocumentId(treeUri) DocumentsContract.getTreeDocumentId(treeUri),
) )
val folderName = snapshot.remoteRootPath.substringAfterLast('/').ifBlank { "Scrcpy" } val folderName = snapshot.remoteRootPath.substringAfterLast('/').ifBlank { "Scrcpy" }
val folderRoot = createUniqueDirectoryDocument(context, rootDocument, folderName) val folderRoot = createUniqueDirectoryDocument(context, rootDocument, folderName)
@@ -327,7 +327,7 @@ object FileManagerService {
val dateTime = runCatching { val dateTime = runCatching {
LocalDateTime.parse( LocalDateTime.parse(
"${match.groupValues[7]} ${match.groupValues[8]}", "${match.groupValues[7]} ${match.groupValues[8]}",
listTimeFormatter listTimeFormatter,
) )
}.getOrNull() }.getOrNull()
val nameField = match.groupValues[9] val nameField = match.groupValues[9]
@@ -542,7 +542,7 @@ object FileManagerService {
private fun createUniqueDirectoryDocument( private fun createUniqueDirectoryDocument(
context: Context, context: Context,
parentDocument: Uri, parentDocument: Uri,
baseName: String baseName: String,
): Uri { ): Uri {
var name = baseName var name = baseName
var index = 1 var index = 1
@@ -551,7 +551,7 @@ object FileManagerService {
context, context,
parentDocument, parentDocument,
name, name,
DocumentsContract.Document.MIME_TYPE_DIR DocumentsContract.Document.MIME_TYPE_DIR,
) != null ) != null
) { ) {
name = "$baseName ($index)" name = "$baseName ($index)"
@@ -592,7 +592,7 @@ object FileManagerService {
private fun ensureTreeDirectory( private fun ensureTreeDirectory(
context: Context, context: Context,
rootDocument: Uri, rootDocument: Uri,
relativePath: String relativePath: String,
): Uri { ): Uri {
var current = rootDocument var current = rootDocument
if (relativePath.isBlank()) return current if (relativePath.isBlank()) return current
@@ -603,14 +603,15 @@ object FileManagerService {
context, context,
current, current,
segment, segment,
DocumentsContract.Document.MIME_TYPE_DIR DocumentsContract.Document.MIME_TYPE_DIR,
) )
current = existing ?: DocumentsContract.createDocument( current = existing ?: DocumentsContract.createDocument(
context.contentResolver, context.contentResolver,
current, current,
DocumentsContract.Document.MIME_TYPE_DIR, DocumentsContract.Document.MIME_TYPE_DIR,
segment, segment,
) ?: throw IOException("${AppRuntime.stringResource(R.string.fm_exception_cannot_create_dir)}: $segment") )
?: throw IOException("${AppRuntime.stringResource(R.string.fm_exception_cannot_create_dir)}: $segment")
} }
return current return current
} }
@@ -653,15 +654,15 @@ object FileManagerService {
class DirectorySnapshotSession private constructor( class DirectorySnapshotSession private constructor(
private val shellSession: InteractiveShellSession, private val shellSession: InteractiveShellSession,
) : Closeable { ): Closeable {
suspend fun load(path: String): DirectoryDownloadSnapshot { suspend fun load(path: String): DirectoryDownloadSnapshot {
val normalizedPath = path.trimEnd('/').ifBlank { "/" } val normalizedPath = path.trimEnd('/').ifBlank { "/" }
val totalBytes = parseDuBytes( val totalBytes = parseDuBytes(
shellSession.execute("du -sb ${FileManagerService.quoteShellArg(normalizedPath)}") shellSession.execute("du -sb ${FileManagerService.quoteShellArg(normalizedPath)}"),
) )
val directories = shellSession.execute( val directories = shellSession.execute(
"find ${FileManagerService.quoteShellArg(normalizedPath)} -type d" "find ${FileManagerService.quoteShellArg(normalizedPath)} -type d",
) )
.lineSequence() .lineSequence()
.map(String::trim) .map(String::trim)
@@ -671,7 +672,7 @@ class DirectorySnapshotSession private constructor(
.toList() .toList()
val files = shellSession.execute( val files = shellSession.execute(
"find ${FileManagerService.quoteShellArg(normalizedPath)} -type f" "find ${FileManagerService.quoteShellArg(normalizedPath)} -type f",
) )
.lineSequence() .lineSequence()
.map(String::trim) .map(String::trim)
@@ -723,7 +724,7 @@ class DirectorySnapshotSession private constructor(
private class InteractiveShellSession private constructor( private class InteractiveShellSession private constructor(
private val stream: AdbSocketStream, private val stream: AdbSocketStream,
) : Closeable { ): Closeable {
private val mutex = Mutex() private val mutex = Mutex()
suspend fun execute(command: String): String = withContext(Dispatchers.IO) { suspend fun execute(command: String): String = withContext(Dispatchers.IO) {

View File

@@ -9,7 +9,7 @@ import io.github.miuzarte.scrcpyforandroid.BuildConfig
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
// not working in MIUI // not working in MIUI
class PictureInPictureActionReceiver : BroadcastReceiver() { class PictureInPictureActionReceiver: BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) { override fun onReceive(context: Context, intent: Intent?) {
if (intent?.action != ACTION_STOP_SCRCPY) return if (intent?.action != ACTION_STOP_SCRCPY) return
val pendingResult = goAsync() val pendingResult = goAsync()

View File

@@ -9,14 +9,14 @@ object PublicDirs {
fun transferDirectory(): File { fun transferDirectory(): File {
return File( return File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
ROOT_DIRECTORY ROOT_DIRECTORY,
) )
} }
fun recordDirectory(): File { fun recordDirectory(): File {
return File( return File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES),
ROOT_DIRECTORY ROOT_DIRECTORY,
) )
} }
} }

View File

@@ -7,7 +7,7 @@ import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
class AdbClientData(context: Context) : Settings(context, "AdbClient") { class AdbClientData(context: Context): Settings(context, "AdbClient") {
companion object { companion object {
val RSA_PRIVATE_KEY = Pair( val RSA_PRIVATE_KEY = Pair(
stringPreferencesKey("rsa_private_key"), stringPreferencesKey("rsa_private_key"),
@@ -50,7 +50,7 @@ class AdbClientData(context: Context) : Settings(context, "AdbClient") {
val importedPrivateKeyFileName: String, val importedPrivateKeyFileName: String,
val importedPublicKeyX509: String, val importedPublicKeyX509: String,
val importedPublicKeyFileName: String, val importedPublicKeyFileName: String,
) : Parcelable { ): Parcelable {
} }
private val bundleFields = arrayOf<BundleField<Bundle>>( private val bundleFields = arrayOf<BundleField<Bundle>>(

View File

@@ -3,19 +3,14 @@ package io.github.miuzarte.scrcpyforandroid.storage
import android.content.Context import android.content.Context
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import io.github.miuzarte.scrcpyforandroid.R import io.github.miuzarte.scrcpyforandroid.R
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import top.yukonga.miuix.kmp.theme.ColorSchemeMode import top.yukonga.miuix.kmp.theme.ColorSchemeMode
class AppSettings(context: Context) : Settings(context, "AppSettings") { class AppSettings(context: Context): Settings(context, "AppSettings") {
object ThemeModes { object ThemeModes {
data class Option( data class Option(
@field:StringRes val labelResId: Int, @field:StringRes val labelResId: Int,
@@ -92,6 +87,7 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
stringPreferencesKey("language_tag"), stringPreferencesKey("language_tag"),
"", "",
) )
// Theme // Theme
val THEME_BASE_INDEX = Pair( val THEME_BASE_INDEX = Pair(
intPreferencesKey("theme_base_index"), intPreferencesKey("theme_base_index"),
@@ -360,7 +356,7 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
val lastUpdateCheckAt: Long, val lastUpdateCheckAt: Long,
val clearLogsOnExit: Boolean, val clearLogsOnExit: Boolean,
val hideDeviceLogs: Boolean, val hideDeviceLogs: Boolean,
) : Parcelable { ): Parcelable {
} }
private val bundleFields = arrayOf<BundleField<Bundle>>( private val bundleFields = arrayOf<BundleField<Bundle>>(

View File

@@ -1,29 +1,9 @@
package io.github.miuzarte.scrcpyforandroid.storage package io.github.miuzarte.scrcpyforandroid.storage
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
/** /**
* Manages a local copy of a bundle from persistent storage with debounced save. * Manages a local copy of a bundle from persistent storage with debounced save.

View File

@@ -7,7 +7,7 @@ import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
class QuickDevices(context: Context) : Settings(context, "QuickDevices") { class QuickDevices(context: Context): Settings(context, "QuickDevices") {
companion object { companion object {
val QUICK_DEVICES_LIST = Pair( val QUICK_DEVICES_LIST = Pair(
stringPreferencesKey("quick_devices_list"), stringPreferencesKey("quick_devices_list"),
@@ -26,7 +26,7 @@ class QuickDevices(context: Context) : Settings(context, "QuickDevices") {
data class Bundle( data class Bundle(
val quickDevicesList: String, val quickDevicesList: String,
val quickConnectInput: String, val quickConnectInput: String,
) : Parcelable { ): Parcelable {
} }
private val bundleFields = arrayOf<BundleField<Bundle>>( private val bundleFields = arrayOf<BundleField<Bundle>>(

View File

@@ -2,31 +2,18 @@ package io.github.miuzarte.scrcpyforandroid.storage
import android.content.Context import android.content.Context
import android.os.Parcelable import android.os.Parcelable
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import io.github.miuzarte.scrcpyforandroid.R import io.github.miuzarte.scrcpyforandroid.R
import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions
import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions.KeyInjectMode import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions.KeyInjectMode
import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions.RecordFormat import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions.RecordFormat
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.AudioSource import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.*
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.CameraFacing
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.DisplayImePolicy
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.ListOptions
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.LogLevel
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Orientation
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.OrientationLock
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Tick
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.VideoSource
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.json.JSONObject import org.json.JSONObject
class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { class ScrcpyOptions(context: Context): Settings(context, "ScrcpyOptions") {
companion object { companion object {
const val GLOBAL_PROFILE_ID = "global" const val GLOBAL_PROFILE_ID = "global"
val GLOBAL_PROFILE_NAME_RES_ID = R.string.text_global val GLOBAL_PROFILE_NAME_RES_ID = R.string.text_global
@@ -435,7 +422,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
val cameraTorch: Boolean, val cameraTorch: Boolean,
val keepActive: Boolean, val keepActive: Boolean,
val flexDisplay: Boolean, val flexDisplay: Boolean,
) : Parcelable { ): Parcelable {
} }
private val bundleFields = arrayOf<BundleField<Bundle>>( private val bundleFields = arrayOf<BundleField<Bundle>>(

View File

@@ -8,7 +8,7 @@ import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.util.UUID import java.util.UUID
class ScrcpyProfiles(context: Context) : Settings(context, "ScrcpyProfiles") { class ScrcpyProfiles(context: Context): Settings(context, "ScrcpyProfiles") {
companion object { companion object {
private val PROFILES_JSON = Pair( private val PROFILES_JSON = Pair(
stringPreferencesKey("profiles_json"), stringPreferencesKey("profiles_json"),
@@ -36,7 +36,7 @@ class ScrcpyProfiles(context: Context) : Settings(context, "ScrcpyProfiles") {
private fun stateFromPreferences(preferences: androidx.datastore.preferences.core.Preferences): State { private fun stateFromPreferences(preferences: androidx.datastore.preferences.core.Preferences): State {
val raw = preferences.read(PROFILES_JSON) val raw = preferences.read(PROFILES_JSON)
return normalizeState( return normalizeState(
runCatching { decodeState(raw) }.getOrNull() ?: State(emptyList()) runCatching { decodeState(raw) }.getOrNull() ?: State(emptyList()),
) )
} }
@@ -107,8 +107,8 @@ class ScrcpyProfiles(context: Context) : Settings(context, "ScrcpyProfiles") {
current.copy( current.copy(
profiles = current.profiles.map { profiles = current.profiles.map {
if (it.id == id) updated else it if (it.id == id) updated else it
} },
) ),
) )
saveState(next) saveState(next)
return next.profiles.firstOrNull { it.id == id } return next.profiles.firstOrNull { it.id == id }
@@ -120,8 +120,8 @@ class ScrcpyProfiles(context: Context) : Settings(context, "ScrcpyProfiles") {
if (current.profiles.none { it.id == id }) return false if (current.profiles.none { it.id == id }) return false
val next = normalizeState( val next = normalizeState(
current.copy( current.copy(
profiles = current.profiles.filterNot { it.id == id } profiles = current.profiles.filterNot { it.id == id },
) ),
) )
saveState(next) saveState(next)
return true return true
@@ -213,7 +213,7 @@ class ScrcpyProfiles(context: Context) : Settings(context, "ScrcpyProfiles") {
JSONObject() JSONObject()
.put("id", profile.id) .put("id", profile.id)
.put("name", profile.name) .put("name", profile.name)
.put("bundle", encodeBundleToJson(profile.bundle)) .put("bundle", encodeBundleToJson(profile.bundle)),
) )
} }
return array.toString() return array.toString()
@@ -236,7 +236,7 @@ class ScrcpyProfiles(context: Context) : Settings(context, "ScrcpyProfiles") {
name = name.ifBlank { textNewProfile }, name = name.ifBlank { textNewProfile },
bundle = decodeBundleFromJson(item.optJSONObject("bundle")), bundle = decodeBundleFromJson(item.optJSONObject("bundle")),
isBuiltinGlobal = id == ScrcpyOptions.GLOBAL_PROFILE_ID, isBuiltinGlobal = id == ScrcpyOptions.GLOBAL_PROFILE_ID,
) ),
) )
} }
} }

View File

@@ -2,11 +2,7 @@ package io.github.miuzarte.scrcpyforandroid.storage
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
@@ -14,17 +10,8 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.*
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@@ -37,7 +24,7 @@ abstract class Settings(
ReplaceFileCorruptionHandler { ReplaceFileCorruptionHandler {
Log.e(TAG, "Preferences corrupted, resetting.", it) Log.e(TAG, "Preferences corrupted, resetting.", it)
emptyPreferences() emptyPreferences()
} },
) { ) {
private val settingsScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val settingsScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@@ -56,7 +43,7 @@ abstract class Settings(
} }
protected fun <B, T> bundleField(pair: Pair<T>, selector: (B) -> T): BundleField<B> = protected fun <B, T> bundleField(pair: Pair<T>, selector: (B) -> T): BundleField<B> =
object : BundleField<B> { object: BundleField<B> {
override suspend fun persist(settings: Settings, current: B, new: B) { override suspend fun persist(settings: Settings, current: B, new: B) {
val currentValue = selector(current) val currentValue = selector(current)
val newValue = selector(new) val newValue = selector(new)
@@ -70,7 +57,7 @@ abstract class Settings(
* 设置项委托类,自动提供 get/set/observe/asState/asMutableState 方法 * 设置项委托类,自动提供 get/set/observe/asState/asMutableState 方法
*/ */
inner class SettingProperty<T>( inner class SettingProperty<T>(
val pair: Pair<T> val pair: Pair<T>,
) { ) {
// 创建时注册自身 // 创建时注册自身
init { init {
@@ -107,7 +94,7 @@ abstract class Settings(
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore( private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = this.name, name = this.name,
corruptionHandler = corruptionHandler, corruptionHandler = corruptionHandler,
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
) )
// 对外暴露的 DataStore 实例 // 对外暴露的 DataStore 实例
@@ -121,7 +108,7 @@ abstract class Settings(
.stateIn( .stateIn(
scope = settingsScope, scope = settingsScope,
started = SharingStarted.Eagerly, started = SharingStarted.Eagerly,
initialValue = runBlocking { loadBundle(reader) } initialValue = runBlocking { loadBundle(reader) },
) )
protected suspend fun <B> loadBundle(reader: (Preferences) -> B): B = protected suspend fun <B> loadBundle(reader: (Preferences) -> B): B =
@@ -156,7 +143,7 @@ abstract class Settings(
val state = asState(pair) val state = asState(pair)
return rememberSaveable(state.value) { return rememberSaveable(state.value) {
object : MutableState<T> { object: MutableState<T> {
override var value: T override var value: T
get() = state.value get() = state.value
set(newValue) { set(newValue) {

View File

@@ -6,11 +6,7 @@ import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import top.yukonga.miuix.kmp.blur.BlendColorEntry import top.yukonga.miuix.kmp.blur.*
import top.yukonga.miuix.kmp.blur.BlurColors
import top.yukonga.miuix.kmp.blur.LayerBackdrop
import top.yukonga.miuix.kmp.blur.rememberLayerBackdrop
import top.yukonga.miuix.kmp.blur.textureBlur
import top.yukonga.miuix.kmp.shader.isRenderEffectSupported import top.yukonga.miuix.kmp.shader.isRenderEffectSupported
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme

View File

@@ -5,30 +5,9 @@ import androidx.compose.animation.core.EaseOut
import androidx.compose.animation.core.spring import androidx.compose.animation.core.spring
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
@@ -84,7 +63,7 @@ fun RowScope.FloatingBottomBarItem(
interactionSource = null, interactionSource = null,
indication = null, indication = null,
role = Role.Tab, role = Role.Tab,
onClick = onClick onClick = onClick,
) )
.fillMaxHeight() .fillMaxHeight()
.weight(1f) .weight(1f)
@@ -95,7 +74,7 @@ fun RowScope.FloatingBottomBarItem(
}, },
verticalArrangement = Arrangement.spacedBy(1.dp, Alignment.CenterVertically), verticalArrangement = Arrangement.spacedBy(1.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
content = content content = content,
) )
} }
@@ -184,13 +163,13 @@ fun FloatingBottomBar(
if (tabWidthPx > 0) { if (tabWidthPx > 0) {
updateValue( updateValue(
(targetValue + dragAmount.x / tabWidthPx * if (isLtr) 1f else -1f) (targetValue + dragAmount.x / tabWidthPx * if (isLtr) 1f else -1f)
.fastCoerceIn(0f, (tabsCount - 1).toFloat()) .fastCoerceIn(0f, (tabsCount - 1).toFloat()),
) )
animationScope.launch { animationScope.launch {
offsetAnimation.snapTo(offsetAnimation.value + dragAmount.x) offsetAnimation.snapTo(offsetAnimation.value + dragAmount.x)
} }
} }
} },
).also { holder.instance = it } ).also { holder.instance = it }
} }
@@ -214,15 +193,15 @@ fun FloatingBottomBar(
} else { } else {
size.width - (dampedDragAnimation.value + 0.5f) * tabWidthPx + panelOffset size.width - (dampedDragAnimation.value + 0.5f) * tabWidthPx + panelOffset
}, },
size.height / 2f size.height / 2f,
) )
} },
) )
} }
Box( Box(
modifier = modifier.width(IntrinsicSize.Min), modifier = modifier.width(IntrinsicSize.Min),
contentAlignment = Alignment.CenterStart contentAlignment = Alignment.CenterStart,
) { ) {
Row( Row(
Modifier Modifier
@@ -235,7 +214,7 @@ fun FloatingBottomBar(
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = null, indication = null,
onClick = {} onClick = {},
) )
.drawBackdrop( .drawBackdrop(
backdrop = backdrop, backdrop = backdrop,
@@ -263,20 +242,20 @@ fun FloatingBottomBar(
scaleY = scale scaleY = scale
} }
}, },
onDrawSurface = { drawRect(containerColor) } onDrawSurface = { drawRect(containerColor) },
) )
.then(if (isBlurEnabled) interactiveHighlight.modifier else Modifier) .then(if (isBlurEnabled) interactiveHighlight.modifier else Modifier)
.height(64.dp) .height(64.dp)
.padding(4.dp), .padding(4.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
content = content content = content,
) )
CompositionLocalProvider( CompositionLocalProvider(
LocalFloatingBottomBarTabScale provides { LocalFloatingBottomBarTabScale provides {
if (isBlurEnabled) lerp(1f, 1.2f, dampedDragAnimation.pressProgress) if (isBlurEnabled) lerp(1f, 1.2f, dampedDragAnimation.pressProgress)
else 1f else 1f
} },
) { ) {
Row( Row(
Modifier Modifier
@@ -298,14 +277,14 @@ fun FloatingBottomBar(
highlight = { highlight = {
Highlight.Default.copy(alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f) Highlight.Default.copy(alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f)
}, },
onDrawSurface = { drawRect(containerColor) } onDrawSurface = { drawRect(containerColor) },
) )
.then(if (isBlurEnabled) interactiveHighlight.modifier else Modifier) .then(if (isBlurEnabled) interactiveHighlight.modifier else Modifier)
.height(56.dp) .height(56.dp)
.padding(horizontal = 4.dp) .padding(horizontal = 4.dp)
.graphicsLayer(colorFilter = ColorFilter.tint(accentColor)), .graphicsLayer(colorFilter = ColorFilter.tint(accentColor)),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
content = content content = content,
) )
} }
@@ -343,7 +322,7 @@ fun FloatingBottomBar(
innerShadow = { innerShadow = {
InnerShadow( InnerShadow(
radius = 8f.dp * dampedDragAnimation.pressProgress, radius = 8f.dp * dampedDragAnimation.pressProgress,
alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f,
) )
}, },
layerBlock = { layerBlock = {
@@ -364,15 +343,15 @@ fun FloatingBottomBar(
} else { } else {
Color.White.copy(0.1f) Color.White.copy(0.1f)
}, },
alpha = 1f - progress alpha = 1f - progress,
) )
drawRect( drawRect(
Color.Black.copy(alpha = 0.03f * progress) Color.Black.copy(alpha = 0.03f * progress),
) )
} },
) )
.height(56.dp) .height(56.dp)
.width(with(density) { ((totalWidthPx - 8.dp.toPx()) / tabsCount).toDp() }) .width(with(density) { ((totalWidthPx - 8.dp.toPx()) / tabsCount).toDp() }),
) )
} }
} }

View File

@@ -65,7 +65,7 @@ class DampedDragAnimation(
onDragCancel = { onDragCancel = {
onDragStopped() onDragStopped()
release() release()
} },
) { change, dragAmount -> ) { change, dragAmount ->
val position = change.position val position = change.position
val previousPosition = change.previousPosition val previousPosition = change.previousPosition
@@ -109,7 +109,7 @@ class DampedDragAnimation(
launch { launch {
valueAnimation.animateTo( valueAnimation.animateTo(
targetValue, targetValue,
valueAnimationSpec valueAnimationSpec,
) { updateVelocity() } ) { updateVelocity() }
} }
} }
@@ -132,7 +132,7 @@ class DampedDragAnimation(
private fun updateVelocity() { private fun updateVelocity() {
velocityTracker.addPosition( velocityTracker.addPosition(
System.currentTimeMillis(), System.currentTimeMillis(),
Offset(value, 0f) Offset(value, 0f),
) )
val targetVelocity = val targetVelocity =
velocityTracker.calculateVelocity().x / (valueRange.endInclusive - valueRange.start) velocityTracker.calculateVelocity().x / (valueRange.endInclusive - valueRange.start)

View File

@@ -49,7 +49,7 @@ class InteractiveHighlight(
float dist = distance(coord, position); float dist = distance(coord, position);
float intensity = smoothstep(radius, radius * 0.5, dist); float intensity = smoothstep(radius, radius * 0.5, dist);
return color * intensity; return color * intensity;
}""" }""",
) )
val modifier: Modifier = Modifier.drawWithContent { val modifier: Modifier = Modifier.drawWithContent {
@@ -57,7 +57,7 @@ class InteractiveHighlight(
if (progress > 0f) { if (progress > 0f) {
drawRect( drawRect(
Color.White.copy(0.06f * progress), Color.White.copy(0.06f * progress),
blendMode = BlendMode.Plus blendMode = BlendMode.Plus,
) )
shader.apply { shader.apply {
val shaderPosition = position(size, positionAnimation.value) val shaderPosition = position(size, positionAnimation.value)
@@ -70,7 +70,7 @@ class InteractiveHighlight(
} }
drawRect( drawRect(
ShaderBrush(shader), ShaderBrush(shader),
blendMode = BlendMode.Plus blendMode = BlendMode.Plus,
) )
} }
@@ -97,7 +97,7 @@ class InteractiveHighlight(
launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) } launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) }
launch { positionAnimation.animateTo(startPosition, positionAnimationSpec) } launch { positionAnimation.animateTo(startPosition, positionAnimationSpec) }
} }
} },
) { change, _ -> ) { change, _ ->
animationScope.launch { positionAnimation.snapTo(change.position) } animationScope.launch { positionAnimation.snapTo(change.position) }
} }

View File

@@ -3,13 +3,7 @@ package io.github.miuzarte.scrcpyforandroid.ui.component.miuix.modifier
import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.util.fastFirstOrNull import androidx.compose.ui.util.fastFirstOrNull
suspend fun PointerInputScope.inspectDragGestures( suspend fun PointerInputScope.inspectDragGestures(
@@ -26,7 +20,7 @@ suspend fun PointerInputScope.inspectDragGestures(
onDrag(initialDown, Offset.Zero) onDrag(initialDown, Offset.Zero)
val upEvent = drag( val upEvent = drag(
pointerId = initialDown.id, pointerId = initialDown.id,
onDrag = { onDrag(it, it.positionChange()) } onDrag = { onDrag(it, it.positionChange()) },
) )
if (upEvent == null) { if (upEvent == null) {
onDragCancel() onDragCancel()

View File

@@ -2,13 +2,7 @@ package io.github.miuzarte.scrcpyforandroid.widgets
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -22,11 +16,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.github.miuzarte.scrcpyforandroid.R import io.github.miuzarte.scrcpyforandroid.R
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
import top.yukonga.miuix.kmp.basic.DropdownColors import top.yukonga.miuix.kmp.basic.*
import top.yukonga.miuix.kmp.basic.DropdownDefaults
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.IconButton
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.extended.Store import top.yukonga.miuix.kmp.icon.extended.Store
import top.yukonga.miuix.kmp.overlay.OverlayBottomSheet import top.yukonga.miuix.kmp.overlay.OverlayBottomSheet

View File

@@ -13,7 +13,7 @@ class ScrcpyInputSurfaceView @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0, defStyleAttr: Int = 0,
) : SurfaceView(context, attrs, defStyleAttr) { ): SurfaceView(context, attrs, defStyleAttr) {
interface InputCallbacks { interface InputCallbacks {
fun handleKeyEvent(event: KeyEvent): Boolean fun handleKeyEvent(event: KeyEvent): Boolean
fun handleCommitText(text: CharSequence): Boolean fun handleCommitText(text: CharSequence): Boolean
@@ -46,7 +46,7 @@ class ScrcpyInputSurfaceView @JvmOverloads constructor(
outAttrs.inputType = InputType.TYPE_CLASS_TEXT outAttrs.inputType = InputType.TYPE_CLASS_TEXT
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
return object : BaseInputConnection(this, false) { return object: BaseInputConnection(this, false) {
override fun commitText(text: CharSequence, newCursorPosition: Int): Boolean { override fun commitText(text: CharSequence, newCursorPosition: Int): Boolean {
if (inputCallbacks?.handleCommitText(text) == true) return true if (inputCallbacks?.handleCommitText(text) == true) return true
return super.commitText(text, newCursorPosition) return super.commitText(text, newCursorPosition)

View File

@@ -1,19 +1,6 @@
package io.github.miuzarte.scrcpyforandroid.widgets package io.github.miuzarte.scrcpyforandroid.widgets
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -129,7 +116,7 @@ internal fun StatusCardLayout(
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.fillMaxHeight() .fillMaxHeight(),
) { ) {
StatusMetricCard( StatusMetricCard(
modifier = Modifier modifier = Modifier

View File

@@ -13,7 +13,7 @@ class TerminalInputView @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0, defStyleAttr: Int = 0,
) : View(context, attrs, defStyleAttr) { ): View(context, attrs, defStyleAttr) {
interface InputCallbacks { interface InputCallbacks {
fun handleKeyEvent(event: KeyEvent): Boolean fun handleKeyEvent(event: KeyEvent): Boolean
fun handleCommitText(text: CharSequence): Boolean fun handleCommitText(text: CharSequence): Boolean
@@ -51,7 +51,7 @@ class TerminalInputView @JvmOverloads constructor(
outAttrs.inputType = InputType.TYPE_CLASS_TEXT outAttrs.inputType = InputType.TYPE_CLASS_TEXT
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
return object : BaseInputConnection(this, false) { return object: BaseInputConnection(this, false) {
override fun commitText(text: CharSequence, newCursorPosition: Int): Boolean { override fun commitText(text: CharSequence, newCursorPosition: Int): Boolean {
if (inputCallbacks?.handleCommitText(text) == true) return true if (inputCallbacks?.handleCommitText(text) == true) return true
return super.commitText(text, newCursorPosition) return super.commitText(text, newCursorPosition)

View File

@@ -3,48 +3,16 @@ package io.github.miuzarte.scrcpyforandroid.widgets
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.automirrored.rounded.VolumeDown import androidx.compose.material.icons.automirrored.rounded.VolumeDown
import androidx.compose.material.icons.automirrored.rounded.VolumeOff import androidx.compose.material.icons.automirrored.rounded.VolumeOff
import androidx.compose.material.icons.automirrored.rounded.VolumeUp import androidx.compose.material.icons.automirrored.rounded.VolumeUp
import androidx.compose.material.icons.rounded.Apps import androidx.compose.material.icons.rounded.*
import androidx.compose.material.icons.rounded.ContentPaste import androidx.compose.runtime.*
import androidx.compose.material.icons.rounded.DashboardCustomize
import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.Keyboard
import androidx.compose.material.icons.rounded.Menu
import androidx.compose.material.icons.rounded.Notifications
import androidx.compose.material.icons.rounded.Password
import androidx.compose.material.icons.rounded.PowerSettingsNew
import androidx.compose.material.icons.rounded.Screenshot
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -69,20 +37,13 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import top.yukonga.miuix.kmp.basic.Button import top.yukonga.miuix.kmp.basic.*
import top.yukonga.miuix.kmp.basic.ButtonDefaults
import top.yukonga.miuix.kmp.basic.DropdownDefaults
import top.yukonga.miuix.kmp.basic.DropdownItem
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.ListPopupColumn
import top.yukonga.miuix.kmp.basic.ListPopupDefaults
import top.yukonga.miuix.kmp.basic.PopupPositionProvider
import top.yukonga.miuix.kmp.basic.SpinnerItemImpl
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.extended.More import top.yukonga.miuix.kmp.icon.extended.More
import top.yukonga.miuix.kmp.overlay.OverlayListPopup import top.yukonga.miuix.kmp.overlay.OverlayListPopup
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
import kotlin.ranges.coerceAtLeast
import kotlin.ranges.coerceIn
enum class VirtualButtonAction( enum class VirtualButtonAction(
val id: String, val id: String,
@@ -94,97 +55,97 @@ enum class VirtualButtonAction(
"more", "more",
R.string.vb_more, R.string.vb_more,
MiuixIcons.More, MiuixIcons.More,
null null,
), ),
HOME( HOME(
"home", "home",
R.string.vb_home, R.string.vb_home,
Icons.Rounded.Home, Icons.Rounded.Home,
UiAndroidKeycodes.HOME UiAndroidKeycodes.HOME,
), ),
BACK( BACK(
"back", "back",
R.string.vb_back, R.string.vb_back,
Icons.AutoMirrored.Rounded.ArrowBack, Icons.AutoMirrored.Rounded.ArrowBack,
UiAndroidKeycodes.BACK UiAndroidKeycodes.BACK,
), ),
APP_SWITCH( APP_SWITCH(
"app_switch", "app_switch",
R.string.vb_app_switch, R.string.vb_app_switch,
Icons.Rounded.Apps, Icons.Rounded.Apps,
UiAndroidKeycodes.APP_SWITCH UiAndroidKeycodes.APP_SWITCH,
), ),
MENU( MENU(
"menu", "menu",
R.string.vb_menu, R.string.vb_menu,
Icons.Rounded.Menu, Icons.Rounded.Menu,
UiAndroidKeycodes.MENU UiAndroidKeycodes.MENU,
), ),
NOTIFICATION( NOTIFICATION(
"notification", "notification",
R.string.vb_notifications, R.string.vb_notifications,
Icons.Rounded.Notifications, Icons.Rounded.Notifications,
UiAndroidKeycodes.NOTIFICATION UiAndroidKeycodes.NOTIFICATION,
), ),
VOLUME_UP( VOLUME_UP(
"volume_up", "volume_up",
R.string.vb_volume_up, R.string.vb_volume_up,
Icons.AutoMirrored.Rounded.VolumeUp, Icons.AutoMirrored.Rounded.VolumeUp,
UiAndroidKeycodes.VOLUME_UP UiAndroidKeycodes.VOLUME_UP,
), ),
VOLUME_DOWN( VOLUME_DOWN(
"volume_down", "volume_down",
R.string.vb_volume_down, R.string.vb_volume_down,
Icons.AutoMirrored.Rounded.VolumeDown, Icons.AutoMirrored.Rounded.VolumeDown,
UiAndroidKeycodes.VOLUME_DOWN UiAndroidKeycodes.VOLUME_DOWN,
), ),
VOLUME_MUTE( VOLUME_MUTE(
"volume_mute", "volume_mute",
R.string.vb_volume_mute, R.string.vb_volume_mute,
Icons.AutoMirrored.Rounded.VolumeOff, Icons.AutoMirrored.Rounded.VolumeOff,
UiAndroidKeycodes.VOLUME_MUTE UiAndroidKeycodes.VOLUME_MUTE,
), ),
POWER( POWER(
"power", "power",
R.string.vb_lock_screen, R.string.vb_lock_screen,
Icons.Rounded.PowerSettingsNew, Icons.Rounded.PowerSettingsNew,
UiAndroidKeycodes.POWER UiAndroidKeycodes.POWER,
), ),
SCREENSHOT( SCREENSHOT(
"screenshot", "screenshot",
R.string.vb_screenshot, R.string.vb_screenshot,
Icons.Rounded.Screenshot, Icons.Rounded.Screenshot,
UiAndroidKeycodes.SYSRQ UiAndroidKeycodes.SYSRQ,
), ),
PASSWORD_INPUT( PASSWORD_INPUT(
"password_input", "password_input",
R.string.vb_fill_password, R.string.vb_fill_password,
Icons.Rounded.Password, Icons.Rounded.Password,
null null,
), ),
ALL_APPS( ALL_APPS(
"all_apps", "all_apps",
R.string.vb_all_apps, R.string.vb_all_apps,
Icons.Rounded.Apps, Icons.Rounded.Apps,
null null,
), ),
RECENT_TASKS( RECENT_TASKS(
"recent_tasks", "recent_tasks",
R.string.vb_recent_tasks, R.string.vb_recent_tasks,
Icons.Rounded.DashboardCustomize, Icons.Rounded.DashboardCustomize,
null null,
), ),
TOGGLE_IME( TOGGLE_IME(
"toggle_ime", "toggle_ime",
R.string.vb_toggle_ime, R.string.vb_toggle_ime,
Icons.Rounded.Keyboard, Icons.Rounded.Keyboard,
null null,
), ),
PASTE_LOCAL_CLIPBOARD( PASTE_LOCAL_CLIPBOARD(
"paste_local_clipboard", "paste_local_clipboard",
R.string.vb_paste_clipboard, R.string.vb_paste_clipboard,
Icons.Rounded.ContentPaste, Icons.Rounded.ContentPaste,
null null,
); );
} }
@@ -418,7 +379,7 @@ class VirtualButtonBar(
Icon( Icon(
imageVector = action.icon, imageVector = action.icon,
contentDescription = stringResource(action.titleResId), contentDescription = stringResource(action.titleResId),
tint = Color.White tint = Color.White,
) )
} }
@@ -475,7 +436,7 @@ class VirtualButtonBar(
Icon( Icon(
imageVector = action.icon, imageVector = action.icon,
contentDescription = stringResource(action.titleResId), contentDescription = stringResource(action.titleResId),
tint = Color.White tint = Color.White,
) )
} }
@@ -546,7 +507,7 @@ class VirtualButtonBar(
latest.copy( latest.copy(
fullscreenFloatingButtonXFraction = offsetXFraction, fullscreenFloatingButtonXFraction = offsetXFraction,
fullscreenFloatingButtonYFraction = offsetYFraction, fullscreenFloatingButtonYFraction = offsetYFraction,
) ),
) )
} }
} }
@@ -637,7 +598,7 @@ class VirtualButtonBar(
) )
} else { } else {
Modifier Modifier
} },
), ),
) )
} }
@@ -785,7 +746,7 @@ class VirtualButtonBar(
private class BottomSafeContextMenuPositionProvider( private class BottomSafeContextMenuPositionProvider(
private val bottomPadding: Dp, private val bottomPadding: Dp,
) : PopupPositionProvider { ): PopupPositionProvider {
private val delegate = ListPopupDefaults.ContextMenuPositionProvider private val delegate = ListPopupDefaults.ContextMenuPositionProvider
override fun calculatePosition( override fun calculatePosition(