feat: landscape layout
This commit is contained in:
@@ -54,8 +54,8 @@ android {
|
||||
applicationId = "io.github.miuzarte.scrcpyforandroid"
|
||||
minSdk = 26
|
||||
targetSdk = 37
|
||||
versionCode = 20
|
||||
versionName = "0.2.4"
|
||||
versionCode = 21
|
||||
versionName = "0.2.5"
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
@@ -120,6 +120,7 @@ dependencies {
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.material3.window.size)
|
||||
implementation(libs.androidx.compose.material.icons.extended)
|
||||
implementation(libs.androidx.navigation3.runtime)
|
||||
implementation(libs.material)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package io.github.miuzarte.scrcpyforandroid
|
||||
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
@@ -16,6 +19,7 @@ import kotlinx.coroutines.runBlocking
|
||||
class MainActivity : FragmentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
applyMainOrientationPolicy()
|
||||
|
||||
AppRuntime.init(applicationContext)
|
||||
AppScreenOn.register(window)
|
||||
@@ -39,6 +43,7 @@ class MainActivity : FragmentActivity() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
applyMainOrientationPolicy()
|
||||
StreamActivity.dismissActivePictureInPicture()
|
||||
}
|
||||
|
||||
@@ -46,4 +51,30 @@ class MainActivity : FragmentActivity() {
|
||||
AppScreenOn.unregister(window)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun applyMainOrientationPolicy() {
|
||||
val aspectRatio = currentDisplayAspectRatio()
|
||||
requestedOrientation =
|
||||
if (aspectRatio > PHONE_LANDSCAPE_LOCK_ASPECT_RATIO)
|
||||
ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
|
||||
else
|
||||
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
|
||||
private fun currentDisplayAspectRatio(): Float {
|
||||
val bounds =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
|
||||
windowManager.maximumWindowMetrics.bounds
|
||||
else resources.displayMetrics.let { metrics ->
|
||||
Rect(0, 0, metrics.widthPixels, metrics.heightPixels)
|
||||
}
|
||||
|
||||
val width = bounds.width().coerceAtLeast(1)
|
||||
val height = bounds.height().coerceAtLeast(1)
|
||||
return maxOf(width, height).toFloat() / minOf(width, height).toFloat()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val PHONE_LANDSCAPE_LOCK_ASPECT_RATIO = 16f / 9f
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ object NativeCoreFacade {
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private val bootstrapLock = Any()
|
||||
private val bootstrapPackets = ArrayDeque<CachedPacket>()
|
||||
|
||||
@Volatile
|
||||
private var recordingSurfaceAttached = false
|
||||
|
||||
@@ -111,9 +112,6 @@ object NativeCoreFacade {
|
||||
Log.i(TAG, "detachVideoSurface(): releasing decoder with destroyed surface")
|
||||
decoder?.release()
|
||||
decoder = null
|
||||
} else if (activeSurfaceId == null && !recordingSurfaceAttached) {
|
||||
decoder?.release()
|
||||
decoder = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,7 +180,6 @@ object NativeCoreFacade {
|
||||
)
|
||||
}
|
||||
val currentDecoder = decoder ?: return@attachVideoConsumer
|
||||
if (activeSurfaceId == null) return@attachVideoConsumer
|
||||
runCatching {
|
||||
currentDecoder.feedAnnexB(
|
||||
packet.data,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package io.github.miuzarte.scrcpyforandroid.nativecore
|
||||
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.opengl.EGLExt
|
||||
import android.opengl.EGL14
|
||||
import android.opengl.EGLConfig
|
||||
import android.opengl.EGLContext
|
||||
import android.opengl.EGLDisplay
|
||||
import android.opengl.EGLExt
|
||||
import android.opengl.EGLSurface
|
||||
import android.opengl.GLES11Ext
|
||||
import android.opengl.GLES20
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,11 +7,13 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
@@ -45,6 +47,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -132,6 +135,7 @@ fun FileManagerScreen(
|
||||
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
|
||||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
val listState = rememberLazyListState()
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
val asBundle by appSettings.bundleState.collectAsState()
|
||||
|
||||
val pathStack = remember {
|
||||
@@ -398,6 +402,89 @@ fun FileManagerScreen(
|
||||
}
|
||||
}
|
||||
|
||||
fun openEntry(entry: RemoteFileEntry) {
|
||||
when {
|
||||
entry.isDirectory -> {
|
||||
rememberCurrentScrollPosition()
|
||||
pathStack.add(normalizePath(entry.fullPath))
|
||||
}
|
||||
|
||||
entry.kind == RemoteFileKind.Link || entry.symlinkTarget != null -> {
|
||||
taskScope.launch {
|
||||
val targetPath = resolveLinkTarget(entry)
|
||||
if (targetPath == null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
snackbar.show("链接目标不可用,长按查看信息")
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
val result = runCatching {
|
||||
FileManagerService.stat(targetPath)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
result.onSuccess { targetStat ->
|
||||
if (isDirectoryStat(targetStat)) {
|
||||
jumpToPath(targetPath)
|
||||
} else {
|
||||
snackbar.show("链接目标不是文件夹,长按查看信息")
|
||||
}
|
||||
}.onFailure { error ->
|
||||
snackbar.show("读取链接目标失败: ${error.message ?: error.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> snackbar.show("长按可查看文件详情")
|
||||
}
|
||||
}
|
||||
|
||||
fun showEntryDetails(entry: RemoteFileEntry) {
|
||||
clearDetails()
|
||||
selectedEntry = entry
|
||||
showDetailsSheet = true
|
||||
detailLoading = true
|
||||
taskScope.launch {
|
||||
val statResult = runCatching {
|
||||
FileManagerService.stat(entry.fullPath)
|
||||
}
|
||||
val linkTargetPath = resolveLinkTarget(entry)
|
||||
val targetStatResult =
|
||||
if (linkTargetPath != null)
|
||||
runCatching { FileManagerService.stat(linkTargetPath) }
|
||||
else null
|
||||
|
||||
val snapshotResult =
|
||||
if (entry.isDirectory)
|
||||
runCatching {
|
||||
val session = DirectorySnapshotSession.open()
|
||||
withContext(Dispatchers.Main) {
|
||||
activeSnapshotSession = session
|
||||
}
|
||||
session.load(entry.fullPath)
|
||||
}
|
||||
else null
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
detailLoading = false
|
||||
statResult
|
||||
.onSuccess { selectedStat = it }
|
||||
.onFailure { error ->
|
||||
snackbar.show("读取详情失败: ${error.message ?: error.javaClass.simpleName}")
|
||||
if (selectedEntry === entry)
|
||||
selectedEntry = null
|
||||
}
|
||||
targetStatResult
|
||||
?.onSuccess { selectedTargetStat = it }
|
||||
snapshotResult
|
||||
?.onSuccess { selectedSnapshot = it }
|
||||
?.onFailure { error ->
|
||||
snackbar.show("目录扫描失败: ${error.message ?: error.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
BlurredBar(backdrop = blurBackdrop) {
|
||||
@@ -546,104 +633,78 @@ fun FileManagerScreen(
|
||||
top = pagePadding.calculateTopPadding() + 12.dp,
|
||||
),
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = pagePadding,
|
||||
bottomInnerPadding = bottomInnerPadding,
|
||||
state = listState,
|
||||
) {
|
||||
if (loading && cachedEntries == null) {
|
||||
item { FileManagerStatusCard("加载中") }
|
||||
} else if (errorText != null && cachedEntries == null) {
|
||||
item { FileManagerStatusCard("加载失败: $errorText") }
|
||||
} else if (displayedEntries.isEmpty()) {
|
||||
item { FileManagerStatusCard("空目录") }
|
||||
} else {
|
||||
items(displayedEntries) { entry ->
|
||||
FileManagerItemCard(
|
||||
entry = entry,
|
||||
summary = FileManagerService.formatSummary(entry),
|
||||
onClick = {
|
||||
when {
|
||||
entry.isDirectory -> {
|
||||
rememberCurrentScrollPosition()
|
||||
pathStack.add(normalizePath(entry.fullPath))
|
||||
}
|
||||
BoxWithConstraints(Modifier.fillMaxWidth()) {
|
||||
val fileCardMinWidth = 220.dp
|
||||
val listHorizontalPadding =
|
||||
pagePadding.calculateLeftPadding(layoutDirection) +
|
||||
pagePadding.calculateRightPadding(layoutDirection) +
|
||||
UiSpacing.PageHorizontal * 2
|
||||
val availableListWidth =
|
||||
(maxWidth - listHorizontalPadding).coerceAtLeast(fileCardMinWidth)
|
||||
val columns = (
|
||||
(availableListWidth.value + UiSpacing.PageItem.value) /
|
||||
(fileCardMinWidth.value + UiSpacing.PageItem.value)
|
||||
).toInt().coerceAtLeast(1)
|
||||
val fileRows = remember(displayedEntries, columns) {
|
||||
displayedEntries.chunked(columns)
|
||||
}
|
||||
|
||||
entry.kind == RemoteFileKind.Link || entry.symlinkTarget != null -> {
|
||||
taskScope.launch {
|
||||
val targetPath = resolveLinkTarget(entry)
|
||||
if (targetPath == null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
snackbar.show("链接目标不可用,长按查看信息")
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
val result = runCatching {
|
||||
FileManagerService.stat(targetPath)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
result.onSuccess { targetStat ->
|
||||
if (isDirectoryStat(targetStat)) {
|
||||
jumpToPath(targetPath)
|
||||
} else {
|
||||
snackbar.show("链接目标不是文件夹,长按查看信息")
|
||||
}
|
||||
}.onFailure { error ->
|
||||
snackbar.show("读取链接目标失败: ${error.message ?: error.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
fun FileStateContent() {
|
||||
when {
|
||||
loading && cachedEntries == null ->
|
||||
FileManagerStatusCard(
|
||||
message = "加载中",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
else -> snackbar.show("长按可查看文件详情")
|
||||
errorText != null && cachedEntries == null ->
|
||||
FileManagerStatusCard(
|
||||
message = "加载失败: $errorText",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
displayedEntries.isEmpty() ->
|
||||
FileManagerStatusCard(
|
||||
message = "空目录",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = pagePadding,
|
||||
bottomInnerPadding = bottomInnerPadding,
|
||||
state = listState,
|
||||
limitLandscapeWidth = false,
|
||||
) {
|
||||
if (loading && cachedEntries == null ||
|
||||
errorText != null && cachedEntries == null ||
|
||||
displayedEntries.isEmpty()
|
||||
) {
|
||||
item { FileStateContent() }
|
||||
} else {
|
||||
items(fileRows) { rowEntries ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(UiSpacing.PageItem),
|
||||
) {
|
||||
rowEntries.forEach { entry ->
|
||||
FileManagerItemCard(
|
||||
entry = entry,
|
||||
summary = FileManagerService.formatSummary(entry),
|
||||
onClick = { openEntry(entry) },
|
||||
onLongClick = { showEntryDetails(entry) },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(72.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
clearDetails()
|
||||
selectedEntry = entry
|
||||
showDetailsSheet = true
|
||||
detailLoading = true
|
||||
taskScope.launch {
|
||||
val statResult = runCatching {
|
||||
FileManagerService.stat(entry.fullPath)
|
||||
}
|
||||
val linkTargetPath = resolveLinkTarget(entry)
|
||||
val targetStatResult =
|
||||
if (linkTargetPath != null)
|
||||
runCatching { FileManagerService.stat(linkTargetPath) }
|
||||
else null
|
||||
|
||||
val snapshotResult =
|
||||
if (entry.isDirectory)
|
||||
runCatching {
|
||||
val session = DirectorySnapshotSession.open()
|
||||
withContext(Dispatchers.Main) {
|
||||
activeSnapshotSession = session
|
||||
}
|
||||
session.load(entry.fullPath)
|
||||
}
|
||||
else null
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
detailLoading = false
|
||||
statResult
|
||||
.onSuccess { selectedStat = it }
|
||||
.onFailure { error ->
|
||||
snackbar.show("读取详情失败: ${error.message ?: error.javaClass.simpleName}")
|
||||
if (selectedEntry === entry)
|
||||
selectedEntry = null
|
||||
}
|
||||
targetStatResult
|
||||
?.onSuccess { selectedTargetStat = it }
|
||||
snapshotResult
|
||||
?.onSuccess { selectedSnapshot = it }
|
||||
?.onFailure { error ->
|
||||
snackbar.show("目录扫描失败: ${error.message ?: error.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
repeat(columns - rowEntries.size) {
|
||||
Box(Modifier.weight(1f))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -719,8 +780,11 @@ fun FileManagerScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FileManagerStatusCard(message: String) {
|
||||
Card {
|
||||
private fun FileManagerStatusCard(
|
||||
message: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Card(modifier = modifier) {
|
||||
Text(
|
||||
text = message,
|
||||
modifier = Modifier
|
||||
@@ -737,10 +801,10 @@ private fun FileManagerItemCard(
|
||||
summary: String,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
modifier = modifier
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
@@ -749,7 +813,8 @@ private fun FileManagerItemCard(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
.fillMaxHeight()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.Surface
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.LocalActivity
|
||||
|
||||
@@ -58,12 +58,11 @@ import com.kyant.backdrop.backdrops.layerBackdrop
|
||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||
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.nativecore.NativeAdbService
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
|
||||
import io.github.miuzarte.scrcpyforandroid.services.AppScreenOn
|
||||
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
|
||||
import io.github.miuzarte.scrcpyforandroid.services.AppScreenOn
|
||||
import io.github.miuzarte.scrcpyforandroid.services.AppUpdateChecker
|
||||
import io.github.miuzarte.scrcpyforandroid.services.ConnectionController
|
||||
import io.github.miuzarte.scrcpyforandroid.services.ConnectionStateStore
|
||||
@@ -75,13 +74,13 @@ import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.Settings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage.quickDevices
|
||||
import io.github.miuzarte.scrcpyforandroid.ui.createThemeController
|
||||
import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar
|
||||
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
|
||||
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableFloatingBottomBar
|
||||
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableFloatingBottomBarBlur
|
||||
import io.github.miuzarte.scrcpyforandroid.ui.component.FloatingBottomBar
|
||||
import io.github.miuzarte.scrcpyforandroid.ui.component.FloatingBottomBarItem
|
||||
import io.github.miuzarte.scrcpyforandroid.ui.createThemeController
|
||||
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -101,8 +100,8 @@ import top.yukonga.miuix.kmp.basic.SnackbarHostState
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
||||
import top.yukonga.miuix.kmp.blur.layerBackdrop as miuixLayerBackdrop
|
||||
import java.io.File
|
||||
import top.yukonga.miuix.kmp.blur.layerBackdrop as miuixLayerBackdrop
|
||||
|
||||
private const val TERMINAL_FONT_RELATIVE_PATH = "terminal/font.ttf"
|
||||
|
||||
@@ -191,6 +190,7 @@ fun MainScreen() {
|
||||
var fileTabCanNavigateUp by remember { mutableStateOf(false) }
|
||||
var fileTabNavigateUp by remember { mutableStateOf<(() -> Boolean)?>(null) }
|
||||
var terminalGestureLock by remember { mutableStateOf(false) }
|
||||
var devicePreviewGestureLock by remember { mutableStateOf(false) }
|
||||
|
||||
// Scroll behaviors
|
||||
val devicesPageScrollBehavior = MiuixScrollBehavior(
|
||||
@@ -538,6 +538,12 @@ fun MainScreen() {
|
||||
.fillMaxSize()
|
||||
.then(if (blurBackdrop != null) Modifier.miuixLayerBackdrop(blurBackdrop) else Modifier),
|
||||
) {
|
||||
val pagerGestureLocked =
|
||||
selectedTabIndex == MainBottomTabDestination.Terminal.ordinal
|
||||
&& terminalGestureLock
|
||||
|| selectedTabIndex == MainBottomTabDestination.Devices.ordinal
|
||||
&& devicePreviewGestureLock
|
||||
|
||||
HorizontalPager(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -550,7 +556,7 @@ fun MainScreen() {
|
||||
),
|
||||
state = pagerState,
|
||||
beyondViewportPageCount = 1,
|
||||
userScrollEnabled = !(selectedTabIndex == MainBottomTabDestination.Terminal.ordinal && terminalGestureLock),
|
||||
userScrollEnabled = !pagerGestureLocked,
|
||||
) { page ->
|
||||
val tab = tabs[page]
|
||||
saveableStateHolder.SaveableStateProvider(tab.name) {
|
||||
@@ -561,6 +567,9 @@ fun MainScreen() {
|
||||
connectionServices = deviceConnectionServices,
|
||||
bottomInnerPadding = bottomInnerPadding,
|
||||
onOpenReorderDevices = { showReorderDevices = true },
|
||||
onPreviewGestureLockChanged = { locked ->
|
||||
devicePreviewGestureLock = locked
|
||||
},
|
||||
)
|
||||
|
||||
MainBottomTabDestination.Terminal -> TerminalScreen(
|
||||
|
||||
@@ -14,7 +14,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.github.miuzarte.scrcpyforandroid.StreamActivity
|
||||
import io.github.miuzarte.scrcpyforandroid.constants.ThemeModes
|
||||
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
|
||||
import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController
|
||||
import io.github.miuzarte.scrcpyforandroid.services.SnackbarController
|
||||
|
||||
@@ -2,15 +2,20 @@ package io.github.miuzarte.scrcpyforandroid.scaffolds
|
||||
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
@@ -33,6 +38,8 @@ fun LazyColumn(
|
||||
verticalPadding: Dp = UiSpacing.PageVertical,
|
||||
bottomInnerPadding: Dp? = null,
|
||||
clearFocusOnTap: Boolean = true,
|
||||
limitLandscapeWidth: Boolean = true,
|
||||
landscapeMaxWidth: Dp = 640.dp,
|
||||
content: LazyListScope.() -> Unit,
|
||||
) {
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
@@ -45,30 +52,45 @@ fun LazyColumn(
|
||||
bottom = contentPadding.calculateBottomPadding() + verticalPadding,
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.then(
|
||||
if (clearFocusOnTap)
|
||||
Modifier.pointerInput(Unit) {
|
||||
detectTapGestures(onTap = { focusManager.clearFocus() })
|
||||
} else Modifier
|
||||
)
|
||||
.overScrollVertical()
|
||||
.then(
|
||||
if (scrollBehavior != null)
|
||||
Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
}
|
||||
else Modifier
|
||||
),
|
||||
state = state,
|
||||
contentPadding = mergedContentPadding,
|
||||
verticalArrangement = Arrangement.spacedBy(itemSpacing),
|
||||
) {
|
||||
content()
|
||||
bottomInnerPadding?.let { padding ->
|
||||
(padding - itemSpacing)
|
||||
.takeIf { it > 0.dp }
|
||||
?.let { item { Spacer(Modifier.height(it)) } }
|
||||
val contentWidthModifier =
|
||||
if (limitLandscapeWidth && maxWidth > maxHeight) Modifier.widthIn(max = landscapeMaxWidth)
|
||||
else Modifier.fillMaxWidth()
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.TopCenter,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = contentWidthModifier
|
||||
.fillMaxSize()
|
||||
.overScrollVertical()
|
||||
.then(
|
||||
if (scrollBehavior != null)
|
||||
Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
else Modifier
|
||||
),
|
||||
state = state,
|
||||
contentPadding = mergedContentPadding,
|
||||
verticalArrangement = Arrangement.spacedBy(itemSpacing),
|
||||
) {
|
||||
content()
|
||||
bottomInnerPadding?.let { padding ->
|
||||
(padding - itemSpacing)
|
||||
.takeIf { it > 0.dp }
|
||||
?.let { item { Spacer(Modifier.height(it)) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,10 +99,13 @@ class Scrcpy(
|
||||
|
||||
@Volatile
|
||||
private var audioPlayer: ScrcpyAudioPlayer? = null
|
||||
|
||||
@Volatile
|
||||
private var mp4Recorder: NativeMp4Recorder? = null
|
||||
|
||||
@Volatile
|
||||
private var wavRecorder: NativeWavRecorder? = null
|
||||
|
||||
@Volatile
|
||||
private var aacRecorder: NativeAacRecorder? = null
|
||||
|
||||
@@ -228,8 +231,7 @@ class Scrcpy(
|
||||
val recordFile = RecordingFileResolver.resolve(options, info)
|
||||
when (options.recordFormat) {
|
||||
ClientOptions.RecordFormat.MP4,
|
||||
ClientOptions.RecordFormat.M4A,
|
||||
-> {
|
||||
ClientOptions.RecordFormat.M4A -> {
|
||||
val recorder = NativeMp4Recorder(
|
||||
outputFile = recordFile,
|
||||
includeVideo = options.video,
|
||||
|
||||
@@ -330,7 +330,15 @@ class TouchEventHandler(
|
||||
justPressedPointerIds += pointerId
|
||||
coroutineScope.launch {
|
||||
runCatching {
|
||||
onInjectTouch(UiMotionActions.DOWN, pointerId.toLong(), x, y, pressure, 0, 0)
|
||||
onInjectTouch(
|
||||
UiMotionActions.DOWN,
|
||||
pointerId.toLong(),
|
||||
x,
|
||||
y,
|
||||
pressure,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
}.onFailure { e ->
|
||||
Log.w(
|
||||
FULLSCREEN_TOUCH_LOG_TAG,
|
||||
|
||||
@@ -21,7 +21,9 @@ internal class DeviceAdbAutoReconnectManager(
|
||||
isForeground = isForeground,
|
||||
intervalMs = intervalMs,
|
||||
keepAliveCheck = { _, _ -> controller.keepAliveCheck(keepAliveTimeoutMs) },
|
||||
reconnect = { host, port -> controller.connectWithTimeout(host, port, connectTimeoutMs) },
|
||||
reconnect = { host, port ->
|
||||
controller.connectWithTimeout(host, port, connectTimeoutMs)
|
||||
},
|
||||
onReconnectSuccess = { host, port ->
|
||||
controller.markKeepAliveReconnectSuccess(host, port)
|
||||
onReconnectSuccess(host, port)
|
||||
|
||||
@@ -207,6 +207,7 @@ class NativeAudioRecorder(
|
||||
muxer.start()
|
||||
muxerStarted = true
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (index < 0) continue
|
||||
val outputBuffer = currentEncoder.getOutputBuffer(index)
|
||||
|
||||
@@ -294,6 +294,7 @@ class NativeMp4Recorder(
|
||||
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
|
||||
registerTrackLocked(TrackType.AUDIO, currentEncoder.outputFormat)
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (index < 0) continue
|
||||
val outputBuffer = currentEncoder.getOutputBuffer(index)
|
||||
@@ -329,6 +330,7 @@ class NativeMp4Recorder(
|
||||
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
|
||||
registerTrackLocked(TrackType.VIDEO, currentEncoder.outputFormat)
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (index < 0) continue
|
||||
val outputBuffer = currentEncoder.getOutputBuffer(index)
|
||||
@@ -362,6 +364,7 @@ class NativeMp4Recorder(
|
||||
if (audioTrackIndex >= 0) return
|
||||
audioTrackIndex = muxer.addTrack(format)
|
||||
}
|
||||
|
||||
TrackType.VIDEO -> {
|
||||
if (videoTrackIndex >= 0) return
|
||||
videoTrackIndex = muxer.addTrack(format)
|
||||
@@ -451,6 +454,7 @@ class NativeMp4Recorder(
|
||||
val normalized = (presentationTimeUs - base).coerceAtLeast(0L)
|
||||
normalized.coerceAtLeast(lastAudioPtsUs).also { lastAudioPtsUs = it }
|
||||
}
|
||||
|
||||
TrackType.VIDEO -> {
|
||||
val base = videoBasePtsUs ?: presentationTimeUs.also {
|
||||
videoBasePtsUs = it
|
||||
|
||||
@@ -122,6 +122,10 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
|
||||
intPreferencesKey("device_preview_card_height_dp"),
|
||||
1080 / 3,
|
||||
)
|
||||
val DEVICE_TWO_PANE_CONFIG_ON_RIGHT = Pair(
|
||||
booleanPreferencesKey("device_two_pane_config_on_right"),
|
||||
false,
|
||||
)
|
||||
val FULLSCREEN_CONTROL_IGNORE_SYSTEM_ROTATION_LOCK = Pair(
|
||||
booleanPreferencesKey("fullscreen_control_ignore_system_rotation_lock"),
|
||||
true,
|
||||
@@ -274,6 +278,7 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
|
||||
val fullscreenDebugInfo: Boolean,
|
||||
val hideSimpleConfigItems: Boolean,
|
||||
val devicePreviewCardHeightDp: Int,
|
||||
val deviceTwoPaneConfigOnRight: Boolean,
|
||||
val fullscreenControlIgnoreSystemRotationLock: Boolean,
|
||||
val showFullscreenVirtualButtons: Boolean,
|
||||
val fullscreenVirtualButtonHeightDp: Int,
|
||||
@@ -330,6 +335,7 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
|
||||
bundleField(FULLSCREEN_DEBUG_INFO) { it.fullscreenDebugInfo },
|
||||
bundleField(HIDE_SIMPLE_CONFIG_ITEMS) { it.hideSimpleConfigItems },
|
||||
bundleField(DEVICE_PREVIEW_CARD_HEIGHT_DP) { it.devicePreviewCardHeightDp },
|
||||
bundleField(DEVICE_TWO_PANE_CONFIG_ON_RIGHT) { it.deviceTwoPaneConfigOnRight },
|
||||
bundleField(FULLSCREEN_CONTROL_IGNORE_SYSTEM_ROTATION_LOCK) { it.fullscreenControlIgnoreSystemRotationLock },
|
||||
bundleField(SHOW_FULLSCREEN_VIRTUAL_BUTTONS) { it.showFullscreenVirtualButtons },
|
||||
bundleField(FULLSCREEN_VIRTUAL_BUTTON_HEIGHT_DP) { it.fullscreenVirtualButtonHeightDp },
|
||||
@@ -387,6 +393,7 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
|
||||
fullscreenDebugInfo = preferences.read(FULLSCREEN_DEBUG_INFO),
|
||||
hideSimpleConfigItems = preferences.read(HIDE_SIMPLE_CONFIG_ITEMS),
|
||||
devicePreviewCardHeightDp = preferences.read(DEVICE_PREVIEW_CARD_HEIGHT_DP),
|
||||
deviceTwoPaneConfigOnRight = preferences.read(DEVICE_TWO_PANE_CONFIG_ON_RIGHT),
|
||||
fullscreenControlIgnoreSystemRotationLock =
|
||||
preferences.read(FULLSCREEN_CONTROL_IGNORE_SYSTEM_ROTATION_LOCK),
|
||||
showFullscreenVirtualButtons = preferences.read(SHOW_FULLSCREEN_VIRTUAL_BUTTONS),
|
||||
|
||||
@@ -306,67 +306,250 @@ class ScrcpyProfiles(context: Context) : Settings(context, "ScrcpyProfiles") {
|
||||
val defaults = ScrcpyOptions.defaultBundle()
|
||||
val json = bundleJson ?: return defaults
|
||||
return defaults.copy(
|
||||
crop = json.optStringOrDefault("crop", defaults.crop),
|
||||
recordFilename = json.optStringOrDefault("recordFilename", defaults.recordFilename),
|
||||
videoCodecOptions = json.optStringOrDefault("videoCodecOptions", defaults.videoCodecOptions),
|
||||
audioCodecOptions = json.optStringOrDefault("audioCodecOptions", defaults.audioCodecOptions),
|
||||
videoEncoder = json.optStringOrDefault("videoEncoder", defaults.videoEncoder),
|
||||
audioEncoder = json.optStringOrDefault("audioEncoder", defaults.audioEncoder),
|
||||
cameraId = json.optStringOrDefault("cameraId", defaults.cameraId),
|
||||
cameraSize = json.optStringOrDefault("cameraSize", defaults.cameraSize),
|
||||
cameraSizeCustom = json.optStringOrDefault("cameraSizeCustom", defaults.cameraSizeCustom),
|
||||
cameraSizeUseCustom = json.optBooleanOrDefault("cameraSizeUseCustom", defaults.cameraSizeUseCustom),
|
||||
cameraAr = json.optStringOrDefault("cameraAr", defaults.cameraAr),
|
||||
cameraFps = json.optIntOrDefault("cameraFps", defaults.cameraFps),
|
||||
logLevel = json.optStringOrDefault("logLevel", defaults.logLevel),
|
||||
videoCodec = json.optStringOrDefault("videoCodec", defaults.videoCodec),
|
||||
audioCodec = json.optStringOrDefault("audioCodec", defaults.audioCodec),
|
||||
videoSource = json.optStringOrDefault("videoSource", defaults.videoSource),
|
||||
audioSource = json.optStringOrDefault("audioSource", defaults.audioSource),
|
||||
recordFormat = json.optStringOrDefault("recordFormat", defaults.recordFormat),
|
||||
cameraFacing = json.optStringOrDefault("cameraFacing", defaults.cameraFacing),
|
||||
maxSize = json.optIntOrDefault("maxSize", defaults.maxSize),
|
||||
videoBitRate = json.optIntOrDefault("videoBitRate", defaults.videoBitRate),
|
||||
audioBitRate = json.optIntOrDefault("audioBitRate", defaults.audioBitRate),
|
||||
maxFps = json.optStringOrDefault("maxFps", defaults.maxFps),
|
||||
angle = json.optStringOrDefault("angle", defaults.angle),
|
||||
captureOrientation = json.optIntOrDefault("captureOrientation", defaults.captureOrientation),
|
||||
captureOrientationLock = json.optStringOrDefault("captureOrientationLock", defaults.captureOrientationLock),
|
||||
displayOrientation = json.optIntOrDefault("displayOrientation", defaults.displayOrientation),
|
||||
recordOrientation = json.optIntOrDefault("recordOrientation", defaults.recordOrientation),
|
||||
displayImePolicy = json.optStringOrDefault("displayImePolicy", defaults.displayImePolicy),
|
||||
displayId = json.optIntOrDefault("displayId", defaults.displayId),
|
||||
screenOffTimeout = json.optLongOrDefault("screenOffTimeout", defaults.screenOffTimeout),
|
||||
showTouches = json.optBooleanOrDefault("showTouches", defaults.showTouches),
|
||||
fullscreen = json.optBooleanOrDefault("fullscreen", defaults.fullscreen),
|
||||
control = json.optBooleanOrDefault("control", defaults.control),
|
||||
videoPlayback = json.optBooleanOrDefault("videoPlayback", defaults.videoPlayback),
|
||||
audioPlayback = json.optBooleanOrDefault("audioPlayback", defaults.audioPlayback),
|
||||
turnScreenOff = json.optBooleanOrDefault("turnScreenOff", defaults.turnScreenOff),
|
||||
keyInjectMode = json.optStringOrDefault("keyInjectMode", defaults.keyInjectMode),
|
||||
forwardKeyRepeat = json.optBooleanOrDefault("forwardKeyRepeat", defaults.forwardKeyRepeat),
|
||||
stayAwake = json.optBooleanOrDefault("stayAwake", defaults.stayAwake),
|
||||
disableScreensaver = json.optBooleanOrDefault("disableScreensaver", defaults.disableScreensaver),
|
||||
powerOffOnClose = json.optBooleanOrDefault("powerOffOnClose", defaults.powerOffOnClose),
|
||||
legacyPaste = json.optBooleanOrDefault("legacyPaste", defaults.legacyPaste),
|
||||
clipboardAutosync = json.optBooleanOrDefault("clipboardAutosync", defaults.clipboardAutosync),
|
||||
downsizeOnError = json.optBooleanOrDefault("downsizeOnError", defaults.downsizeOnError),
|
||||
mouseHover = json.optBooleanOrDefault("mouseHover", defaults.mouseHover),
|
||||
cleanup = json.optBooleanOrDefault("cleanup", defaults.cleanup),
|
||||
powerOn = json.optBooleanOrDefault("powerOn", defaults.powerOn),
|
||||
video = json.optBooleanOrDefault("video", defaults.video),
|
||||
audio = json.optBooleanOrDefault("audio", defaults.audio),
|
||||
requireAudio = json.optBooleanOrDefault("requireAudio", defaults.requireAudio),
|
||||
killAdbOnClose = json.optBooleanOrDefault("killAdbOnClose", defaults.killAdbOnClose),
|
||||
cameraHighSpeed = json.optBooleanOrDefault("cameraHighSpeed", defaults.cameraHighSpeed),
|
||||
list = json.optStringOrDefault("list", defaults.list),
|
||||
audioDup = json.optBooleanOrDefault("audioDup", defaults.audioDup),
|
||||
newDisplay = json.optStringOrDefault("newDisplay", defaults.newDisplay),
|
||||
startApp = json.optStringOrDefault("startApp", defaults.startApp),
|
||||
startAppCustom = json.optStringOrDefault("startAppCustom", defaults.startAppCustom),
|
||||
startAppUseCustom = json.optBooleanOrDefault("startAppUseCustom", defaults.startAppUseCustom),
|
||||
vdDestroyContent = json.optBooleanOrDefault("vdDestroyContent", defaults.vdDestroyContent),
|
||||
vdSystemDecorations = json.optBooleanOrDefault("vdSystemDecorations", defaults.vdSystemDecorations),
|
||||
crop = json.optStringOrDefault(
|
||||
"crop",
|
||||
defaults.crop,
|
||||
),
|
||||
recordFilename = json.optStringOrDefault(
|
||||
"recordFilename",
|
||||
defaults.recordFilename,
|
||||
),
|
||||
videoCodecOptions = json.optStringOrDefault(
|
||||
"videoCodecOptions",
|
||||
defaults.videoCodecOptions,
|
||||
),
|
||||
audioCodecOptions = json.optStringOrDefault(
|
||||
"audioCodecOptions",
|
||||
defaults.audioCodecOptions,
|
||||
),
|
||||
videoEncoder = json.optStringOrDefault(
|
||||
"videoEncoder",
|
||||
defaults.videoEncoder,
|
||||
),
|
||||
audioEncoder = json.optStringOrDefault(
|
||||
"audioEncoder",
|
||||
defaults.audioEncoder,
|
||||
),
|
||||
cameraId = json.optStringOrDefault(
|
||||
"cameraId",
|
||||
defaults.cameraId,
|
||||
),
|
||||
cameraSize = json.optStringOrDefault(
|
||||
"cameraSize",
|
||||
defaults.cameraSize,
|
||||
),
|
||||
cameraSizeCustom = json.optStringOrDefault(
|
||||
"cameraSizeCustom",
|
||||
defaults.cameraSizeCustom,
|
||||
),
|
||||
cameraSizeUseCustom = json.optBooleanOrDefault(
|
||||
"cameraSizeUseCustom",
|
||||
defaults.cameraSizeUseCustom,
|
||||
),
|
||||
cameraAr = json.optStringOrDefault(
|
||||
"cameraAr",
|
||||
defaults.cameraAr,
|
||||
),
|
||||
cameraFps = json.optIntOrDefault(
|
||||
"cameraFps",
|
||||
defaults.cameraFps,
|
||||
),
|
||||
logLevel = json.optStringOrDefault(
|
||||
"logLevel",
|
||||
defaults.logLevel,
|
||||
),
|
||||
videoCodec = json.optStringOrDefault(
|
||||
"videoCodec",
|
||||
defaults.videoCodec,
|
||||
),
|
||||
audioCodec = json.optStringOrDefault(
|
||||
"audioCodec",
|
||||
defaults.audioCodec,
|
||||
),
|
||||
videoSource = json.optStringOrDefault(
|
||||
"videoSource",
|
||||
defaults.videoSource,
|
||||
),
|
||||
audioSource = json.optStringOrDefault(
|
||||
"audioSource",
|
||||
defaults.audioSource,
|
||||
),
|
||||
recordFormat = json.optStringOrDefault(
|
||||
"recordFormat",
|
||||
defaults.recordFormat,
|
||||
),
|
||||
cameraFacing = json.optStringOrDefault(
|
||||
"cameraFacing",
|
||||
defaults.cameraFacing,
|
||||
),
|
||||
maxSize = json.optIntOrDefault(
|
||||
"maxSize",
|
||||
defaults.maxSize,
|
||||
),
|
||||
videoBitRate = json.optIntOrDefault(
|
||||
"videoBitRate",
|
||||
defaults.videoBitRate,
|
||||
),
|
||||
audioBitRate = json.optIntOrDefault(
|
||||
"audioBitRate",
|
||||
defaults.audioBitRate,
|
||||
),
|
||||
maxFps = json.optStringOrDefault(
|
||||
"maxFps",
|
||||
defaults.maxFps,
|
||||
),
|
||||
angle = json.optStringOrDefault(
|
||||
"angle",
|
||||
defaults.angle,
|
||||
),
|
||||
captureOrientation = json.optIntOrDefault(
|
||||
"captureOrientation",
|
||||
defaults.captureOrientation,
|
||||
),
|
||||
captureOrientationLock = json.optStringOrDefault(
|
||||
"captureOrientationLock",
|
||||
defaults.captureOrientationLock,
|
||||
),
|
||||
displayOrientation = json.optIntOrDefault(
|
||||
"displayOrientation",
|
||||
defaults.displayOrientation,
|
||||
),
|
||||
recordOrientation = json.optIntOrDefault(
|
||||
"recordOrientation",
|
||||
defaults.recordOrientation,
|
||||
),
|
||||
displayImePolicy = json.optStringOrDefault(
|
||||
"displayImePolicy",
|
||||
defaults.displayImePolicy,
|
||||
),
|
||||
displayId = json.optIntOrDefault(
|
||||
"displayId",
|
||||
defaults.displayId,
|
||||
),
|
||||
screenOffTimeout = json.optLongOrDefault(
|
||||
"screenOffTimeout",
|
||||
defaults.screenOffTimeout,
|
||||
),
|
||||
showTouches = json.optBooleanOrDefault(
|
||||
"showTouches",
|
||||
defaults.showTouches,
|
||||
),
|
||||
fullscreen = json.optBooleanOrDefault(
|
||||
"fullscreen",
|
||||
defaults.fullscreen,
|
||||
),
|
||||
control = json.optBooleanOrDefault(
|
||||
"control",
|
||||
defaults.control,
|
||||
),
|
||||
videoPlayback = json.optBooleanOrDefault(
|
||||
"videoPlayback",
|
||||
defaults.videoPlayback,
|
||||
),
|
||||
audioPlayback = json.optBooleanOrDefault(
|
||||
"audioPlayback",
|
||||
defaults.audioPlayback,
|
||||
),
|
||||
turnScreenOff = json.optBooleanOrDefault(
|
||||
"turnScreenOff",
|
||||
defaults.turnScreenOff,
|
||||
),
|
||||
keyInjectMode = json.optStringOrDefault(
|
||||
"keyInjectMode",
|
||||
defaults.keyInjectMode,
|
||||
),
|
||||
forwardKeyRepeat = json.optBooleanOrDefault(
|
||||
"forwardKeyRepeat",
|
||||
defaults.forwardKeyRepeat,
|
||||
),
|
||||
stayAwake = json.optBooleanOrDefault(
|
||||
"stayAwake",
|
||||
defaults.stayAwake,
|
||||
),
|
||||
disableScreensaver = json.optBooleanOrDefault(
|
||||
"disableScreensaver",
|
||||
defaults.disableScreensaver,
|
||||
),
|
||||
powerOffOnClose = json.optBooleanOrDefault(
|
||||
"powerOffOnClose",
|
||||
defaults.powerOffOnClose,
|
||||
),
|
||||
legacyPaste = json.optBooleanOrDefault(
|
||||
"legacyPaste",
|
||||
defaults.legacyPaste,
|
||||
),
|
||||
clipboardAutosync = json.optBooleanOrDefault(
|
||||
"clipboardAutosync",
|
||||
defaults.clipboardAutosync,
|
||||
),
|
||||
downsizeOnError = json.optBooleanOrDefault(
|
||||
"downsizeOnError",
|
||||
defaults.downsizeOnError,
|
||||
),
|
||||
mouseHover = json.optBooleanOrDefault(
|
||||
"mouseHover",
|
||||
defaults.mouseHover,
|
||||
),
|
||||
cleanup = json.optBooleanOrDefault(
|
||||
"cleanup",
|
||||
defaults.cleanup,
|
||||
),
|
||||
powerOn = json.optBooleanOrDefault(
|
||||
"powerOn",
|
||||
defaults.powerOn,
|
||||
),
|
||||
video = json.optBooleanOrDefault(
|
||||
"video",
|
||||
defaults.video,
|
||||
),
|
||||
audio = json.optBooleanOrDefault(
|
||||
"audio",
|
||||
defaults.audio,
|
||||
),
|
||||
requireAudio = json.optBooleanOrDefault(
|
||||
"requireAudio",
|
||||
defaults.requireAudio,
|
||||
),
|
||||
killAdbOnClose = json.optBooleanOrDefault(
|
||||
"killAdbOnClose",
|
||||
defaults.killAdbOnClose,
|
||||
),
|
||||
cameraHighSpeed = json.optBooleanOrDefault(
|
||||
"cameraHighSpeed",
|
||||
defaults.cameraHighSpeed,
|
||||
),
|
||||
list = json.optStringOrDefault(
|
||||
"list",
|
||||
defaults.list,
|
||||
),
|
||||
audioDup = json.optBooleanOrDefault(
|
||||
"audioDup",
|
||||
defaults.audioDup,
|
||||
),
|
||||
newDisplay = json.optStringOrDefault(
|
||||
"newDisplay",
|
||||
defaults.newDisplay,
|
||||
),
|
||||
startApp = json.optStringOrDefault(
|
||||
"startApp",
|
||||
defaults.startApp,
|
||||
),
|
||||
startAppCustom = json.optStringOrDefault(
|
||||
"startAppCustom",
|
||||
defaults.startAppCustom,
|
||||
),
|
||||
startAppUseCustom = json.optBooleanOrDefault(
|
||||
"startAppUseCustom",
|
||||
defaults.startAppUseCustom,
|
||||
),
|
||||
vdDestroyContent = json.optBooleanOrDefault(
|
||||
"vdDestroyContent",
|
||||
defaults.vdDestroyContent,
|
||||
),
|
||||
vdSystemDecorations = json.optBooleanOrDefault(
|
||||
"vdSystemDecorations",
|
||||
defaults.vdSystemDecorations,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.github.miuzarte.scrcpyforandroid.widgets
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.Surface
|
||||
import android.view.SurfaceHolder
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
@@ -41,6 +42,7 @@ import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -51,14 +53,19 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.input.pointer.pointerInteropFilter
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
@@ -69,17 +76,18 @@ import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade
|
||||
import io.github.miuzarte.scrcpyforandroid.constants.Defaults
|
||||
import io.github.miuzarte.scrcpyforandroid.constants.ScrcpyPresets
|
||||
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
||||
import io.github.miuzarte.scrcpyforandroid.ui.contextClick
|
||||
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
|
||||
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.scrcpy.Shared.Codec
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.TouchEventHandler
|
||||
import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.Settings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions
|
||||
import io.github.miuzarte.scrcpyforandroid.ui.contextClick
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -255,16 +263,35 @@ internal fun PreviewCard(
|
||||
sessionInfo: Scrcpy.Session.SessionInfo?,
|
||||
previewHeightDp: Int,
|
||||
onOpenFullscreen: () -> Unit,
|
||||
directControlEnabled: Boolean = false,
|
||||
onInjectTouch: (suspend (
|
||||
action: Int,
|
||||
pointerId: Long,
|
||||
x: Int,
|
||||
y: Int,
|
||||
pressure: Float,
|
||||
actionButton: Int,
|
||||
buttons: Int,
|
||||
) -> Unit)? = null,
|
||||
onBackOrScreenOn: (suspend (action: Int) -> Unit)? = null,
|
||||
imeRequestToken: Int = 0,
|
||||
onImeCommitText: (suspend (String) -> Unit)? = null,
|
||||
onImeDeleteSurroundingText: (suspend (beforeLength: Int, afterLength: Int) -> Unit)? = null,
|
||||
onImeKeyEvent: (suspend (KeyEvent) -> Boolean)? = null,
|
||||
autoBringIntoView: Boolean = false,
|
||||
onAutoBringIntoViewConsumed: () -> Unit = {},
|
||||
onTouchActiveChanged: (Boolean) -> Unit = {},
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
var previewControlsVisible by rememberSaveable { mutableStateOf(false) }
|
||||
var touchAreaSize by remember { mutableStateOf(IntSize.Zero) }
|
||||
val activePointerIds = remember { linkedSetOf<Int>() }
|
||||
val activePointerPositions = remember { linkedMapOf<Int, Offset>() }
|
||||
val activePointerDevicePositions = remember { linkedMapOf<Int, Pair<Int, Int>>() }
|
||||
val pointerLabels = remember { linkedMapOf<Int, Int>() }
|
||||
var nextPointerLabel by rememberSaveable { mutableIntStateOf(1) }
|
||||
val alpha by animateFloatAsState(
|
||||
if (previewControlsVisible) 1f else 0f,
|
||||
label = "preview-controls"
|
||||
@@ -278,6 +305,34 @@ internal fun PreviewCard(
|
||||
onAutoBringIntoViewConsumed()
|
||||
}
|
||||
|
||||
val touchEventHandler = remember(
|
||||
directControlEnabled,
|
||||
sessionInfo,
|
||||
touchAreaSize,
|
||||
onInjectTouch,
|
||||
onBackOrScreenOn,
|
||||
) {
|
||||
if (!directControlEnabled || sessionInfo == null || onInjectTouch == null)
|
||||
null
|
||||
else TouchEventHandler(
|
||||
coroutineScope = coroutineScope,
|
||||
session = sessionInfo,
|
||||
touchAreaSize = touchAreaSize,
|
||||
activePointerIds = activePointerIds,
|
||||
activePointerPositions = activePointerPositions,
|
||||
activePointerDevicePositions = activePointerDevicePositions,
|
||||
pointerLabels = pointerLabels,
|
||||
nextPointerLabel = nextPointerLabel,
|
||||
mouseHoverEnabled = sessionInfo.mouseHover,
|
||||
onInjectTouch = onInjectTouch,
|
||||
onBackOrScreenOn = onBackOrScreenOn ?: {},
|
||||
onActiveTouchCountChanged = {},
|
||||
onActiveTouchDebugChanged = {},
|
||||
onNextPointerLabelChanged = { nextPointerLabel = it },
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
|
||||
VideoOutputTargetState.set(VideoOutputTarget.PREVIEW)
|
||||
@@ -294,6 +349,7 @@ internal fun PreviewCard(
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
onTouchActiveChanged(false)
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
if (VideoOutputTargetState.current.value == VideoOutputTarget.PREVIEW) {
|
||||
VideoOutputTargetState.set(VideoOutputTarget.NONE)
|
||||
@@ -310,11 +366,25 @@ internal fun PreviewCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(previewHeightDp.coerceAtLeast(120).dp)
|
||||
.pointerInput(sessionInfo) {
|
||||
detectTapGestures(onTap = {
|
||||
previewControlsVisible = !previewControlsVisible
|
||||
})
|
||||
},
|
||||
.then(
|
||||
if (directControlEnabled && touchEventHandler != null) {
|
||||
Modifier.pointerInteropFilter { event ->
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> onTouchActiveChanged(true)
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_CANCEL -> onTouchActiveChanged(false)
|
||||
}
|
||||
touchEventHandler.handleMotionEvent(event)
|
||||
}
|
||||
} else {
|
||||
Modifier.pointerInput(sessionInfo) {
|
||||
detectTapGestures(onTap = {
|
||||
previewControlsVisible = !previewControlsVisible
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
.onSizeChanged { touchAreaSize = it },
|
||||
) {
|
||||
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
|
||||
val sessionAspect =
|
||||
@@ -379,12 +449,14 @@ internal fun PreviewCard(
|
||||
|
||||
@Composable
|
||||
internal fun VirtualButtonCard(
|
||||
modifier: Modifier = Modifier,
|
||||
busy: Boolean,
|
||||
outsideActions: List<VirtualButtonAction>,
|
||||
moreActions: List<VirtualButtonAction>,
|
||||
showText: Boolean,
|
||||
onAction: (VirtualButtonAction) -> Unit,
|
||||
passwordPopupContent: (@Composable (onDismissRequest: () -> Unit) -> Unit)? = null,
|
||||
popupBottomPadding: Dp = 0.dp,
|
||||
) {
|
||||
val bar = remember(outsideActions, moreActions) {
|
||||
VirtualButtonBar(
|
||||
@@ -393,12 +465,13 @@ internal fun VirtualButtonCard(
|
||||
)
|
||||
}
|
||||
|
||||
Card {
|
||||
Card(modifier = modifier) {
|
||||
bar.Preview(
|
||||
enabled = !busy,
|
||||
enabled = true,
|
||||
showText = showText,
|
||||
onAction = onAction,
|
||||
onAction = { if (!busy) onAction(it) },
|
||||
passwordPopupContent = passwordPopupContent,
|
||||
popupBottomPadding = popupBottomPadding,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(UiSpacing.ContentVertical),
|
||||
@@ -427,6 +500,9 @@ internal fun ConfigPanel(
|
||||
onStop: () -> Unit,
|
||||
sessionInfo: Scrcpy.Session.SessionInfo?,
|
||||
onDisconnect: () -> Unit = {},
|
||||
showFullscreenAction: Boolean = false,
|
||||
onOpenFullscreen: () -> Unit = {},
|
||||
reverseSideActions: Boolean = false,
|
||||
) {
|
||||
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
|
||||
|
||||
@@ -646,34 +722,71 @@ internal fun ConfigPanel(
|
||||
.padding(all = UiSpacing.ContentVertical),
|
||||
horizontalArrangement = Arrangement.spacedBy(UiSpacing.Medium),
|
||||
) {
|
||||
if (isQuickConnected) TextButton(
|
||||
text = "断开",
|
||||
onClick = {
|
||||
onStartStopHaptic()
|
||||
onDisconnect()
|
||||
},
|
||||
modifier = Modifier.weight(1f / 4f),
|
||||
enabled = !busy,
|
||||
)
|
||||
if (!sessionStarted) TextButton(
|
||||
text = "启动",
|
||||
onClick = {
|
||||
onStartStopHaptic()
|
||||
onStart()
|
||||
},
|
||||
modifier = Modifier.weight(if (isQuickConnected) 3f / 4f else 1f),
|
||||
enabled = !busy,
|
||||
colors = ButtonDefaults.textButtonColorsPrimary(),
|
||||
)
|
||||
if (sessionStarted) TextButton(
|
||||
text = "停止",
|
||||
onClick = {
|
||||
onStartStopHaptic()
|
||||
onStop()
|
||||
},
|
||||
modifier = Modifier.weight(if (isQuickConnected) 3f / 4f else 1f),
|
||||
enabled = !busy,
|
||||
)
|
||||
val sideButtonWeight = 1f / 4f
|
||||
val mainButtonWeight = 1f -
|
||||
if (isQuickConnected || showFullscreenAction)
|
||||
sideButtonWeight * listOf(isQuickConnected, showFullscreenAction)
|
||||
.count { it }
|
||||
else 0f
|
||||
|
||||
@Composable
|
||||
fun DisconnectButton() {
|
||||
if (isQuickConnected) TextButton(
|
||||
text = "断开",
|
||||
onClick = {
|
||||
onStartStopHaptic()
|
||||
onDisconnect()
|
||||
},
|
||||
modifier = Modifier.weight(sideButtonWeight),
|
||||
enabled = !busy,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainButton() {
|
||||
if (!sessionStarted) TextButton(
|
||||
text = "启动",
|
||||
onClick = {
|
||||
onStartStopHaptic()
|
||||
onStart()
|
||||
},
|
||||
modifier = Modifier.weight(mainButtonWeight),
|
||||
enabled = !busy,
|
||||
colors = ButtonDefaults.textButtonColorsPrimary(),
|
||||
)
|
||||
if (sessionStarted) TextButton(
|
||||
text = "停止",
|
||||
onClick = {
|
||||
onStartStopHaptic()
|
||||
onStop()
|
||||
},
|
||||
modifier = Modifier.weight(mainButtonWeight),
|
||||
enabled = !busy,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FullscreenButton() {
|
||||
if (showFullscreenAction) TextButton(
|
||||
text = "全屏",
|
||||
onClick = {
|
||||
onStartStopHaptic()
|
||||
onOpenFullscreen()
|
||||
},
|
||||
modifier = Modifier.weight(sideButtonWeight),
|
||||
enabled = !busy,
|
||||
)
|
||||
}
|
||||
|
||||
if (reverseSideActions) {
|
||||
FullscreenButton()
|
||||
MainButton()
|
||||
DisconnectButton()
|
||||
} else {
|
||||
DisconnectButton()
|
||||
MainButton()
|
||||
FullscreenButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package io.github.miuzarte.scrcpyforandroid.widgets
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
@@ -13,12 +13,12 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import top.yukonga.miuix.kmp.basic.DropdownColors
|
||||
import top.yukonga.miuix.kmp.basic.DropdownDefaults
|
||||
import top.yukonga.miuix.kmp.basic.BasicComponent
|
||||
import top.yukonga.miuix.kmp.basic.BasicComponentColors
|
||||
import top.yukonga.miuix.kmp.basic.BasicComponentDefaults
|
||||
import top.yukonga.miuix.kmp.basic.DropdownArrowEndAction
|
||||
import top.yukonga.miuix.kmp.basic.DropdownColors
|
||||
import top.yukonga.miuix.kmp.basic.DropdownDefaults
|
||||
import top.yukonga.miuix.kmp.basic.DropdownImpl
|
||||
import top.yukonga.miuix.kmp.basic.HorizontalDivider
|
||||
import top.yukonga.miuix.kmp.basic.ListPopupColumn
|
||||
|
||||
@@ -52,6 +52,9 @@ import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntRect
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.github.miuzarte.scrcpyforandroid.constants.UiAndroidKeycodes
|
||||
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
||||
@@ -255,6 +258,7 @@ class VirtualButtonBar(
|
||||
onAction: (VirtualButtonAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
passwordPopupContent: (@Composable (onDismissRequest: () -> Unit) -> Unit)? = null,
|
||||
popupBottomPadding: Dp = 0.dp,
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
|
||||
@@ -321,6 +325,7 @@ class VirtualButtonBar(
|
||||
},
|
||||
passwordPopupContent = passwordPopupContent,
|
||||
renderInRootScaffold = false,
|
||||
popupBottomPadding = popupBottomPadding,
|
||||
)
|
||||
}
|
||||
if (
|
||||
@@ -329,7 +334,8 @@ class VirtualButtonBar(
|
||||
) {
|
||||
OverlayListPopup(
|
||||
show = showPasswordPopup,
|
||||
popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider,
|
||||
popupPositionProvider =
|
||||
rememberBottomSafeContextMenuPositionProvider(popupBottomPadding),
|
||||
alignment = PopupPositionProvider.Align.TopEnd,
|
||||
onDismissRequest = { showPasswordPopup = false },
|
||||
renderInRootScaffold = false,
|
||||
@@ -667,6 +673,7 @@ class VirtualButtonBar(
|
||||
passwordPopupContent: (@Composable (onDismissRequest: () -> Unit) -> Unit)? = null,
|
||||
renderInRootScaffold: Boolean,
|
||||
popupAlignment: PopupPositionProvider.Align = PopupPositionProvider.Align.TopEnd,
|
||||
popupBottomPadding: Dp = 0.dp,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val haptic = LocalHapticFeedback.current
|
||||
@@ -692,6 +699,7 @@ class VirtualButtonBar(
|
||||
popupAlignment = popupAlignment,
|
||||
onDismiss = onDismiss,
|
||||
renderInRootScaffold = renderInRootScaffold,
|
||||
popupBottomPadding = popupBottomPadding,
|
||||
) { destination, navigateTo, dismiss ->
|
||||
ListPopupColumn {
|
||||
if (destination == ActionPopupDestination.Actions)
|
||||
@@ -733,6 +741,7 @@ class VirtualButtonBar(
|
||||
popupAlignment: PopupPositionProvider.Align,
|
||||
onDismiss: () -> Unit,
|
||||
renderInRootScaffold: Boolean,
|
||||
popupBottomPadding: Dp = 0.dp,
|
||||
content: @Composable (
|
||||
destination: Destination,
|
||||
navigateTo: (Destination) -> Unit,
|
||||
@@ -742,7 +751,8 @@ class VirtualButtonBar(
|
||||
var destination by remember(show, startDestination) { mutableStateOf(startDestination) }
|
||||
OverlayListPopup(
|
||||
show = show,
|
||||
popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider,
|
||||
popupPositionProvider =
|
||||
rememberBottomSafeContextMenuPositionProvider(popupBottomPadding),
|
||||
alignment = popupAlignment,
|
||||
onDismissRequest = onDismiss,
|
||||
renderInRootScaffold = renderInRootScaffold,
|
||||
@@ -751,4 +761,39 @@ class VirtualButtonBar(
|
||||
content(destination, { destination = it }, onDismiss)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberBottomSafeContextMenuPositionProvider(
|
||||
bottomPadding: Dp,
|
||||
): PopupPositionProvider = remember(bottomPadding) {
|
||||
if (bottomPadding <= 0.dp) {
|
||||
ListPopupDefaults.ContextMenuPositionProvider
|
||||
} else {
|
||||
BottomSafeContextMenuPositionProvider(bottomPadding)
|
||||
}
|
||||
}
|
||||
|
||||
private class BottomSafeContextMenuPositionProvider(
|
||||
private val bottomPadding: Dp,
|
||||
) : PopupPositionProvider {
|
||||
private val delegate = ListPopupDefaults.ContextMenuPositionProvider
|
||||
|
||||
override fun calculatePosition(
|
||||
anchorBounds: IntRect,
|
||||
windowBounds: IntRect,
|
||||
layoutDirection: LayoutDirection,
|
||||
popupContentSize: IntSize,
|
||||
popupMargin: IntRect,
|
||||
alignment: PopupPositionProvider.Align,
|
||||
): IntOffset = delegate.calculatePosition(
|
||||
anchorBounds = anchorBounds,
|
||||
windowBounds = windowBounds,
|
||||
layoutDirection = layoutDirection,
|
||||
popupContentSize = popupContentSize,
|
||||
popupMargin = popupMargin,
|
||||
alignment = alignment,
|
||||
)
|
||||
|
||||
override fun getMargins(): PaddingValues = PaddingValues(bottom = bottomPadding)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user