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.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<AppHaptics> {
error("No AppHaptics provided")
}
@Composable
fun rememberAppHaptics(): AppHaptics {
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
@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 = "返回",
)
}

View File

@@ -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)
}
},
)

View File

@@ -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,

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.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<NavKey>(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() {
// 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<NavKey> {
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) {
CompositionLocalProvider(
LocalRootNavigator provides rootNavigator,
LocalSnackbarController provides snackbarController,
LocalAppHaptics provides haptics,
LocalFullscreenNavigationState provides fullscreenNavigationState,
LocalServerPicker provides serverPicker,
) {
NavDisplay(
entries = rootEntries,
onBack = ::popRoot,
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.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) }

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.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) },
)
}
}

View File

@@ -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 = "返回"

View File

@@ -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<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.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()

View File

@@ -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 {

View File

@@ -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),

View File

@@ -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(