refactor: improve base class Settings
This commit is contained in:
@@ -23,8 +23,8 @@ android {
|
||||
applicationId = "io.github.miuzarte.scrcpyforandroid"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 5
|
||||
versionName = "0.0.5"
|
||||
versionCode = 6
|
||||
versionName = "0.1.0"
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
|
||||
@@ -5,10 +5,15 @@ import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import io.github.miuzarte.scrcpyforandroid.pages.MainPage
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// initialize settings singleton
|
||||
Storage.init(applicationContext)
|
||||
|
||||
enableEdgeToEdge()
|
||||
|
||||
setContent {
|
||||
|
||||
@@ -4,8 +4,10 @@ import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.core.content.edit
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.AdbClientData
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.Closeable
|
||||
import java.io.EOFException
|
||||
@@ -44,7 +46,7 @@ import kotlin.concurrent.thread
|
||||
*/
|
||||
internal class DirectAdbTransport(private val context: Context) {
|
||||
|
||||
private val keys: Pair<PrivateKey, ByteArray> by lazy { loadOrCreate() }
|
||||
private val keys: Pair<PrivateKey, ByteArray> by lazy { runBlocking { loadOrCreate() } }
|
||||
|
||||
val privateKey: PrivateKey get() = keys.first
|
||||
val publicKeyX509: ByteArray get() = keys.second
|
||||
@@ -101,40 +103,52 @@ internal class DirectAdbTransport(private val context: Context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load persisted RSA keypair from shared preferences, or generate a new one.
|
||||
* Load persisted RSA keypair from DataStore, or generate a new one.
|
||||
* Returns (privateKey, publicX509Bytes).
|
||||
*/
|
||||
private fun loadOrCreate(): Pair<PrivateKey, ByteArray> {
|
||||
// TODO: migrate to data store
|
||||
val prefs = context.getSharedPreferences(
|
||||
"nativecore_adb_rsa",
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
val privB64 = prefs.getString("priv", null)
|
||||
if (privB64 != null) {
|
||||
private suspend fun loadOrCreate(
|
||||
forceNew: Boolean = false,
|
||||
): Pair<PrivateKey, ByteArray> {
|
||||
val adbClientData = Storage.adbClientData
|
||||
|
||||
val privB64 = adbClientData.rsaPrivateKey.get()
|
||||
|
||||
if (privB64.isNotBlank() && !forceNew) {
|
||||
try {
|
||||
val kf = KeyFactory.getInstance("RSA")
|
||||
val priv =
|
||||
kf.generatePrivate(PKCS8EncodedKeySpec(Base64.decode(privB64, Base64.DEFAULT)))
|
||||
val priv = kf.generatePrivate(
|
||||
PKCS8EncodedKeySpec(
|
||||
Base64.decode(privB64, Base64.DEFAULT)
|
||||
)
|
||||
)
|
||||
val pub = derivePublicX509(priv)
|
||||
Log.i(TAG, "loadOrCreate(): loaded persisted RSA key pair, fp=${fingerprint(pub)}")
|
||||
Log.i(
|
||||
TAG,
|
||||
"loadOrCreate(): loaded persisted RSA key pair from DataStore, " +
|
||||
"fp=${fingerprint(pub)}"
|
||||
)
|
||||
return Pair(priv, pub)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "loadOrCreate(): failed to load persisted key, regenerating", e)
|
||||
Log.w(
|
||||
TAG,
|
||||
"loadOrCreate(): failed to load persisted key from DataStore, regenerating",
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
val kpg = KeyPairGenerator.getInstance("RSA")
|
||||
kpg.initialize(2048)
|
||||
val kp = kpg.generateKeyPair()
|
||||
prefs.edit {
|
||||
putString(
|
||||
"priv",
|
||||
Base64.encodeToString(kp.private.encoded, Base64.NO_WRAP)
|
||||
)
|
||||
}
|
||||
|
||||
// 保存到 DataStore
|
||||
val privateKeyB64 = Base64.encodeToString(kp.private.encoded, Base64.NO_WRAP)
|
||||
val publicKeyB64 = Base64.encodeToString(kp.public.encoded, Base64.NO_WRAP)
|
||||
adbClientData.rsaPrivateKey.set(privateKeyB64)
|
||||
adbClientData.rsaPublicKeyX509.set(publicKeyB64)
|
||||
|
||||
Log.i(
|
||||
TAG,
|
||||
"loadOrCreate(): generated new RSA key pair, fp=${fingerprint(kp.public.encoded)}"
|
||||
"loadOrCreate(): generated new RSA key pair and saved to DataStore, fp=${fingerprint(kp.public.encoded)}"
|
||||
)
|
||||
return Pair(kp.private, kp.public.encoded)
|
||||
}
|
||||
|
||||
@@ -28,8 +28,7 @@ import io.github.miuzarte.scrcpyforandroid.constants.ScrcpyPresets
|
||||
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn
|
||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlide
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage
|
||||
import kotlinx.coroutines.launch
|
||||
import top.yukonga.miuix.kmp.basic.Card
|
||||
import top.yukonga.miuix.kmp.basic.ScrollBehavior
|
||||
@@ -89,10 +88,9 @@ internal fun AdvancedConfigPage(
|
||||
onRefreshEncoders: () -> Unit,
|
||||
onRefreshCameraSizes: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scrcpyOptions = Storage.scrcpyOptions
|
||||
|
||||
val appSettings = remember(context) { AppSettings(context) }
|
||||
val scrcpyOptions = remember(context) { ScrcpyOptions(context) }
|
||||
val context = LocalContext.current
|
||||
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
|
||||
@@ -34,11 +34,7 @@ import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.ListOptions
|
||||
import io.github.miuzarte.scrcpyforandroid.services.EventLogger
|
||||
import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent
|
||||
import io.github.miuzarte.scrcpyforandroid.services.fetchConnectedDeviceInfo
|
||||
import io.github.miuzarte.scrcpyforandroid.services.parseQuickTarget
|
||||
import io.github.miuzarte.scrcpyforandroid.services.upsertQuickDevice
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.QuickDevices
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.ConfigPanel
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.DeviceEditorScreen
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.DeviceTile
|
||||
@@ -131,11 +127,11 @@ fun DeviceTabScreen(
|
||||
onOpenAdvancedPage: () -> Unit,
|
||||
onOpenFullscreenPage: (ScrcpySessionInfo) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val appSettings = Storage.appSettings
|
||||
val quickDevices = Storage.quickDevices
|
||||
val scrcpyOptions = Storage.scrcpyOptions
|
||||
|
||||
val appSettings = remember(context) { AppSettings(context) }
|
||||
val quickDevices = remember(context) { QuickDevices(context) }
|
||||
val scrcpyOptions = remember(context) { ScrcpyOptions(context) }
|
||||
val context = LocalContext.current
|
||||
|
||||
val haptics = rememberAppHaptics()
|
||||
|
||||
@@ -216,11 +212,19 @@ fun DeviceTabScreen(
|
||||
}
|
||||
|
||||
var quickDevicesList by quickDevices.quickDevicesList.asMutableState()
|
||||
var savedShortcuts = rememberSaveable(saver = DeviceShortcutsSaver) {
|
||||
var savedShortcuts = rememberSaveable(quickDevicesList, saver = DeviceShortcutsSaver) {
|
||||
DeviceShortcuts.unmarshalFrom(quickDevicesList)
|
||||
}
|
||||
var quickConnectInput by quickDevices.quickConnectInput.asMutableState()
|
||||
|
||||
// save changes when [savedShortcuts] was modified
|
||||
LaunchedEffect(savedShortcuts) {
|
||||
val serialized = savedShortcuts.marshalToString()
|
||||
if (serialized != quickDevicesList) {
|
||||
quickDevicesList = serialized
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the current ADB connection and stop any running scrcpy session.
|
||||
*
|
||||
@@ -232,7 +236,7 @@ fun DeviceTabScreen(
|
||||
* - Calls `nativeCore.scrcpyStop()` and `nativeCore.adbDisconnect()` (best-effort).
|
||||
* - Resets UI-visible connection state: `adbConnected`, `currentTargetHost/Port`,
|
||||
* `sessionInfo`, device capability flags, `statusLine`, and `connectedDeviceLabel`.
|
||||
* - Updates the saved quick-device list via [upsertQuickDevice] when a target is provided.
|
||||
* - Updates the saved quick-device list via [savedShortcuts.update] when a target is provided.
|
||||
* - Logs an optional [logMessage] to the local event log.
|
||||
* - Shows an optional snackbar message asynchronously (launched on the composition scope)
|
||||
* so callers don't get blocked by `snack.showSnackbar` (it is suspending).
|
||||
@@ -263,8 +267,9 @@ fun DeviceTabScreen(
|
||||
connectedDeviceLabel = "未连接"
|
||||
clearQuickOnlineForTarget?.let { target ->
|
||||
if (target.host.isNotBlank())
|
||||
savedShortcuts =
|
||||
savedShortcuts.update(host = target.host, port = target.port, online = false)
|
||||
savedShortcuts = savedShortcuts.update(
|
||||
host = target.host, port = target.port, online = false
|
||||
)
|
||||
}
|
||||
logMessage?.let { logEvent(it) }
|
||||
if (!showSnackMessage.isNullOrBlank()) {
|
||||
@@ -539,10 +544,24 @@ fun DeviceTabScreen(
|
||||
|
||||
connectedDeviceLabel = info.model
|
||||
applyConnectedDeviceCapabilities(info.sdkInt, info.androidRelease)
|
||||
quickDevices.updateQuickDeviceNameIfEmpty(host, port, fullLabel)
|
||||
savedShortcuts = savedShortcuts.update(
|
||||
host = host,
|
||||
port = port,
|
||||
name = fullLabel,
|
||||
updateNameOnlyWhenEmpty = true
|
||||
)
|
||||
statusLine = "$host:$port"
|
||||
|
||||
logEvent("ADB 已连接: model=${info.model}, serial=${info.serial.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}")
|
||||
logEvent(
|
||||
"ADB 已连接: " +
|
||||
"model=${info.model}, " +
|
||||
"serial=${info.serial.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}"
|
||||
)
|
||||
scope.launch {
|
||||
snack.showSnackbar("ADB 已连接")
|
||||
}
|
||||
@@ -857,7 +876,8 @@ fun DeviceTabScreen(
|
||||
try {
|
||||
connectWithTimeout(host, port)
|
||||
adbConnected = true
|
||||
savedShortcuts = savedShortcuts.update(host = host, port = port, online = true)
|
||||
savedShortcuts =
|
||||
savedShortcuts.update(host = host, port = port, online = true)
|
||||
handleAdbConnected(host, port)
|
||||
} catch (e: Exception) {
|
||||
statusLine = "ADB 连接失败"
|
||||
@@ -886,23 +906,32 @@ fun DeviceTabScreen(
|
||||
// "快速连接"
|
||||
QuickConnectCard(
|
||||
input = quickConnectInput,
|
||||
onInputChange = { quickConnectInput = it },
|
||||
onValueChange = { quickConnectInput = it },
|
||||
enabled = !adbConnecting,
|
||||
onAddDevice = {
|
||||
val target = parseQuickTarget(quickConnectInput) ?: return@QuickConnectCard
|
||||
savedShortcuts = savedShortcuts.update(host = target.host, port = target.port)
|
||||
val target = ConnectionTarget.unmarshalFrom(quickConnectInput)
|
||||
?: return@QuickConnectCard
|
||||
savedShortcuts = savedShortcuts.upsert(
|
||||
DeviceShortcut(host = target.host, port = target.port)
|
||||
)
|
||||
Log.i("SavedShortcuts", "size: ${savedShortcuts.size}, list: $quickDevicesList")
|
||||
scope.launch {
|
||||
snack.showSnackbar("已添加设备: ${target.host}:${target.port}")
|
||||
}
|
||||
},
|
||||
onConnect = {
|
||||
val target = parseQuickTarget(quickConnectInput) ?: return@QuickConnectCard
|
||||
val target = ConnectionTarget.unmarshalFrom(quickConnectInput)
|
||||
?: return@QuickConnectCard
|
||||
runAdbConnect("连接 ADB", onFinished = { activeDeviceActionId = null }) {
|
||||
disconnectCurrentTargetBeforeConnecting(target.host, target.port)
|
||||
try {
|
||||
connectWithTimeout(target.host, target.port)
|
||||
adbConnected = true
|
||||
savedShortcuts = savedShortcuts.update(host = target.host, port = target.port, online = true)
|
||||
savedShortcuts = savedShortcuts.update(
|
||||
host = target.host,
|
||||
port = target.port,
|
||||
online = true
|
||||
)
|
||||
handleAdbConnected(target.host, target.port)
|
||||
} catch (e: Exception) {
|
||||
statusLine = "ADB 连接失败"
|
||||
|
||||
@@ -22,8 +22,7 @@ import androidx.core.view.WindowInsetsControllerCompat
|
||||
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade
|
||||
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade.ScrcpySessionInfo
|
||||
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.FullscreenControlScreen
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonBar
|
||||
@@ -46,10 +45,9 @@ fun FullscreenControlPage(
|
||||
// Disable predictive back handler temporarily to avoid decoding issues.
|
||||
BackHandler(enabled = true, onBack = onDismiss)
|
||||
|
||||
val context = LocalContext.current
|
||||
val appSettings = Storage.appSettings
|
||||
|
||||
val appSettings = remember(context) { AppSettings(context) }
|
||||
val scrcpyOptions = remember(context) { ScrcpyOptions(context) }
|
||||
val context = LocalContext.current
|
||||
|
||||
val haptics = rememberAppHaptics()
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
||||
import io.github.miuzarte.scrcpyforandroid.nativecore.NativeAdbService
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -191,8 +191,8 @@ fun MainPage() {
|
||||
}
|
||||
}
|
||||
|
||||
val appSettings = remember(context) { AppSettings(context) }
|
||||
val scrcpyOptions = remember(context) { ScrcpyOptions(context) }
|
||||
val appSettings = Storage.appSettings
|
||||
val scrcpyOptions = Storage.scrcpyOptions
|
||||
|
||||
val videoEncoderOptions = remember { mutableStateListOf<String>() }
|
||||
val audioEncoderOptions = remember { mutableStateListOf<String>() }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.github.miuzarte.scrcpyforandroid.pages
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Process
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -13,6 +14,7 @@ import androidx.compose.material.icons.rounded.FileOpen
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -22,12 +24,16 @@ import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn
|
||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlide
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.PreferenceMigration
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.SectionSmallTitle
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import top.yukonga.miuix.kmp.basic.Card
|
||||
import top.yukonga.miuix.kmp.basic.Icon
|
||||
import top.yukonga.miuix.kmp.basic.IconButton
|
||||
import top.yukonga.miuix.kmp.basic.ScrollBehavior
|
||||
import top.yukonga.miuix.kmp.basic.SnackbarHostState
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
import top.yukonga.miuix.kmp.basic.TextField
|
||||
import top.yukonga.miuix.kmp.extra.SuperArrow
|
||||
@@ -35,6 +41,7 @@ import top.yukonga.miuix.kmp.extra.SuperDropdown
|
||||
import top.yukonga.miuix.kmp.extra.SuperSwitch
|
||||
import top.yukonga.miuix.kmp.theme.ColorSchemeMode
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
private data class ThemeModeOption(
|
||||
val label: String,
|
||||
@@ -68,10 +75,13 @@ fun SettingsScreen(
|
||||
onPickServer: () -> Unit,
|
||||
scrollBehavior: ScrollBehavior,
|
||||
) {
|
||||
val appContext = LocalContext.current.applicationContext
|
||||
val appSettings = Storage.appSettings
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
val appSettings = remember(context) { AppSettings(context) }
|
||||
val scrcpyOptions = remember(context) { ScrcpyOptions(context) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackHostState = remember { SnackbarHostState() }
|
||||
|
||||
val baseModeItems = THEME_BASE_OPTIONS.map { it.label }
|
||||
|
||||
@@ -255,17 +265,49 @@ fun SettingsScreen(
|
||||
)
|
||||
}
|
||||
|
||||
SectionSmallTitle("应用")
|
||||
Card {
|
||||
SuperArrow(
|
||||
title = "恢复旧版本配置",
|
||||
summary = "从旧版本的 SharedPreferences 恢复至 DataStore",
|
||||
onClick = {
|
||||
scope.launch {
|
||||
val migration = PreferenceMigration(appContext)
|
||||
migration.migrate(clearSharedPrefs = false)
|
||||
snackHostState.showSnackbar("迁移完成,应用将重启")
|
||||
|
||||
delay(1000)
|
||||
|
||||
val intent = context.packageManager.getLaunchIntentForPackage(
|
||||
context.packageName
|
||||
)
|
||||
intent?.apply {
|
||||
addFlags(
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
|
||||
Process.killProcess(Process.myPid())
|
||||
exitProcess(0)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
SectionSmallTitle("关于")
|
||||
Card {
|
||||
SuperArrow(
|
||||
title = "前往仓库",
|
||||
summary = "github.com/Miuzarte/ScrcpyForAndroid",
|
||||
onClick = {
|
||||
val intent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
"https://github.com/Miuzarte/ScrcpyForAndroid".toUri()
|
||||
context.startActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
"https://github.com/Miuzarte/ScrcpyForAndroid".toUri(),
|
||||
)
|
||||
)
|
||||
context.startActivity(intent)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.ReorderableList
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions
|
||||
@@ -22,10 +21,9 @@ internal fun VirtualButtonOrderPage(
|
||||
contentPadding: PaddingValues,
|
||||
scrollBehavior: ScrollBehavior,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val appSettings = Storage.appSettings
|
||||
|
||||
val appSettings = remember(context) { AppSettings(context) }
|
||||
val scrcpyOptions = remember(context) { ScrcpyOptions(context) }
|
||||
val context = LocalContext.current
|
||||
|
||||
var previewVirtualButtonShowText by appSettings.previewVirtualButtonShowText.asMutableState()
|
||||
var virtualButtonsLayout by appSettings.virtualButtonsLayout.asMutableState()
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import io.github.miuzarte.scrcpyforandroid.constants.AppPreferenceKeys
|
||||
import io.github.miuzarte.scrcpyforandroid.constants.Defaults
|
||||
import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget
|
||||
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
|
||||
|
||||
internal fun loadQuickDevices(context: Context): List<DeviceShortcut> {
|
||||
@@ -63,70 +62,3 @@ internal fun saveQuickDevices(context: Context, quickDevices: List<DeviceShortcu
|
||||
putString(AppPreferenceKeys.QUICK_DEVICES, raw)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun parseQuickTarget(raw: String): ConnectionTarget? {
|
||||
val value = raw.trim()
|
||||
if (value.isEmpty()) return null
|
||||
val host = value.substringBefore(':').trim()
|
||||
if (host.isEmpty()) return null
|
||||
val port = value.substringAfter(':', Defaults.ADB_PORT.toString()).trim().toIntOrNull()
|
||||
?: Defaults.ADB_PORT
|
||||
return ConnectionTarget(host, port)
|
||||
}
|
||||
|
||||
internal fun upsertQuickDevice(
|
||||
context: Context,
|
||||
quickDevices: MutableList<DeviceShortcut>,
|
||||
host: String,
|
||||
port: Int,
|
||||
online: Boolean,
|
||||
) {
|
||||
val idx = quickDevices.indexOfFirst { it.id == "$host:$port" }
|
||||
val existingName = if (idx >= 0) quickDevices[idx].name else ""
|
||||
val item = DeviceShortcut(
|
||||
name = existingName,
|
||||
host = host,
|
||||
port = port,
|
||||
online = online,
|
||||
)
|
||||
if (idx >= 0) quickDevices[idx] = item else quickDevices.add(0, item)
|
||||
saveQuickDevices(context, quickDevices)
|
||||
}
|
||||
|
||||
internal fun updateQuickDeviceNameIfEmpty(
|
||||
context: Context,
|
||||
quickDevices: MutableList<DeviceShortcut>,
|
||||
host: String,
|
||||
port: Int,
|
||||
fallbackName: String,
|
||||
) {
|
||||
val idx = quickDevices.indexOfFirst { it.host == host && it.port == port }
|
||||
if (idx >= 0 && quickDevices[idx].name.isBlank()) {
|
||||
quickDevices[idx] = quickDevices[idx].copy(name = fallbackName)
|
||||
saveQuickDevices(context, quickDevices)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun replaceQuickDevicePort(
|
||||
context: Context,
|
||||
quickDevices: MutableList<DeviceShortcut>,
|
||||
host: String,
|
||||
oldPort: Int,
|
||||
newPort: Int,
|
||||
online: Boolean,
|
||||
) {
|
||||
val idx = quickDevices.indexOfFirst { it.host == host && it.port == oldPort }
|
||||
if (idx < 0) return
|
||||
|
||||
val old = quickDevices[idx]
|
||||
val updated = old.copy(
|
||||
port = newPort,
|
||||
online = online,
|
||||
)
|
||||
|
||||
quickDevices[idx] = updated
|
||||
val dedup = quickDevices.distinctBy { it.id }
|
||||
quickDevices.clear()
|
||||
quickDevices.addAll(dedup)
|
||||
saveQuickDevices(context, quickDevices)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package io.github.miuzarte.scrcpyforandroid.storage
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
|
||||
class AdbClientData(context: Context) : Settings(context, "AdbClient") {
|
||||
companion object {
|
||||
val RSA_PRIVATE_KEY = Pair(
|
||||
stringPreferencesKey("rsa_private_key"),
|
||||
"",
|
||||
)
|
||||
val RSA_PUBLIC_KEY_X509 = Pair(
|
||||
stringPreferencesKey("rsa_public_key_x509"),
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
val rsaPrivateKey by setting(RSA_PRIVATE_KEY)
|
||||
val rsaPublicKeyX509 by setting(RSA_PUBLIC_KEY_X509)
|
||||
}
|
||||
@@ -87,30 +87,6 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
|
||||
val adbAutoReconnectPairedDevice by setting(ADB_AUTO_RECONNECT_PAIRED_DEVICE)
|
||||
val adbMdnsLanDiscovery by setting(ADB_MDNS_LAN_DISCOVERY)
|
||||
|
||||
override suspend fun toMap(): Map<String, Any> = mapOf(
|
||||
// Theme Settings
|
||||
THEME_BASE_INDEX.name to themeBaseIndex.get(),
|
||||
MONET.name to monet.get(),
|
||||
|
||||
// Scrcpy Settings
|
||||
FULLSCREEN_DEBUG_INFO.name to fullscreenDebugInfo.get(),
|
||||
SHOW_FULLSCREEN_VIRTUAL_BUTTONS.name to showFullscreenVirtualButtons.get(),
|
||||
KEEP_SCREEN_ON_WHEN_STREAMING.name to keepScreenOnWhenStreaming.get(),
|
||||
DEVICE_PREVIEW_CARD_HEIGHT_DP.name to devicePreviewCardHeightDp.get(),
|
||||
PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT.name to previewVirtualButtonShowText.get(),
|
||||
VIRTUAL_BUTTONS_LAYOUT.name to virtualButtonsLayout.get(),
|
||||
|
||||
// Scrcpy Server Settings
|
||||
CUSTOM_SERVER_URI.name to customServerUri.get(),
|
||||
SERVER_REMOTE_PATH.name to serverRemotePath.get(),
|
||||
|
||||
// ADB Settings
|
||||
ADB_KEY_NAME.name to adbKeyName.get(),
|
||||
ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN.name to adbPairingAutoDiscoverOnDialogOpen.get(),
|
||||
ADB_AUTO_RECONNECT_PAIRED_DEVICE.name to adbAutoReconnectPairedDevice.get(),
|
||||
ADB_MDNS_LAN_DISCOVERY.name to adbMdnsLanDiscovery.get()
|
||||
)
|
||||
|
||||
// TODO?
|
||||
override fun validate(): Boolean = true
|
||||
// fun validate(): Boolean = true
|
||||
}
|
||||
|
||||
@@ -3,69 +3,80 @@ package io.github.miuzarte.scrcpyforandroid.storage
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.core.content.edit
|
||||
import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults
|
||||
import io.github.miuzarte.scrcpyforandroid.constants.AppPreferenceKeys
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private const val TAG = "PreferenceMigration"
|
||||
private const val MIGRATION_COMPLETED_KEY = "migration_completed"
|
||||
|
||||
/**
|
||||
* 从旧的 SharedPreferences 迁移到新的 DataStore
|
||||
*/
|
||||
class PreferenceMigration(private val context: Context) {
|
||||
|
||||
class PreferenceMigration(private val appContext: Context) {
|
||||
private val appSharedPrefs: SharedPreferences by lazy {
|
||||
appContext.getSharedPreferences(AppPreferenceKeys.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
private val sharedPrefs: SharedPreferences by lazy {
|
||||
context.getSharedPreferences(AppPreferenceKeys.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
appContext.getSharedPreferences("nativecore_adb_rsa", Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已完成迁移
|
||||
* 检查是否需要迁移(SharedPreferences 是否包含数据)
|
||||
*/
|
||||
@Deprecated("做成设置内入口手动调用,这个没必要")
|
||||
suspend fun isMigrationCompleted(): Boolean = withContext(Dispatchers.IO) {
|
||||
sharedPrefs.getBoolean(MIGRATION_COMPLETED_KEY, false)
|
||||
suspend fun needsMigration(): Boolean = withContext(Dispatchers.IO) {
|
||||
appSharedPrefs.all.isNotEmpty() || sharedPrefs.all.isNotEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行完整迁移
|
||||
* @return 迁移是否成功
|
||||
*/
|
||||
suspend fun migrate(): Boolean = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (isMigrationCompleted()) {
|
||||
Log.i(TAG, "Migration already completed, skipping")
|
||||
return@withContext true
|
||||
}
|
||||
|
||||
Log.i(TAG, "Starting migration from SharedPreferences to DataStore")
|
||||
|
||||
// 迁移 AppSettings
|
||||
migrateAppSettings()
|
||||
|
||||
// 迁移 ScrcpyOptions
|
||||
migrateScrcpyOptions()
|
||||
|
||||
// 迁移 QuickDevices
|
||||
migrateQuickDevices()
|
||||
|
||||
// 标记迁移完成
|
||||
markMigrationCompleted()
|
||||
|
||||
Log.i(TAG, "Migration completed successfully")
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Migration failed", e)
|
||||
false
|
||||
suspend fun migrate(
|
||||
clearSharedPrefs: Boolean = false,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
if (!needsMigration()) {
|
||||
Log.i(TAG, "No data to migrate, skipping")
|
||||
return@withContext
|
||||
} else {
|
||||
val appList = appSharedPrefs.all.entries.joinToString { (k, v) -> "\"$k\" to $v" }
|
||||
val adbList = sharedPrefs.all.entries.joinToString { (k, v) -> "\"$k\" to $v" }
|
||||
Log.d(TAG, "Migrating appSharedPrefs ($appList)")
|
||||
}
|
||||
|
||||
Log.i(TAG, "Starting migration from SharedPreferences to DataStore")
|
||||
|
||||
// 迁移 AppSettings
|
||||
migrateAppSettings()
|
||||
|
||||
// 迁移 ScrcpyOptions
|
||||
migrateScrcpyOptions()
|
||||
|
||||
// 迁移 QuickDevices
|
||||
migrateQuickDevices()
|
||||
|
||||
// 迁移 ADB 密钥
|
||||
migrateAdbClientData()
|
||||
|
||||
// 清空 SharedPreferences
|
||||
if (clearSharedPrefs) {
|
||||
appSharedPrefs.edit { clear() }
|
||||
sharedPrefs.edit { clear() }
|
||||
Log.d(TAG, "SharedPreferences cleared")
|
||||
}
|
||||
|
||||
Log.i(
|
||||
TAG, "Migration completed successfully" +
|
||||
" and SharedPreferences ${if (clearSharedPrefs) "" else "is not "}cleared"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移应用设置
|
||||
*/
|
||||
private suspend fun migrateAppSettings() {
|
||||
val appSettings = AppSettings(context)
|
||||
val appSettings = Storage.appSettings
|
||||
|
||||
// Theme Settings
|
||||
migrateInt(
|
||||
@@ -152,7 +163,7 @@ class PreferenceMigration(private val context: Context) {
|
||||
* 迁移 Scrcpy 选项
|
||||
*/
|
||||
private suspend fun migrateScrcpyOptions() {
|
||||
val scrcpyOptions = ScrcpyOptions(context)
|
||||
val scrcpyOptions = Storage.scrcpyOptions
|
||||
|
||||
// Audio & Video Codecs
|
||||
migrateString(
|
||||
@@ -383,10 +394,10 @@ class PreferenceMigration(private val context: Context) {
|
||||
* 迁移快速设备列表
|
||||
*/
|
||||
private suspend fun migrateQuickDevices() {
|
||||
val quickDevices = QuickDevices(context)
|
||||
val quickDevices = Storage.quickDevices
|
||||
|
||||
// Migrate quick devices list
|
||||
val quickDevicesRaw = sharedPrefs.getString(
|
||||
val quickDevicesRaw = appSharedPrefs.getString(
|
||||
AppPreferenceKeys.QUICK_DEVICES,
|
||||
""
|
||||
).orEmpty()
|
||||
@@ -403,12 +414,21 @@ class PreferenceMigration(private val context: Context) {
|
||||
|
||||
Log.d(TAG, "QuickDevices migration completed")
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 标记迁移完成
|
||||
* 迁移 ADB 客户端数据(RSA 密钥)
|
||||
*/
|
||||
private fun markMigrationCompleted() {
|
||||
sharedPrefs.edit().putBoolean(MIGRATION_COMPLETED_KEY, true).apply()
|
||||
private suspend fun migrateAdbClientData() {
|
||||
val adbClientData = Storage.adbClientData
|
||||
|
||||
// 迁移 RSA 私钥
|
||||
val privKey = sharedPrefs.getString("priv", null)
|
||||
if (privKey != null) {
|
||||
adbClientData.rsaPrivateKey.set(privKey)
|
||||
Log.d(TAG, "ADB RSA private key migrated")
|
||||
}
|
||||
|
||||
Log.d(TAG, "AdbClientData migration completed")
|
||||
}
|
||||
|
||||
// Helper methods for different data types
|
||||
@@ -418,7 +438,7 @@ class PreferenceMigration(private val context: Context) {
|
||||
defaultValue: String,
|
||||
settingProperty: Settings.SettingProperty<String>
|
||||
) {
|
||||
val value = sharedPrefs.getString(key, defaultValue)
|
||||
val value = appSharedPrefs.getString(key, defaultValue)
|
||||
.orEmpty()
|
||||
.ifBlank { defaultValue }
|
||||
settingProperty.set(value)
|
||||
@@ -429,7 +449,7 @@ class PreferenceMigration(private val context: Context) {
|
||||
defaultValue: String,
|
||||
action: suspend (String) -> Unit
|
||||
) {
|
||||
val value = sharedPrefs.getString(key, defaultValue)
|
||||
val value = appSharedPrefs.getString(key, defaultValue)
|
||||
.orEmpty()
|
||||
.ifBlank { defaultValue }
|
||||
action(value)
|
||||
@@ -440,7 +460,7 @@ class PreferenceMigration(private val context: Context) {
|
||||
defaultValue: Int,
|
||||
settingProperty: Settings.SettingProperty<Int>
|
||||
) {
|
||||
val value = sharedPrefs.getInt(key, defaultValue)
|
||||
val value = appSharedPrefs.getInt(key, defaultValue)
|
||||
settingProperty.set(value)
|
||||
}
|
||||
|
||||
@@ -449,7 +469,7 @@ class PreferenceMigration(private val context: Context) {
|
||||
defaultValue: Boolean,
|
||||
settingProperty: Settings.SettingProperty<Boolean>
|
||||
) {
|
||||
val value = sharedPrefs.getBoolean(key, defaultValue)
|
||||
val value = appSharedPrefs.getBoolean(key, defaultValue)
|
||||
settingProperty.set(value)
|
||||
}
|
||||
|
||||
@@ -458,7 +478,7 @@ class PreferenceMigration(private val context: Context) {
|
||||
defaultValue: Boolean,
|
||||
action: suspend (Boolean) -> Unit
|
||||
) {
|
||||
val value = sharedPrefs.getBoolean(key, defaultValue)
|
||||
val value = appSharedPrefs.getBoolean(key, defaultValue)
|
||||
action(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,6 @@ package io.github.miuzarte.scrcpyforandroid.storage
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class QuickDevices(context: Context) : Settings(context, "QuickDevices") {
|
||||
companion object {
|
||||
@@ -20,145 +17,4 @@ class QuickDevices(context: Context) : Settings(context, "QuickDevices") {
|
||||
|
||||
val quickDevicesList by setting(QUICK_DEVICES_LIST)
|
||||
val quickConnectInput by setting(QUICK_CONNECT_INPUT)
|
||||
|
||||
override suspend fun toMap(): Map<String, Any> = mapOf(
|
||||
QUICK_DEVICES_LIST.name to quickDevicesList.get(),
|
||||
QUICK_CONNECT_INPUT.name to quickConnectInput.get(),
|
||||
)
|
||||
|
||||
override fun validate(): Boolean = true
|
||||
|
||||
suspend fun getQuickDevices(): List<DeviceShortcut> {
|
||||
val raw = quickDevicesList.get()
|
||||
if (raw.isBlank()) return emptyList()
|
||||
|
||||
val result = mutableListOf<DeviceShortcut>()
|
||||
raw.lineSequence().forEach { line ->
|
||||
DeviceShortcut.unmarshalFrom(line)?.let { result.add(it) }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun setQuickDevices(list: List<DeviceShortcut>) =
|
||||
quickDevicesList.set(list.joinToString("\n") { it.marshalToString() })
|
||||
|
||||
fun observeQuickDevices(): Flow<List<DeviceShortcut>> = quickDevicesList.observe()
|
||||
.map { raw ->
|
||||
if (raw.isBlank()) return@map emptyList()
|
||||
|
||||
val result = mutableListOf<DeviceShortcut>()
|
||||
raw.lineSequence().forEach { line ->
|
||||
DeviceShortcut.unmarshalFrom(line)?.let { result.add(it) }
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入或更新快速设备
|
||||
*/
|
||||
suspend fun upsertQuickDevice(
|
||||
host: String,
|
||||
port: Int,
|
||||
online: Boolean,
|
||||
index: Int? = 0,
|
||||
) {
|
||||
val quickDevices = getQuickDevices().toMutableList()
|
||||
val id = "$host:$port"
|
||||
val idx = quickDevices.indexOfFirst { it.id == id }
|
||||
val existingName = if (idx >= 0) quickDevices[idx].name else ""
|
||||
val item = DeviceShortcut(
|
||||
name = existingName,
|
||||
host = host,
|
||||
port = port,
|
||||
online = online,
|
||||
)
|
||||
if (idx >= 0) {
|
||||
quickDevices[idx] = item
|
||||
} else {
|
||||
if (index != null)
|
||||
quickDevices.add(index, item)
|
||||
else quickDevices.add(item)
|
||||
}
|
||||
setQuickDevices(quickDevices)
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果设备名称为空,则更新为备用名称
|
||||
*/
|
||||
suspend fun updateQuickDeviceNameIfEmpty(
|
||||
host: String,
|
||||
port: Int,
|
||||
fallbackName: String,
|
||||
) {
|
||||
val quickDevices = getQuickDevices().toMutableList()
|
||||
val idx = quickDevices.indexOfFirst { it.host == host && it.port == port }
|
||||
if (idx >= 0 && quickDevices[idx].name.isBlank()) {
|
||||
quickDevices[idx] = quickDevices[idx].copy(name = fallbackName)
|
||||
setQuickDevices(quickDevices)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换快速设备的端口
|
||||
*/
|
||||
suspend fun replaceQuickDevicePort(
|
||||
host: String,
|
||||
oldPort: Int,
|
||||
newPort: Int,
|
||||
online: Boolean,
|
||||
) {
|
||||
val quickDevices = getQuickDevices().toMutableList()
|
||||
val idx = quickDevices.indexOfFirst { it.host == host && it.port == oldPort }
|
||||
if (idx < 0) return
|
||||
|
||||
val old = quickDevices[idx]
|
||||
val updated = old.copy(
|
||||
port = newPort,
|
||||
online = online,
|
||||
)
|
||||
|
||||
quickDevices[idx] = updated
|
||||
val dedup = quickDevices.distinctBy { it.id }
|
||||
setQuickDevices(dedup)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除快速设备
|
||||
*/
|
||||
suspend fun removeQuickDevice(id: String) {
|
||||
val quickDevices = getQuickDevices().toMutableList()
|
||||
quickDevices.removeAll { it.id == id }
|
||||
setQuickDevices(quickDevices)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新设备在线状态
|
||||
*/
|
||||
suspend fun updateDeviceOnlineStatus(host: String, port: Int, online: Boolean) {
|
||||
val quickDevices = getQuickDevices().toMutableList()
|
||||
val idx = quickDevices.indexOfFirst { it.host == host && it.port == port }
|
||||
if (idx >= 0) {
|
||||
quickDevices[idx] = quickDevices[idx].copy(online = online)
|
||||
setQuickDevices(quickDevices)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新设备名称
|
||||
*/
|
||||
suspend fun updateDeviceName(id: String, name: String) {
|
||||
val quickDevices = getQuickDevices().toMutableList()
|
||||
val idx = quickDevices.indexOfFirst { it.id == id }
|
||||
if (idx >= 0) {
|
||||
quickDevices[idx] = quickDevices[idx].copy(name = name)
|
||||
setQuickDevices(quickDevices)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有快速设备
|
||||
*/
|
||||
suspend fun clearQuickDevices() {
|
||||
quickDevicesList.set("")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,61 +278,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
||||
val vdDestroyContent by setting(VD_DESTROY_CONTENT)
|
||||
val vdSystemDecorations by setting(VD_SYSTEM_DECORATIONS)
|
||||
|
||||
override suspend fun toMap(): Map<String, Any> = mapOf(
|
||||
CROP.name to crop.get(),
|
||||
RECORD_FILENAME.name to recordFilename.get(),
|
||||
VIDEO_CODEC_OPTIONS.name to videoCodecOptions.get(),
|
||||
AUDIO_CODEC_OPTIONS.name to audioCodecOptions.get(),
|
||||
VIDEO_ENCODER.name to videoEncoder.get(),
|
||||
AUDIO_ENCODER.name to audioEncoder.get(),
|
||||
CAMERA_ID.name to cameraId.get(),
|
||||
CAMERA_SIZE.name to cameraSize.get(),
|
||||
CAMERA_AR.name to cameraAr.get(),
|
||||
CAMERA_FPS.name to cameraFps.get(),
|
||||
LOG_LEVEL.name to logLevel.get(),
|
||||
VIDEO_CODEC.name to videoCodec.get(),
|
||||
AUDIO_CODEC.name to audioCodec.get(),
|
||||
VIDEO_SOURCE.name to videoSource.get(),
|
||||
AUDIO_SOURCE.name to audioSource.get(),
|
||||
RECORD_FORMAT.name to recordFormat.get(),
|
||||
CAMERA_FACING.name to cameraFacing.get(),
|
||||
MAX_SIZE.name to maxSize.get(),
|
||||
VIDEO_BIT_RATE.name to videoBitRate.get(),
|
||||
AUDIO_BIT_RATE.name to audioBitRate.get(),
|
||||
MAX_FPS.name to maxFps.get(),
|
||||
ANGLE.name to angle.get(),
|
||||
CAPTURE_ORIENTATION.name to captureOrientation.get(),
|
||||
CAPTURE_ORIENTATION_LOCK.name to captureOrientationLock.get(),
|
||||
DISPLAY_ORIENTATION.name to displayOrientation.get(),
|
||||
RECORD_ORIENTATION.name to recordOrientation.get(),
|
||||
DISPLAY_IME_POLICY.name to displayImePolicy.get(),
|
||||
DISPLAY_ID.name to displayId.get(),
|
||||
SCREEN_OFF_TIMEOUT.name to screenOffTimeout.get(),
|
||||
SHOW_TOUCHES.name to showTouches.get(),
|
||||
FULLSCREEN.name to fullscreen.get(),
|
||||
CONTROL.name to control.get(),
|
||||
VIDEO_PLAYBACK.name to videoPlayback.get(),
|
||||
AUDIO_PLAYBACK.name to audioPlayback.get(),
|
||||
TURN_SCREEN_OFF.name to turnScreenOff.get(),
|
||||
STAY_AWAKE.name to stayAwake.get(),
|
||||
DISABLE_SCREENSAVER.name to disableScreensaver.get(),
|
||||
POWER_OFF_ON_CLOSE.name to powerOffOnClose.get(),
|
||||
CLEANUP.name to cleanup.get(),
|
||||
POWER_ON.name to powerOn.get(),
|
||||
VIDEO.name to video.get(),
|
||||
AUDIO.name to audio.get(),
|
||||
REQUIRE_AUDIO.name to requireAudio.get(),
|
||||
KILL_ADB_ON_CLOSE.name to killAdbOnClose.get(),
|
||||
CAMERA_HIGH_SPEED.name to cameraHighSpeed.get(),
|
||||
LIST.name to list.get(),
|
||||
AUDIO_DUP.name to audioDup.get(),
|
||||
NEW_DISPLAY.name to newDisplay.get(),
|
||||
START_APP.name to startApp.get(),
|
||||
VD_DESTROY_CONTENT.name to vdDestroyContent.get(),
|
||||
VD_SYSTEM_DECORATIONS.name to vdSystemDecorations.get()
|
||||
)
|
||||
|
||||
override fun validate(): Boolean = runBlocking {
|
||||
fun validate(): Boolean = runBlocking {
|
||||
runCatching {
|
||||
toClientOptions().validate()
|
||||
true
|
||||
|
||||
@@ -42,11 +42,16 @@ abstract class Settings(
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置项委托类,自动提供 get/set/observe/observeAsState/asMutableState 方法
|
||||
* 设置项委托类,自动提供 get/set/observe/asState/asMutableState 方法
|
||||
*/
|
||||
inner class SettingProperty<T>(
|
||||
private val pair: Pair<T>
|
||||
val pair: Pair<T>
|
||||
) {
|
||||
// 创建时注册自身
|
||||
init {
|
||||
registerProperty(pair.name, this)
|
||||
}
|
||||
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): SettingProperty<T> = this
|
||||
|
||||
suspend fun get(): T = getValue(pair)
|
||||
@@ -62,6 +67,15 @@ abstract class Settings(
|
||||
fun asMutableState(): MutableState<T> = this@Settings.asMutableState(pair)
|
||||
}
|
||||
|
||||
// 注册表, 用于遍历
|
||||
private val propertyRegistry = mutableMapOf<String, SettingProperty<*>>()
|
||||
|
||||
private fun <T> registerProperty(name: String, property: SettingProperty<T>) {
|
||||
propertyRegistry[name] = property
|
||||
}
|
||||
|
||||
protected fun getAllProperties(): Map<String, SettingProperty<*>> = propertyRegistry.toMap()
|
||||
|
||||
// 为 Context 添加扩展委托属性,确保 DataStore 单例
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
|
||||
name = this.name,
|
||||
@@ -69,21 +83,16 @@ abstract class Settings(
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
)
|
||||
|
||||
abstract suspend fun toMap(): Map<String, Any>
|
||||
abstract fun validate(): Boolean
|
||||
|
||||
// 对外暴露的 DataStore 实例
|
||||
protected val dataStore: DataStore<Preferences> = context.dataStore
|
||||
|
||||
protected fun <T> setting(pair: Pair<T>): SettingProperty<T> =
|
||||
SettingProperty(pair)
|
||||
protected fun <T> setting(pair: Pair<T>) = SettingProperty(pair)
|
||||
|
||||
protected suspend fun <T> getValue(pair: Pair<T>): T =
|
||||
dataStore.data.first()[pair.key] ?: pair.defaultValue
|
||||
|
||||
protected suspend fun <T> setValue(pair: Pair<T>, value: T) {
|
||||
protected suspend fun <T> setValue(pair: Pair<T>, value: T) =
|
||||
dataStore.edit { preferences -> preferences[pair.key] = value }
|
||||
}
|
||||
|
||||
protected fun <T> observe(pair: Pair<T>): Flow<T> =
|
||||
dataStore.data.map { preferences -> preferences[pair.key] ?: pair.defaultValue }
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package io.github.miuzarte.scrcpyforandroid.storage
|
||||
|
||||
import android.content.Context
|
||||
|
||||
// settings singleton
|
||||
object Storage {
|
||||
private lateinit var appContext: Context
|
||||
|
||||
fun init(context: Context) {
|
||||
appContext = context.applicationContext
|
||||
}
|
||||
|
||||
val appSettings: AppSettings by lazy { AppSettings(appContext) }
|
||||
val quickDevices: QuickDevices by lazy { QuickDevices(appContext) }
|
||||
val scrcpyOptions: ScrcpyOptions by lazy { ScrcpyOptions(appContext) }
|
||||
val adbClientData: AdbClientData by lazy { AdbClientData(appContext) }
|
||||
}
|
||||
@@ -48,6 +48,9 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
@@ -71,8 +74,7 @@ import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
||||
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics
|
||||
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
|
||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlide
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -132,10 +134,9 @@ internal fun StatusCard(
|
||||
busyLabel: String?,
|
||||
connectedDeviceLabel: String,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val appSettings = Storage.appSettings
|
||||
|
||||
val appSettings = remember(context) { AppSettings(context) }
|
||||
val scrcpyOptions = remember(context) { ScrcpyOptions(context) }
|
||||
val context = LocalContext.current
|
||||
|
||||
val themeBaseIndex by appSettings.themeBaseIndex.asState()
|
||||
|
||||
@@ -381,10 +382,9 @@ internal fun ConfigPanel(
|
||||
onStop: () -> Unit,
|
||||
sessionStarted: Boolean,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scrcpyOptions = Storage.scrcpyOptions
|
||||
|
||||
// val appSettings = remember(context) { AppSettings(context) }
|
||||
val scrcpyOptions = remember(context) { ScrcpyOptions(context) }
|
||||
val context = LocalContext.current
|
||||
|
||||
SectionSmallTitle("Scrcpy")
|
||||
Card {
|
||||
@@ -1221,12 +1221,16 @@ internal fun DeviceTile(
|
||||
@Composable
|
||||
internal fun QuickConnectCard(
|
||||
input: String,
|
||||
onInputChange: (String) -> Unit,
|
||||
onValueChange: (String) -> Unit,
|
||||
onConnect: () -> Unit,
|
||||
onAddDevice: () -> Unit,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val focusManager = LocalFocusManager.current
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
var tempText by remember(input) { mutableStateOf(input) }
|
||||
var tempFocusState by remember(input) { mutableStateOf(false) }
|
||||
|
||||
Card(
|
||||
colors = CardDefaults.defaultColors(color = MiuixTheme.colorScheme.primaryContainer),
|
||||
@@ -1256,10 +1260,8 @@ internal fun QuickConnectCard(
|
||||
)
|
||||
}
|
||||
TextField(
|
||||
value = input,
|
||||
onValueChange = {
|
||||
if (enabled) onInputChange(it)
|
||||
},
|
||||
value = tempText,
|
||||
onValueChange = { tempText = it },
|
||||
label = "IP:PORT",
|
||||
enabled = enabled,
|
||||
useLabelAsPlaceholder = true,
|
||||
@@ -1267,7 +1269,15 @@ internal fun QuickConnectCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = UiSpacing.CardContent)
|
||||
.padding(bottom = UiSpacing.SectionTitleLeadingGap),
|
||||
.padding(bottom = UiSpacing.SectionTitleLeadingGap)
|
||||
.onFocusChanged { focusState ->
|
||||
// 失去焦点时回调
|
||||
if (!focusState.isFocused && tempFocusState) {
|
||||
onValueChange(tempText)
|
||||
}
|
||||
tempFocusState = focusState.isFocused
|
||||
}
|
||||
.focusRequester(focusRequester),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user