From c4003826ffa7b4245406c088ec8ee83dce229768 Mon Sep 17 00:00:00 2001 From: Miuzarte <982809597@qq.com> Date: Fri, 10 Apr 2026 17:23:11 +0800 Subject: [PATCH] refactor: navigation and haptic move into composition locals --- .../scrcpyforandroid/haptics/AppHaptics.kt | 5 + .../scrcpyforandroid/pages/AboutScreen.kt | 9 +- .../scrcpyforandroid/pages/DeviceTabScreen.kt | 39 +++-- .../pages/FullscreenControlScreen.kt | 10 +- .../pages/LocalServerPicker.kt | 11 ++ .../scrcpyforandroid/pages/MainScreen.kt | 164 +++++++++--------- .../scrcpyforandroid/pages/RootNavigator.kt | 21 +++ .../pages/ScrcpyAllOptionsScreen.kt | 11 +- .../scrcpyforandroid/pages/SettingsScreen.kt | 28 ++- .../pages/VirtualButtonOrderScreen.kt | 4 +- .../services/SnackbarController.kt | 5 + .../scrcpyforandroid/widgets/DeviceWidgets.kt | 5 +- .../widgets/ReorderableList.kt | 6 +- .../widgets/StatusCardModels.kt | 8 +- .../widgets/VirtualButtons.kt | 8 +- 15 files changed, 189 insertions(+), 145 deletions(-) create mode 100644 app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/LocalServerPicker.kt create mode 100644 app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/RootNavigator.kt diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/haptics/AppHaptics.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/haptics/AppHaptics.kt index e036e46..aa1e921 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/haptics/AppHaptics.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/haptics/AppHaptics.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback @@ -29,6 +30,10 @@ data class AppHaptics( val virtualKey: () -> Unit, ) +val LocalAppHaptics = staticCompositionLocalOf { + error("No AppHaptics provided") +} + @Composable fun rememberAppHaptics(): AppHaptics { val hapticFeedback = LocalHapticFeedback.current diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AboutScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AboutScreen.kt index 3c8ac8f..66d6bee 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AboutScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AboutScreen.kt @@ -74,9 +74,8 @@ import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import androidx.compose.ui.graphics.BlendMode as ComposeBlendMode @Composable -internal fun AboutScreen( - onBack: () -> Unit, -) { +internal fun AboutScreen() { + val navigator = LocalRootNavigator.current val topAppBarScrollBehavior = MiuixScrollBehavior() val lazyListState = rememberLazyListState() var logoHeightPx by remember { mutableIntStateOf(0) } @@ -102,9 +101,9 @@ internal fun AboutScreen( titleColor = colorScheme.onSurface.copy(alpha = scrollProgress), defaultWindowInsetsPadding = false, navigationIcon = { - IconButton(onClick = onBack) { + IconButton(onClick = navigator.pop) { Icon( - imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "返回", ) } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt index 77771a0..c3f3b2f 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt @@ -2,6 +2,7 @@ package io.github.miuzarte.scrcpyforandroid.pages import android.annotation.SuppressLint import android.app.Activity +import android.content.pm.ActivityInfo import android.util.Log import android.view.WindowManager 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.constants.Defaults 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.DeviceShortcut 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.services.EventLogger 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.storage.Settings import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings @@ -90,13 +91,10 @@ fun DeviceTabScreen( nativeCore: NativeCoreFacade, adbService: NativeAdbService, scrcpy: Scrcpy, - snackbar: SnackbarController, scrollBehavior: ScrollBehavior, - onOpenVirtualButtonOrder: () -> Unit, onOpenReorderDevices: () -> Unit, - onOpenAdvancedPage: () -> Unit, - onOpenFullscreenPage: () -> Unit, ) { + val navigator = LocalRootNavigator.current var showThreePointMenu by rememberSaveable { mutableStateOf(false) } Scaffold( @@ -121,7 +119,7 @@ fun DeviceTabScreen( showThreePointMenu = false }, onOpenVirtualButtonOrder = { - onOpenVirtualButtonOrder() + navigator.push(RootScreen.VirtualButtonOrder) showThreePointMenu = false }, canClearLogs = EventLogger.hasLogs(), @@ -140,10 +138,7 @@ fun DeviceTabScreen( nativeCore = nativeCore, adbService = adbService, scrcpy = scrcpy, - snackbar = snackbar, scrollBehavior = scrollBehavior, - onOpenAdvancedPage = onOpenAdvancedPage, - onOpenFullscreenPage = onOpenFullscreenPage, ) } } @@ -154,17 +149,18 @@ fun DeviceTabPage( nativeCore: NativeCoreFacade, adbService: NativeAdbService, scrcpy: Scrcpy, - snackbar: SnackbarController, scrollBehavior: ScrollBehavior, - onOpenAdvancedPage: () -> Unit, - onOpenFullscreenPage: () -> Unit, ) { val context = LocalContext.current val scope = rememberCoroutineScope() val taskScope = remember { CoroutineScope(Dispatchers.IO + SupervisorJob()) } - val haptics = rememberAppHaptics() 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 asBundleSharedLatest by rememberUpdatedState(asBundleShared) var asBundle by rememberSaveable(asBundleShared) { mutableStateOf(asBundleShared) } @@ -869,12 +865,11 @@ fun DeviceTabPage( SectionSmallTitle("Scrcpy") ConfigPanel( busy = busy, - snackbar = snackbar, audioForwardingSupported = audioForwardingSupported, cameraMirroringSupported = cameraMirroringSupported, adbConnecting = adbConnecting, isQuickConnected = isQuickConnected, - onOpenAdvanced = onOpenAdvancedPage, + onOpenAdvanced = { navigator.push(RootScreen.Advanced) }, onStartStopHaptic = { haptics.contextClick() }, onStart = { runBusy("启动 scrcpy") { @@ -949,8 +944,16 @@ fun DeviceTabPage( previewControlsVisible = !previewControlsVisible }, onOpenFullscreen = { - if (sessionInfo != null) { - onOpenFullscreenPage() + val currentSession = sessionInfo + if (currentSession != null) { + fullscreenNavigationState.setOrientation( + if (currentSession.width >= currentSession.height) { + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + } else { + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } + ) + navigator.push(RootScreen.Fullscreen) } }, ) diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlScreen.kt index 60e8d9d..221fa0d 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlScreen.kt @@ -25,7 +25,7 @@ import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat 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.storage.Settings import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings @@ -42,16 +42,16 @@ import top.yukonga.miuix.kmp.basic.Scaffold @Composable fun FullscreenControlScreen( - onBack: () -> Unit, scrcpy: Scrcpy, nativeCore: NativeCoreFacade, 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 haptics = rememberAppHaptics() + val haptics = LocalAppHaptics.current val scope = rememberCoroutineScope() val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) } @@ -154,7 +154,7 @@ fun FullscreenControlScreen( FullscreenControlScreen( session = session, nativeCore = nativeCore, - onDismiss = onBack, + onDismiss = navigator.pop, showDebugInfo = fullscreenDebugInfo, currentFps = currentFps, enableBackHandler = false, diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/LocalServerPicker.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/LocalServerPicker.kt new file mode 100644 index 0000000..9f8fe29 --- /dev/null +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/LocalServerPicker.kt @@ -0,0 +1,11 @@ +package io.github.miuzarte.scrcpyforandroid.pages + +import androidx.compose.runtime.staticCompositionLocalOf + +class ServerPicker( + val pick: () -> Unit, +) + +val LocalServerPicker = staticCompositionLocalOf { + error("No ServerPicker provided") +} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainScreen.kt index 58cf356..7cdc7df 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainScreen.kt @@ -11,16 +11,15 @@ import androidx.activity.compose.PredictiveBackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.core.spring -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Devices import androidx.compose.material.icons.rounded.Settings import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect 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.constants.ThemeModes 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.scrcpy.Scrcpy 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.storage.AppSettings import io.github.miuzarte.scrcpyforandroid.storage.Settings @@ -90,21 +91,19 @@ sealed interface RootScreen : NavKey { @Composable fun MainScreen() { + // Environment val context = LocalContext.current val appContext = context.applicationContext - val scope = rememberCoroutineScope() - val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) } - val activity = remember(context) { context as? Activity } val initialOrientation = remember(activity) { 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) { NativeCoreFacade.get(appContext) } @@ -112,6 +111,7 @@ fun MainScreen() { NativeAdbService(appContext) } + // Global controllers provided to the compose tree val snackHostState = remember { SnackbarHostState() } val snackbarController = remember(scope, snackHostState) { SnackbarController( @@ -119,6 +119,9 @@ fun MainScreen() { hostState = snackHostState, ) } + val haptics = rememberAppHaptics() + + // Root navigation and UI chrome state val saveableStateHolder = rememberSaveableStateHolder() val tabs = remember { MainBottomTabDestination.entries } val pagerState = rememberPagerState( @@ -127,6 +130,19 @@ fun MainScreen() { val currentTab = tabs[pagerState.currentPage] val rootBackStack = remember { mutableStateListOf(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( canScroll = { currentTab == MainBottomTabDestination.Device }) val settingsPageScrollBehavior = MiuixScrollBehavior( @@ -140,17 +156,23 @@ fun MainScreen() { } }) - fun popRoot() { - if (rootBackStack.size > 1) - rootBackStack.removeAt(rootBackStack.lastIndex) + // Navigation helpers + val rootNavigator = remember { + RootNavigator( + push = { rootBackStack.add(it) }, + pop = { + if (rootBackStack.size > 1) + rootBackStack.removeAt(rootBackStack.lastIndex) + }, + ) + } + val fullscreenNavigationState = remember { + FullscreenNavigationState( + setOrientation = { fullscreenOrientation = it } + ) } - // Unified back behavior: - // 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) } - + // Shared settings bundles val asBundleShared by appSettings.bundleState.collectAsState() val asBundleSharedLatest by rememberUpdatedState(asBundleShared) var asBundle by rememberSaveable(asBundleShared) { mutableStateOf(asBundleShared) } @@ -197,6 +219,7 @@ fun MainScreen() { } } + // Scrcpy instance and session state val customServerUri = asBundle.customServerUri .ifBlank { null } val customServerVersion = asBundle.customServerVersion @@ -220,6 +243,35 @@ fun MainScreen() { } 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) { val now = System.currentTimeMillis() if (now - asBundle.lastUpdateCheckAt < AppUpdateChecker.CHECK_INTERVAL_MS) return@LaunchedEffect @@ -231,7 +283,7 @@ fun MainScreen() { fun handleBackNavigation() { if (rootBackStack.size > 1) { - popRoot() + rootNavigator.pop() } else if (pagerState.currentPage != MainBottomTabDestination.Device.ordinal) { scope.launch { 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) { handleBackNavigation() } @@ -317,18 +361,6 @@ fun MainScreen() { LaunchedEffect(asBundle.adbKeyName) { 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 { entry(RootScreen.Home) { @@ -371,39 +403,13 @@ fun MainScreen() { nativeCore = nativeCore, adbService = adbService, scrcpy = scrcpy, - snackbar = snackbarController, scrollBehavior = devicesPageScrollBehavior, - onOpenVirtualButtonOrder = { rootBackStack.add(RootScreen.VirtualButtonOrder) }, 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( scrollBehavior = settingsPageScrollBehavior, - snackbar = snackbarController, 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) { ScrcpyAllOptionsScreen( - onBack = ::popRoot, scrollBehavior = advancedPageScrollBehavior, - snackbar = snackbarController, scrcpy = scrcpy, ) } entry(RootScreen.About) { - AboutScreen( - onBack = ::popRoot, - ) + AboutScreen() } entry(RootScreen.VirtualButtonOrder) { VirtualButtonOrderScreen( - onBack = ::popRoot, scrollBehavior = advancedPageScrollBehavior, ) } entry(RootScreen.Fullscreen) { FullscreenControlScreen( - onBack = ::popRoot, scrcpy = scrcpy, nativeCore = nativeCore, onVideoSizeChanged = { width, height -> @@ -465,9 +465,17 @@ fun MainScreen() { val themeController = remember(themeMode) { ThemeController(colorSchemeMode = themeMode) } MiuixTheme(controller = themeController) { - NavDisplay( - entries = rootEntries, - onBack = ::popRoot, - ) + CompositionLocalProvider( + LocalRootNavigator provides rootNavigator, + LocalSnackbarController provides snackbarController, + LocalAppHaptics provides haptics, + LocalFullscreenNavigationState provides fullscreenNavigationState, + LocalServerPicker provides serverPicker, + ) { + NavDisplay( + entries = rootEntries, + onBack = rootNavigator.pop, + ) + } } } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/RootNavigator.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/RootNavigator.kt new file mode 100644 index 0000000..878287d --- /dev/null +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/RootNavigator.kt @@ -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 { + error("No RootNavigator provided") +} + +class FullscreenNavigationState( + val setOrientation: (Int) -> Unit, +) + +val LocalFullscreenNavigationState = staticCompositionLocalOf { + error("No FullscreenNavigationState provided") +} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt index c047c2a..6442daf 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt @@ -42,7 +42,7 @@ import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperTextField import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared 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.Storage.scrcpyOptions import kotlinx.coroutines.CoroutineScope @@ -69,17 +69,17 @@ import kotlin.math.roundToInt @Composable internal fun ScrcpyAllOptionsScreen( - onBack: () -> Unit, scrollBehavior: ScrollBehavior, - snackbar: SnackbarController, scrcpy: Scrcpy, ) { + val snackbar = LocalSnackbarController.current + val navigator = LocalRootNavigator.current Scaffold( topBar = { TopAppBar( title = "所有参数", navigationIcon = { - IconButton(onClick = onBack) { + IconButton(onClick = navigator.pop) { Icon( Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "返回" @@ -94,7 +94,6 @@ internal fun ScrcpyAllOptionsScreen( ScrcpyAllOptionsPage( contentPadding = contentPadding, scrollBehavior = scrollBehavior, - snackbar = snackbar, scrcpy = scrcpy, ) } @@ -104,12 +103,12 @@ internal fun ScrcpyAllOptionsScreen( internal fun ScrcpyAllOptionsPage( contentPadding: PaddingValues, scrollBehavior: ScrollBehavior, - snackbar: SnackbarController, scrcpy: Scrcpy, ) { val focusManager = LocalFocusManager.current val scope = rememberCoroutineScope() val taskScope = remember { CoroutineScope(Dispatchers.IO + SupervisorJob()) } + val snackbar = LocalSnackbarController.current var refreshBusy by rememberSaveable { mutableStateOf(false) } var listRefreshVersion by rememberSaveable { mutableIntStateOf(0) } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsScreen.kt index 0c1e657..1b2969e 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsScreen.kt @@ -36,7 +36,7 @@ import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlider import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperTextField import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy 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.PreferenceMigration import io.github.miuzarte.scrcpyforandroid.storage.Settings @@ -64,11 +64,7 @@ import kotlin.system.exitProcess @Composable fun SettingsScreen( scrollBehavior: ScrollBehavior, - snackbar: SnackbarController, onOpenReorderDevices: () -> Unit, - onOpenVirtualButtonOrder: () -> Unit, - onOpenAbout: () -> Unit, - onPickServer: () -> Unit, ) { Scaffold( topBar = { @@ -81,11 +77,7 @@ fun SettingsScreen( SettingsPage( contentPadding = pagePadding, scrollBehavior = scrollBehavior, - snackbar = snackbar, onOpenReorderDevices = onOpenReorderDevices, - onOpenVirtualButtonOrder = onOpenVirtualButtonOrder, - onOpenAbout = onOpenAbout, - onPickServer = onPickServer, ) } } @@ -94,11 +86,7 @@ fun SettingsScreen( fun SettingsPage( contentPadding: PaddingValues, scrollBehavior: ScrollBehavior, - snackbar: SnackbarController, onOpenReorderDevices: () -> Unit, - onOpenVirtualButtonOrder: () -> Unit, - onOpenAbout: () -> Unit, - onPickServer: () -> Unit, ) { val context = LocalContext.current val appContext = context.applicationContext @@ -111,6 +99,10 @@ fun SettingsPage( val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) } val scope = rememberCoroutineScope() + val snackbar = LocalSnackbarController.current + val navigator = LocalRootNavigator.current + val serverPicker = LocalServerPicker.current + val asBundleShared by appSettings.bundleState.collectAsState() val asBundleSharedLatest by rememberUpdatedState(asBundleShared) var asBundle by rememberSaveable(asBundleShared) { mutableStateOf(asBundleShared) } @@ -257,7 +249,7 @@ fun SettingsPage( ArrowPreference( title = "虚拟按钮排序", summary = "手动排序预览/全屏时的虚拟按钮,并选择哪些按钮展示在外", - onClick = onOpenVirtualButtonOrder, + onClick = { navigator.push(RootScreen.VirtualButtonOrder) }, ) SwitchPreference( title = "全屏显示虚拟按钮", @@ -307,13 +299,13 @@ fun SettingsPage( }, ) { Icon( - imageVector = Icons.Rounded.Clear, + Icons.Rounded.Clear, contentDescription = "清空", ) } - IconButton(onClick = onPickServer) { + IconButton(onClick = serverPicker.pick) { Icon( - imageVector = Icons.Rounded.FileOpen, + Icons.Rounded.FileOpen, contentDescription = "选择文件", ) } @@ -474,7 +466,7 @@ fun SettingsPage( ArrowPreference( title = "关于", summary = updateSummary, - onClick = onOpenAbout, + onClick = { navigator.push(RootScreen.About) }, ) } } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderScreen.kt index b373e94..097f5c2 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderScreen.kt @@ -37,16 +37,16 @@ import top.yukonga.miuix.kmp.preference.SwitchPreference @Composable internal fun VirtualButtonOrderScreen( - onBack: () -> Unit, scrollBehavior: ScrollBehavior, ) { + val navigator = LocalRootNavigator.current Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { TopAppBar( title = "虚拟按钮排序", navigationIcon = { - IconButton(onClick = onBack) { + IconButton(onClick = navigator.pop) { Icon( Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "返回" diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/SnackbarController.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/SnackbarController.kt index d23a244..4021f39 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/SnackbarController.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/SnackbarController.kt @@ -1,5 +1,6 @@ package io.github.miuzarte.scrcpyforandroid.services +import androidx.compose.runtime.staticCompositionLocalOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import top.yukonga.miuix.kmp.basic.SnackbarDuration @@ -29,3 +30,7 @@ class SnackbarController( } } } + +val LocalSnackbarController = staticCompositionLocalOf { + error("No SnackbarController provided") +} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt index 2d36775..f2ec1a4 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt @@ -76,7 +76,7 @@ import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperTextField import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec 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.Storage import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions @@ -352,7 +352,6 @@ internal fun VirtualButtonCard( @Composable internal fun ConfigPanel( busy: Boolean, - snackbar: SnackbarController, audioForwardingSupported: Boolean, cameraMirroringSupported: Boolean, adbConnecting: Boolean, @@ -366,6 +365,8 @@ internal fun ConfigPanel( ) { val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) } + val snackbar = LocalSnackbarController.current + val sessionStarted = sessionInfo != null val soBundleShared by scrcpyOptions.bundleState.collectAsState() diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/ReorderableList.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/ReorderableList.kt index de22c61..dad7ce2 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/ReorderableList.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/ReorderableList.kt @@ -20,7 +20,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp 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.ReorderableRow import top.yukonga.miuix.kmp.basic.Card @@ -50,7 +50,7 @@ class ReorderableList( @Composable operator fun invoke() { - val haptics = rememberAppHaptics() + val haptics = LocalAppHaptics.current val items = itemsProvider() when (orientation) { Orientation.Column -> { @@ -81,7 +81,7 @@ class ReorderableList( horizontalArrangement = Arrangement.spacedBy(UiSpacing.Small) ) { if (item.icon != null) Icon( - imageVector = item.icon, + item.icon, contentDescription = item.title ) Column { diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/StatusCardModels.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/StatusCardModels.kt index 2d15c3b..0833994 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/StatusCardModels.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/StatusCardModels.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp 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.CardDefaults.defaultColors import top.yukonga.miuix.kmp.basic.Icon @@ -67,7 +67,7 @@ internal fun StatusCardLayout( spec: StatusCardSpec, busyLabel: String?, ) { - val haptics = rememberAppHaptics() + val haptics = LocalAppHaptics.current Row( modifier = Modifier .fillMaxWidth() @@ -91,7 +91,7 @@ internal fun StatusCardLayout( contentAlignment = Alignment.BottomEnd, ) { Icon( - imageVector = spec.big.icon, + spec.big.icon, contentDescription = null, modifier = Modifier.size(170.dp), tint = spec.big.iconTint, @@ -153,7 +153,7 @@ internal fun StatusCardLayout( @Composable private fun StatusMetricCard(spec: StatusSmallCardSpec, modifier: Modifier) { - val haptics = rememberAppHaptics() + val haptics = LocalAppHaptics.current Card( modifier = modifier, insideMargin = PaddingValues(UiSpacing.Large), diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt index d53f005..1129273 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import io.github.miuzarte.scrcpyforandroid.constants.UiAndroidKeycodes 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 kotlinx.coroutines.launch import top.yukonga.miuix.kmp.basic.Button @@ -171,7 +171,7 @@ class VirtualButtonBar( onAction: (VirtualButtonAction) -> Unit, modifier: Modifier = Modifier, ) { - val haptics = rememberAppHaptics() + val haptics = LocalAppHaptics.current val activeContainerColor = MiuixTheme.colorScheme.primary val disabledContainerColor = MiuixTheme.colorScheme.primary.copy(alpha = 0.35f) val activeContentColor = MiuixTheme.colorScheme.onPrimary @@ -236,7 +236,7 @@ class VirtualButtonBar( modifier: Modifier = Modifier, ) { val scope = rememberCoroutineScope() - val haptics = rememberAppHaptics() + val haptics = LocalAppHaptics.current var showMorePopup by remember { mutableStateOf(false) } Row( @@ -293,7 +293,7 @@ class VirtualButtonBar( renderInRootScaffold: Boolean, ) { val scope = rememberCoroutineScope() - val haptics = rememberAppHaptics() + val haptics = LocalAppHaptics.current val spinnerItems = remember(moreActions) { moreActions.map { action -> SpinnerEntry(