refactor: navigation and haptic move into composition locals

This commit is contained in:
Miuzarte
2026-04-10 17:23:11 +08:00
parent 1a270ae2dd
commit c4003826ff
15 changed files with 189 additions and 145 deletions

View File

@@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
@@ -29,6 +30,10 @@ data class AppHaptics(
val virtualKey: () -> Unit, val virtualKey: () -> Unit,
) )
val LocalAppHaptics = staticCompositionLocalOf<AppHaptics> {
error("No AppHaptics provided")
}
@Composable @Composable
fun rememberAppHaptics(): AppHaptics { fun rememberAppHaptics(): AppHaptics {
val hapticFeedback = LocalHapticFeedback.current val hapticFeedback = LocalHapticFeedback.current

View File

@@ -74,9 +74,8 @@ import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
import androidx.compose.ui.graphics.BlendMode as ComposeBlendMode import androidx.compose.ui.graphics.BlendMode as ComposeBlendMode
@Composable @Composable
internal fun AboutScreen( internal fun AboutScreen() {
onBack: () -> Unit, val navigator = LocalRootNavigator.current
) {
val topAppBarScrollBehavior = MiuixScrollBehavior() val topAppBarScrollBehavior = MiuixScrollBehavior()
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
var logoHeightPx by remember { mutableIntStateOf(0) } var logoHeightPx by remember { mutableIntStateOf(0) }
@@ -102,9 +101,9 @@ internal fun AboutScreen(
titleColor = colorScheme.onSurface.copy(alpha = scrollProgress), titleColor = colorScheme.onSurface.copy(alpha = scrollProgress),
defaultWindowInsetsPadding = false, defaultWindowInsetsPadding = false,
navigationIcon = { navigationIcon = {
IconButton(onClick = onBack) { IconButton(onClick = navigator.pop) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack, Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = "返回", contentDescription = "返回",
) )
} }

View File

@@ -2,6 +2,7 @@ package io.github.miuzarte.scrcpyforandroid.pages
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.pm.ActivityInfo
import android.util.Log import android.util.Log
import android.view.WindowManager import android.view.WindowManager
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -28,7 +29,7 @@ import androidx.compose.ui.text.font.FontWeight
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade
import io.github.miuzarte.scrcpyforandroid.constants.Defaults import io.github.miuzarte.scrcpyforandroid.constants.Defaults
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics import io.github.miuzarte.scrcpyforandroid.haptics.LocalAppHaptics
import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcuts import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcuts
@@ -37,7 +38,7 @@ import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
import io.github.miuzarte.scrcpyforandroid.services.EventLogger import io.github.miuzarte.scrcpyforandroid.services.EventLogger
import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent
import io.github.miuzarte.scrcpyforandroid.services.SnackbarController import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController
import io.github.miuzarte.scrcpyforandroid.services.fetchConnectedDeviceInfo import io.github.miuzarte.scrcpyforandroid.services.fetchConnectedDeviceInfo
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
@@ -90,13 +91,10 @@ fun DeviceTabScreen(
nativeCore: NativeCoreFacade, nativeCore: NativeCoreFacade,
adbService: NativeAdbService, adbService: NativeAdbService,
scrcpy: Scrcpy, scrcpy: Scrcpy,
snackbar: SnackbarController,
scrollBehavior: ScrollBehavior, scrollBehavior: ScrollBehavior,
onOpenVirtualButtonOrder: () -> Unit,
onOpenReorderDevices: () -> Unit, onOpenReorderDevices: () -> Unit,
onOpenAdvancedPage: () -> Unit,
onOpenFullscreenPage: () -> Unit,
) { ) {
val navigator = LocalRootNavigator.current
var showThreePointMenu by rememberSaveable { mutableStateOf(false) } var showThreePointMenu by rememberSaveable { mutableStateOf(false) }
Scaffold( Scaffold(
@@ -121,7 +119,7 @@ fun DeviceTabScreen(
showThreePointMenu = false showThreePointMenu = false
}, },
onOpenVirtualButtonOrder = { onOpenVirtualButtonOrder = {
onOpenVirtualButtonOrder() navigator.push(RootScreen.VirtualButtonOrder)
showThreePointMenu = false showThreePointMenu = false
}, },
canClearLogs = EventLogger.hasLogs(), canClearLogs = EventLogger.hasLogs(),
@@ -140,10 +138,7 @@ fun DeviceTabScreen(
nativeCore = nativeCore, nativeCore = nativeCore,
adbService = adbService, adbService = adbService,
scrcpy = scrcpy, scrcpy = scrcpy,
snackbar = snackbar,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
onOpenAdvancedPage = onOpenAdvancedPage,
onOpenFullscreenPage = onOpenFullscreenPage,
) )
} }
} }
@@ -154,17 +149,18 @@ fun DeviceTabPage(
nativeCore: NativeCoreFacade, nativeCore: NativeCoreFacade,
adbService: NativeAdbService, adbService: NativeAdbService,
scrcpy: Scrcpy, scrcpy: Scrcpy,
snackbar: SnackbarController,
scrollBehavior: ScrollBehavior, scrollBehavior: ScrollBehavior,
onOpenAdvancedPage: () -> Unit,
onOpenFullscreenPage: () -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val taskScope = remember { CoroutineScope(Dispatchers.IO + SupervisorJob()) } val taskScope = remember { CoroutineScope(Dispatchers.IO + SupervisorJob()) }
val haptics = rememberAppHaptics()
val activity = remember(context) { context as? Activity } val activity = remember(context) { context as? Activity }
val haptics = LocalAppHaptics.current
val navigator = LocalRootNavigator.current
val fullscreenNavigationState = LocalFullscreenNavigationState.current
val snackbar = LocalSnackbarController.current
val asBundleShared by appSettings.bundleState.collectAsState() val asBundleShared by appSettings.bundleState.collectAsState()
val asBundleSharedLatest by rememberUpdatedState(asBundleShared) val asBundleSharedLatest by rememberUpdatedState(asBundleShared)
var asBundle by rememberSaveable(asBundleShared) { mutableStateOf(asBundleShared) } var asBundle by rememberSaveable(asBundleShared) { mutableStateOf(asBundleShared) }
@@ -869,12 +865,11 @@ fun DeviceTabPage(
SectionSmallTitle("Scrcpy") SectionSmallTitle("Scrcpy")
ConfigPanel( ConfigPanel(
busy = busy, busy = busy,
snackbar = snackbar,
audioForwardingSupported = audioForwardingSupported, audioForwardingSupported = audioForwardingSupported,
cameraMirroringSupported = cameraMirroringSupported, cameraMirroringSupported = cameraMirroringSupported,
adbConnecting = adbConnecting, adbConnecting = adbConnecting,
isQuickConnected = isQuickConnected, isQuickConnected = isQuickConnected,
onOpenAdvanced = onOpenAdvancedPage, onOpenAdvanced = { navigator.push(RootScreen.Advanced) },
onStartStopHaptic = { haptics.contextClick() }, onStartStopHaptic = { haptics.contextClick() },
onStart = { onStart = {
runBusy("启动 scrcpy") { runBusy("启动 scrcpy") {
@@ -949,8 +944,16 @@ fun DeviceTabPage(
previewControlsVisible = !previewControlsVisible previewControlsVisible = !previewControlsVisible
}, },
onOpenFullscreen = { onOpenFullscreen = {
if (sessionInfo != null) { val currentSession = sessionInfo
onOpenFullscreenPage() if (currentSession != null) {
fullscreenNavigationState.setOrientation(
if (currentSession.width >= currentSession.height) {
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
} else {
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
)
navigator.push(RootScreen.Fullscreen)
} }
}, },
) )

View File

@@ -25,7 +25,7 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics import io.github.miuzarte.scrcpyforandroid.haptics.LocalAppHaptics
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
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
@@ -42,16 +42,16 @@ import top.yukonga.miuix.kmp.basic.Scaffold
@Composable @Composable
fun FullscreenControlScreen( fun FullscreenControlScreen(
onBack: () -> Unit,
scrcpy: Scrcpy, scrcpy: Scrcpy,
nativeCore: NativeCoreFacade, nativeCore: NativeCoreFacade,
onVideoSizeChanged: (width: Int, height: Int) -> Unit, onVideoSizeChanged: (width: Int, height: Int) -> Unit,
) { ) {
BackHandler(enabled = true, onBack = onBack) val navigator = LocalRootNavigator.current
BackHandler(enabled = true, onBack = navigator.pop)
val context = LocalContext.current val context = LocalContext.current
val haptics = rememberAppHaptics() val haptics = LocalAppHaptics.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) } val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
@@ -154,7 +154,7 @@ fun FullscreenControlScreen(
FullscreenControlScreen( FullscreenControlScreen(
session = session, session = session,
nativeCore = nativeCore, nativeCore = nativeCore,
onDismiss = onBack, onDismiss = navigator.pop,
showDebugInfo = fullscreenDebugInfo, showDebugInfo = fullscreenDebugInfo,
currentFps = currentFps, currentFps = currentFps,
enableBackHandler = false, enableBackHandler = false,

View File

@@ -0,0 +1,11 @@
package io.github.miuzarte.scrcpyforandroid.pages
import androidx.compose.runtime.staticCompositionLocalOf
class ServerPicker(
val pick: () -> Unit,
)
val LocalServerPicker = staticCompositionLocalOf<ServerPicker> {
error("No ServerPicker provided")
}

View File

@@ -11,16 +11,15 @@ import androidx.activity.compose.PredictiveBackHandler
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.spring import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Devices import androidx.compose.material.icons.rounded.Devices
import androidx.compose.material.icons.rounded.Settings import androidx.compose.material.icons.rounded.Settings
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -46,10 +45,12 @@ import io.github.miuzarte.scrcpyforandroid.BuildConfig
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade
import io.github.miuzarte.scrcpyforandroid.constants.ThemeModes import io.github.miuzarte.scrcpyforandroid.constants.ThemeModes
import io.github.miuzarte.scrcpyforandroid.constants.UiMotion import io.github.miuzarte.scrcpyforandroid.constants.UiMotion
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcuts import io.github.miuzarte.scrcpyforandroid.haptics.LocalAppHaptics
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics
import io.github.miuzarte.scrcpyforandroid.nativecore.NativeAdbService import io.github.miuzarte.scrcpyforandroid.nativecore.NativeAdbService
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
import io.github.miuzarte.scrcpyforandroid.services.AppUpdateChecker import io.github.miuzarte.scrcpyforandroid.services.AppUpdateChecker
import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController
import io.github.miuzarte.scrcpyforandroid.services.SnackbarController import io.github.miuzarte.scrcpyforandroid.services.SnackbarController
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
import io.github.miuzarte.scrcpyforandroid.storage.Settings import io.github.miuzarte.scrcpyforandroid.storage.Settings
@@ -90,21 +91,19 @@ sealed interface RootScreen : NavKey {
@Composable @Composable
fun MainScreen() { fun MainScreen() {
// Environment
val context = LocalContext.current val context = LocalContext.current
val appContext = context.applicationContext val appContext = context.applicationContext
val scope = rememberCoroutineScope()
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
val activity = remember(context) { context as? Activity } val activity = remember(context) { context as? Activity }
val initialOrientation = remember(activity) { val initialOrientation = remember(activity) {
activity?.requestedOrientation ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED activity?.requestedOrientation ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
} }
DisposableEffect(activity) {
onDispose {
activity?.requestedOrientation = initialOrientation
}
}
// Scopes
val scope = rememberCoroutineScope()
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
// Core services
val nativeCore = remember(appContext) { val nativeCore = remember(appContext) {
NativeCoreFacade.get(appContext) NativeCoreFacade.get(appContext)
} }
@@ -112,6 +111,7 @@ fun MainScreen() {
NativeAdbService(appContext) NativeAdbService(appContext)
} }
// Global controllers provided to the compose tree
val snackHostState = remember { SnackbarHostState() } val snackHostState = remember { SnackbarHostState() }
val snackbarController = remember(scope, snackHostState) { val snackbarController = remember(scope, snackHostState) {
SnackbarController( SnackbarController(
@@ -119,6 +119,9 @@ fun MainScreen() {
hostState = snackHostState, hostState = snackHostState,
) )
} }
val haptics = rememberAppHaptics()
// Root navigation and UI chrome state
val saveableStateHolder = rememberSaveableStateHolder() val saveableStateHolder = rememberSaveableStateHolder()
val tabs = remember { MainBottomTabDestination.entries } val tabs = remember { MainBottomTabDestination.entries }
val pagerState = rememberPagerState( val pagerState = rememberPagerState(
@@ -127,6 +130,19 @@ fun MainScreen() {
val currentTab = tabs[pagerState.currentPage] val currentTab = tabs[pagerState.currentPage]
val rootBackStack = remember { mutableStateListOf<NavKey>(RootScreen.Home) } val rootBackStack = remember { mutableStateListOf<NavKey>(RootScreen.Home) }
val currentRootScreen = rootBackStack.lastOrNull() as? RootScreen ?: RootScreen.Home val currentRootScreen = rootBackStack.lastOrNull() as? RootScreen ?: RootScreen.Home
var showReorderDevices by rememberSaveable { mutableStateOf(false) }
var fullscreenOrientation by rememberSaveable {
mutableIntStateOf(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
}
var lastExitBackPressAtMs by rememberSaveable { mutableLongStateOf(0L) }
DisposableEffect(activity) {
onDispose {
activity?.requestedOrientation = initialOrientation
}
}
// Scroll behaviors
val devicesPageScrollBehavior = MiuixScrollBehavior( val devicesPageScrollBehavior = MiuixScrollBehavior(
canScroll = { currentTab == MainBottomTabDestination.Device }) canScroll = { currentTab == MainBottomTabDestination.Device })
val settingsPageScrollBehavior = MiuixScrollBehavior( val settingsPageScrollBehavior = MiuixScrollBehavior(
@@ -140,17 +156,23 @@ fun MainScreen() {
} }
}) })
fun popRoot() { // Navigation helpers
if (rootBackStack.size > 1) val rootNavigator = remember {
rootBackStack.removeAt(rootBackStack.lastIndex) RootNavigator(
push = { rootBackStack.add(it) },
pop = {
if (rootBackStack.size > 1)
rootBackStack.removeAt(rootBackStack.lastIndex)
},
)
}
val fullscreenNavigationState = remember {
FullscreenNavigationState(
setOrientation = { fullscreenOrientation = it }
)
} }
// Unified back behavior: // Shared settings bundles
// 1) pop inner route
// 2) switch tab back to Device
// 3) double-back to exit and disconnect adb/scrcpy
var lastExitBackPressAtMs by rememberSaveable { mutableLongStateOf(0L) }
val asBundleShared by appSettings.bundleState.collectAsState() val asBundleShared by appSettings.bundleState.collectAsState()
val asBundleSharedLatest by rememberUpdatedState(asBundleShared) val asBundleSharedLatest by rememberUpdatedState(asBundleShared)
var asBundle by rememberSaveable(asBundleShared) { mutableStateOf(asBundleShared) } var asBundle by rememberSaveable(asBundleShared) { mutableStateOf(asBundleShared) }
@@ -197,6 +219,7 @@ fun MainScreen() {
} }
} }
// Scrcpy instance and session state
val customServerUri = asBundle.customServerUri val customServerUri = asBundle.customServerUri
.ifBlank { null } .ifBlank { null }
val customServerVersion = asBundle.customServerVersion val customServerVersion = asBundle.customServerVersion
@@ -220,6 +243,35 @@ fun MainScreen() {
} }
val currentSession by scrcpy.currentSessionState.collectAsState() val currentSession by scrcpy.currentSessionState.collectAsState()
// Side-effect launchers and composition locals
val picker = rememberLauncherForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri: Uri? ->
if (uri == null) return@rememberLauncherForActivityResult
runCatching {
context.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
}
asBundle = asBundle.copy(customServerUri = uri.toString())
}
val serverPicker = remember(picker) {
ServerPicker {
picker.launch(
arrayOf(
"application/java-archive",
"application/octet-stream",
"*/*"
)
)
}
}
// Derived flags
val canNavigateBack = rootBackStack.size > 1
|| pagerState.currentPage != MainBottomTabDestination.Device.ordinal
LaunchedEffect(asBundle.lastUpdateCheckAt) { LaunchedEffect(asBundle.lastUpdateCheckAt) {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
if (now - asBundle.lastUpdateCheckAt < AppUpdateChecker.CHECK_INTERVAL_MS) return@LaunchedEffect if (now - asBundle.lastUpdateCheckAt < AppUpdateChecker.CHECK_INTERVAL_MS) return@LaunchedEffect
@@ -231,7 +283,7 @@ fun MainScreen() {
fun handleBackNavigation() { fun handleBackNavigation() {
if (rootBackStack.size > 1) { if (rootBackStack.size > 1) {
popRoot() rootNavigator.pop()
} else if (pagerState.currentPage != MainBottomTabDestination.Device.ordinal) { } else if (pagerState.currentPage != MainBottomTabDestination.Device.ordinal) {
scope.launch { scope.launch {
pagerState.animateScrollToPage( pagerState.animateScrollToPage(
@@ -260,14 +312,6 @@ fun MainScreen() {
} }
} }
var showReorderDevices by rememberSaveable { mutableStateOf(false) }
var fullscreenOrientation by rememberSaveable {
mutableIntStateOf(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
}
val canNavigateBack = rootBackStack.size > 1
|| pagerState.currentPage != MainBottomTabDestination.Device.ordinal
BackHandler(enabled = currentRootScreen !is RootScreen.Fullscreen) { BackHandler(enabled = currentRootScreen !is RootScreen.Fullscreen) {
handleBackNavigation() handleBackNavigation()
} }
@@ -317,18 +361,6 @@ fun MainScreen() {
LaunchedEffect(asBundle.adbKeyName) { LaunchedEffect(asBundle.adbKeyName) {
adbService.keyName = asBundle.adbKeyName.ifBlank { AppSettings.ADB_KEY_NAME.defaultValue } adbService.keyName = asBundle.adbKeyName.ifBlank { AppSettings.ADB_KEY_NAME.defaultValue }
} }
val picker = rememberLauncherForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri: Uri? ->
if (uri == null) return@rememberLauncherForActivityResult
runCatching {
context.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
}
asBundle = asBundle.copy(customServerUri = uri.toString())
}
val rootEntryProvider = entryProvider<NavKey> { val rootEntryProvider = entryProvider<NavKey> {
entry(RootScreen.Home) { entry(RootScreen.Home) {
@@ -371,39 +403,13 @@ fun MainScreen() {
nativeCore = nativeCore, nativeCore = nativeCore,
adbService = adbService, adbService = adbService,
scrcpy = scrcpy, scrcpy = scrcpy,
snackbar = snackbarController,
scrollBehavior = devicesPageScrollBehavior, scrollBehavior = devicesPageScrollBehavior,
onOpenVirtualButtonOrder = { rootBackStack.add(RootScreen.VirtualButtonOrder) },
onOpenReorderDevices = { showReorderDevices = true }, onOpenReorderDevices = { showReorderDevices = true },
onOpenAdvancedPage = { rootBackStack.add(RootScreen.Advanced) },
onOpenFullscreenPage = {
currentSession?.let { session ->
fullscreenOrientation =
if (session.width >= session.height) {
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
} else {
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
rootBackStack.add(RootScreen.Fullscreen)
}
},
) )
MainBottomTabDestination.Settings -> SettingsScreen( MainBottomTabDestination.Settings -> SettingsScreen(
scrollBehavior = settingsPageScrollBehavior, scrollBehavior = settingsPageScrollBehavior,
snackbar = snackbarController,
onOpenReorderDevices = { showReorderDevices = true }, onOpenReorderDevices = { showReorderDevices = true },
onOpenVirtualButtonOrder = { rootBackStack.add(RootScreen.VirtualButtonOrder) },
onOpenAbout = { rootBackStack.add(RootScreen.About) },
onPickServer = {
picker.launch(
arrayOf(
"application/java-archive",
"application/octet-stream",
"*/*"
)
)
},
) )
} }
} }
@@ -418,29 +424,23 @@ fun MainScreen() {
entry(RootScreen.Advanced) { entry(RootScreen.Advanced) {
ScrcpyAllOptionsScreen( ScrcpyAllOptionsScreen(
onBack = ::popRoot,
scrollBehavior = advancedPageScrollBehavior, scrollBehavior = advancedPageScrollBehavior,
snackbar = snackbarController,
scrcpy = scrcpy, scrcpy = scrcpy,
) )
} }
entry(RootScreen.About) { entry(RootScreen.About) {
AboutScreen( AboutScreen()
onBack = ::popRoot,
)
} }
entry(RootScreen.VirtualButtonOrder) { entry(RootScreen.VirtualButtonOrder) {
VirtualButtonOrderScreen( VirtualButtonOrderScreen(
onBack = ::popRoot,
scrollBehavior = advancedPageScrollBehavior, scrollBehavior = advancedPageScrollBehavior,
) )
} }
entry(RootScreen.Fullscreen) { entry(RootScreen.Fullscreen) {
FullscreenControlScreen( FullscreenControlScreen(
onBack = ::popRoot,
scrcpy = scrcpy, scrcpy = scrcpy,
nativeCore = nativeCore, nativeCore = nativeCore,
onVideoSizeChanged = { width, height -> onVideoSizeChanged = { width, height ->
@@ -465,9 +465,17 @@ fun MainScreen() {
val themeController = remember(themeMode) { ThemeController(colorSchemeMode = themeMode) } val themeController = remember(themeMode) { ThemeController(colorSchemeMode = themeMode) }
MiuixTheme(controller = themeController) { MiuixTheme(controller = themeController) {
NavDisplay( CompositionLocalProvider(
entries = rootEntries, LocalRootNavigator provides rootNavigator,
onBack = ::popRoot, LocalSnackbarController provides snackbarController,
) LocalAppHaptics provides haptics,
LocalFullscreenNavigationState provides fullscreenNavigationState,
LocalServerPicker provides serverPicker,
) {
NavDisplay(
entries = rootEntries,
onBack = rootNavigator.pop,
)
}
} }
} }

View File

@@ -0,0 +1,21 @@
package io.github.miuzarte.scrcpyforandroid.pages
import android.content.pm.ActivityInfo
import androidx.compose.runtime.staticCompositionLocalOf
class RootNavigator(
val push: (RootScreen) -> Unit,
val pop: () -> Unit,
)
val LocalRootNavigator = staticCompositionLocalOf<RootNavigator> {
error("No RootNavigator provided")
}
class FullscreenNavigationState(
val setOrientation: (Int) -> Unit,
)
val LocalFullscreenNavigationState = staticCompositionLocalOf<FullscreenNavigationState> {
error("No FullscreenNavigationState provided")
}

View File

@@ -42,7 +42,7 @@ import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperTextField
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec
import io.github.miuzarte.scrcpyforandroid.services.SnackbarController import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController
import io.github.miuzarte.scrcpyforandroid.storage.Settings import io.github.miuzarte.scrcpyforandroid.storage.Settings
import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -69,17 +69,17 @@ import kotlin.math.roundToInt
@Composable @Composable
internal fun ScrcpyAllOptionsScreen( internal fun ScrcpyAllOptionsScreen(
onBack: () -> Unit,
scrollBehavior: ScrollBehavior, scrollBehavior: ScrollBehavior,
snackbar: SnackbarController,
scrcpy: Scrcpy, scrcpy: Scrcpy,
) { ) {
val snackbar = LocalSnackbarController.current
val navigator = LocalRootNavigator.current
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = "所有参数", title = "所有参数",
navigationIcon = { navigationIcon = {
IconButton(onClick = onBack) { IconButton(onClick = navigator.pop) {
Icon( Icon(
Icons.AutoMirrored.Rounded.ArrowBack, Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = "返回" contentDescription = "返回"
@@ -94,7 +94,6 @@ internal fun ScrcpyAllOptionsScreen(
ScrcpyAllOptionsPage( ScrcpyAllOptionsPage(
contentPadding = contentPadding, contentPadding = contentPadding,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
snackbar = snackbar,
scrcpy = scrcpy, scrcpy = scrcpy,
) )
} }
@@ -104,12 +103,12 @@ internal fun ScrcpyAllOptionsScreen(
internal fun ScrcpyAllOptionsPage( internal fun ScrcpyAllOptionsPage(
contentPadding: PaddingValues, contentPadding: PaddingValues,
scrollBehavior: ScrollBehavior, scrollBehavior: ScrollBehavior,
snackbar: SnackbarController,
scrcpy: Scrcpy, scrcpy: Scrcpy,
) { ) {
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val taskScope = remember { CoroutineScope(Dispatchers.IO + SupervisorJob()) } val taskScope = remember { CoroutineScope(Dispatchers.IO + SupervisorJob()) }
val snackbar = LocalSnackbarController.current
var refreshBusy by rememberSaveable { mutableStateOf(false) } var refreshBusy by rememberSaveable { mutableStateOf(false) }
var listRefreshVersion by rememberSaveable { mutableIntStateOf(0) } var listRefreshVersion by rememberSaveable { mutableIntStateOf(0) }

View File

@@ -36,7 +36,7 @@ import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlider
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperTextField import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperTextField
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
import io.github.miuzarte.scrcpyforandroid.services.AppUpdateChecker import io.github.miuzarte.scrcpyforandroid.services.AppUpdateChecker
import io.github.miuzarte.scrcpyforandroid.services.SnackbarController import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
import io.github.miuzarte.scrcpyforandroid.storage.PreferenceMigration import io.github.miuzarte.scrcpyforandroid.storage.PreferenceMigration
import io.github.miuzarte.scrcpyforandroid.storage.Settings import io.github.miuzarte.scrcpyforandroid.storage.Settings
@@ -64,11 +64,7 @@ import kotlin.system.exitProcess
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(
scrollBehavior: ScrollBehavior, scrollBehavior: ScrollBehavior,
snackbar: SnackbarController,
onOpenReorderDevices: () -> Unit, onOpenReorderDevices: () -> Unit,
onOpenVirtualButtonOrder: () -> Unit,
onOpenAbout: () -> Unit,
onPickServer: () -> Unit,
) { ) {
Scaffold( Scaffold(
topBar = { topBar = {
@@ -81,11 +77,7 @@ fun SettingsScreen(
SettingsPage( SettingsPage(
contentPadding = pagePadding, contentPadding = pagePadding,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
snackbar = snackbar,
onOpenReorderDevices = onOpenReorderDevices, onOpenReorderDevices = onOpenReorderDevices,
onOpenVirtualButtonOrder = onOpenVirtualButtonOrder,
onOpenAbout = onOpenAbout,
onPickServer = onPickServer,
) )
} }
} }
@@ -94,11 +86,7 @@ fun SettingsScreen(
fun SettingsPage( fun SettingsPage(
contentPadding: PaddingValues, contentPadding: PaddingValues,
scrollBehavior: ScrollBehavior, scrollBehavior: ScrollBehavior,
snackbar: SnackbarController,
onOpenReorderDevices: () -> Unit, onOpenReorderDevices: () -> Unit,
onOpenVirtualButtonOrder: () -> Unit,
onOpenAbout: () -> Unit,
onPickServer: () -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val appContext = context.applicationContext val appContext = context.applicationContext
@@ -111,6 +99,10 @@ fun SettingsPage(
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) } val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val snackbar = LocalSnackbarController.current
val navigator = LocalRootNavigator.current
val serverPicker = LocalServerPicker.current
val asBundleShared by appSettings.bundleState.collectAsState() val asBundleShared by appSettings.bundleState.collectAsState()
val asBundleSharedLatest by rememberUpdatedState(asBundleShared) val asBundleSharedLatest by rememberUpdatedState(asBundleShared)
var asBundle by rememberSaveable(asBundleShared) { mutableStateOf(asBundleShared) } var asBundle by rememberSaveable(asBundleShared) { mutableStateOf(asBundleShared) }
@@ -257,7 +249,7 @@ fun SettingsPage(
ArrowPreference( ArrowPreference(
title = "虚拟按钮排序", title = "虚拟按钮排序",
summary = "手动排序预览/全屏时的虚拟按钮,并选择哪些按钮展示在外", summary = "手动排序预览/全屏时的虚拟按钮,并选择哪些按钮展示在外",
onClick = onOpenVirtualButtonOrder, onClick = { navigator.push(RootScreen.VirtualButtonOrder) },
) )
SwitchPreference( SwitchPreference(
title = "全屏显示虚拟按钮", title = "全屏显示虚拟按钮",
@@ -307,13 +299,13 @@ fun SettingsPage(
}, },
) { ) {
Icon( Icon(
imageVector = Icons.Rounded.Clear, Icons.Rounded.Clear,
contentDescription = "清空", contentDescription = "清空",
) )
} }
IconButton(onClick = onPickServer) { IconButton(onClick = serverPicker.pick) {
Icon( Icon(
imageVector = Icons.Rounded.FileOpen, Icons.Rounded.FileOpen,
contentDescription = "选择文件", contentDescription = "选择文件",
) )
} }
@@ -474,7 +466,7 @@ fun SettingsPage(
ArrowPreference( ArrowPreference(
title = "关于", title = "关于",
summary = updateSummary, summary = updateSummary,
onClick = onOpenAbout, onClick = { navigator.push(RootScreen.About) },
) )
} }
} }

View File

@@ -37,16 +37,16 @@ import top.yukonga.miuix.kmp.preference.SwitchPreference
@Composable @Composable
internal fun VirtualButtonOrderScreen( internal fun VirtualButtonOrderScreen(
onBack: () -> Unit,
scrollBehavior: ScrollBehavior, scrollBehavior: ScrollBehavior,
) { ) {
val navigator = LocalRootNavigator.current
Scaffold( Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = { topBar = {
TopAppBar( TopAppBar(
title = "虚拟按钮排序", title = "虚拟按钮排序",
navigationIcon = { navigationIcon = {
IconButton(onClick = onBack) { IconButton(onClick = navigator.pop) {
Icon( Icon(
Icons.AutoMirrored.Rounded.ArrowBack, Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = "返回" contentDescription = "返回"

View File

@@ -1,5 +1,6 @@
package io.github.miuzarte.scrcpyforandroid.services package io.github.miuzarte.scrcpyforandroid.services
import androidx.compose.runtime.staticCompositionLocalOf
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import top.yukonga.miuix.kmp.basic.SnackbarDuration import top.yukonga.miuix.kmp.basic.SnackbarDuration
@@ -29,3 +30,7 @@ class SnackbarController(
} }
} }
} }
val LocalSnackbarController = staticCompositionLocalOf<SnackbarController> {
error("No SnackbarController provided")
}

View File

@@ -76,7 +76,7 @@ import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperTextField
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec
import io.github.miuzarte.scrcpyforandroid.scrcpy.TouchEventHandler import io.github.miuzarte.scrcpyforandroid.scrcpy.TouchEventHandler
import io.github.miuzarte.scrcpyforandroid.services.SnackbarController import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController
import io.github.miuzarte.scrcpyforandroid.storage.Settings import io.github.miuzarte.scrcpyforandroid.storage.Settings
import io.github.miuzarte.scrcpyforandroid.storage.Storage import io.github.miuzarte.scrcpyforandroid.storage.Storage
import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions
@@ -352,7 +352,6 @@ internal fun VirtualButtonCard(
@Composable @Composable
internal fun ConfigPanel( internal fun ConfigPanel(
busy: Boolean, busy: Boolean,
snackbar: SnackbarController,
audioForwardingSupported: Boolean, audioForwardingSupported: Boolean,
cameraMirroringSupported: Boolean, cameraMirroringSupported: Boolean,
adbConnecting: Boolean, adbConnecting: Boolean,
@@ -366,6 +365,8 @@ internal fun ConfigPanel(
) { ) {
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) } val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
val snackbar = LocalSnackbarController.current
val sessionStarted = sessionInfo != null val sessionStarted = sessionInfo != null
val soBundleShared by scrcpyOptions.bundleState.collectAsState() val soBundleShared by scrcpyOptions.bundleState.collectAsState()

View File

@@ -20,7 +20,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics import io.github.miuzarte.scrcpyforandroid.haptics.LocalAppHaptics
import sh.calvin.reorderable.ReorderableColumn import sh.calvin.reorderable.ReorderableColumn
import sh.calvin.reorderable.ReorderableRow import sh.calvin.reorderable.ReorderableRow
import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.Card
@@ -50,7 +50,7 @@ class ReorderableList(
@Composable @Composable
operator fun invoke() { operator fun invoke() {
val haptics = rememberAppHaptics() val haptics = LocalAppHaptics.current
val items = itemsProvider() val items = itemsProvider()
when (orientation) { when (orientation) {
Orientation.Column -> { Orientation.Column -> {
@@ -81,7 +81,7 @@ class ReorderableList(
horizontalArrangement = Arrangement.spacedBy(UiSpacing.Small) horizontalArrangement = Arrangement.spacedBy(UiSpacing.Small)
) { ) {
if (item.icon != null) Icon( if (item.icon != null) Icon(
imageVector = item.icon, item.icon,
contentDescription = item.title contentDescription = item.title
) )
Column { Column {

View File

@@ -25,7 +25,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics import io.github.miuzarte.scrcpyforandroid.haptics.LocalAppHaptics
import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.CardDefaults.defaultColors import top.yukonga.miuix.kmp.basic.CardDefaults.defaultColors
import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.Icon
@@ -67,7 +67,7 @@ internal fun StatusCardLayout(
spec: StatusCardSpec, spec: StatusCardSpec,
busyLabel: String?, busyLabel: String?,
) { ) {
val haptics = rememberAppHaptics() val haptics = LocalAppHaptics.current
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -91,7 +91,7 @@ internal fun StatusCardLayout(
contentAlignment = Alignment.BottomEnd, contentAlignment = Alignment.BottomEnd,
) { ) {
Icon( Icon(
imageVector = spec.big.icon, spec.big.icon,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(170.dp), modifier = Modifier.size(170.dp),
tint = spec.big.iconTint, tint = spec.big.iconTint,
@@ -153,7 +153,7 @@ internal fun StatusCardLayout(
@Composable @Composable
private fun StatusMetricCard(spec: StatusSmallCardSpec, modifier: Modifier) { private fun StatusMetricCard(spec: StatusSmallCardSpec, modifier: Modifier) {
val haptics = rememberAppHaptics() val haptics = LocalAppHaptics.current
Card( Card(
modifier = modifier, modifier = modifier,
insideMargin = PaddingValues(UiSpacing.Large), insideMargin = PaddingValues(UiSpacing.Large),

View File

@@ -33,7 +33,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.github.miuzarte.scrcpyforandroid.constants.UiAndroidKeycodes import io.github.miuzarte.scrcpyforandroid.constants.UiAndroidKeycodes
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics import io.github.miuzarte.scrcpyforandroid.haptics.LocalAppHaptics
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import top.yukonga.miuix.kmp.basic.Button import top.yukonga.miuix.kmp.basic.Button
@@ -171,7 +171,7 @@ class VirtualButtonBar(
onAction: (VirtualButtonAction) -> Unit, onAction: (VirtualButtonAction) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val haptics = rememberAppHaptics() val haptics = LocalAppHaptics.current
val activeContainerColor = MiuixTheme.colorScheme.primary val activeContainerColor = MiuixTheme.colorScheme.primary
val disabledContainerColor = MiuixTheme.colorScheme.primary.copy(alpha = 0.35f) val disabledContainerColor = MiuixTheme.colorScheme.primary.copy(alpha = 0.35f)
val activeContentColor = MiuixTheme.colorScheme.onPrimary val activeContentColor = MiuixTheme.colorScheme.onPrimary
@@ -236,7 +236,7 @@ class VirtualButtonBar(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val haptics = rememberAppHaptics() val haptics = LocalAppHaptics.current
var showMorePopup by remember { mutableStateOf(false) } var showMorePopup by remember { mutableStateOf(false) }
Row( Row(
@@ -293,7 +293,7 @@ class VirtualButtonBar(
renderInRootScaffold: Boolean, renderInRootScaffold: Boolean,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val haptics = rememberAppHaptics() val haptics = LocalAppHaptics.current
val spinnerItems = remember(moreActions) { val spinnerItems = remember(moreActions) {
moreActions.map { action -> moreActions.map { action ->
SpinnerEntry( SpinnerEntry(