feat: landscape layout
This commit is contained in:
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
~~什么我居然开始写更新日志了~~
|
~~什么我居然开始写更新日志了~~
|
||||||
|
|
||||||
|
## 0.2.5
|
||||||
|
|
||||||
|
- 修复: 预览销毁再重组会导致画面冻结
|
||||||
|
- 新增: 横屏布局
|
||||||
|
- 修复: 虚拟按钮 Popup 弹出时被 dock 栏阻挡
|
||||||
|
|
||||||
## 0.2.4
|
## 0.2.4
|
||||||
|
|
||||||
- 改进: 再次修改图标至 monochrome 主题色填充以避免与上游项目混淆
|
- 改进: 再次修改图标至 monochrome 主题色填充以避免与上游项目混淆
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<!-- markdownlint-disable MD033 -->
|
<!-- markdownlint-disable MD033 -->
|
||||||
|
|
||||||
# Scrcpy For Android
|
# Scrcpy for Android
|
||||||
|
|
||||||
<a href="https://github.com/Genymobile/scrcpy/blob/master/app/data/icon.svg" title="Modified from the original version">
|
<a href="https://github.com/Genymobile/scrcpy/blob/master/app/data/icon.svg" title="Modified from the original version">
|
||||||
<img src="app/src/main/assets/icon/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
|
<img src="app/src/main/assets/icon/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
|
||||||
@@ -48,6 +48,8 @@
|
|||||||
- 可替换 scrcpy-server
|
- 可替换 scrcpy-server
|
||||||
- 利用 mDNS 服务实现自动连接启用无线调试的设备、自动发现等待配对设备的IP与端口
|
- 利用 mDNS 服务实现自动连接启用无线调试的设备、自动发现等待配对设备的IP与端口
|
||||||
- 自动横竖屏切换(算吗
|
- 自动横竖屏切换(算吗
|
||||||
|
- 横屏布局
|
||||||
|
- 仅屏幕比例小于 16:9 的设备
|
||||||
- 画中画
|
- 画中画
|
||||||
- 双向文件传输
|
- 双向文件传输
|
||||||
- 流式 adb 终端
|
- 流式 adb 终端
|
||||||
@@ -112,6 +114,9 @@
|
|||||||
- /sdcard/Movies/Scrcpy/
|
- /sdcard/Movies/Scrcpy/
|
||||||
- /sdcard/Download/Scrcpy/
|
- /sdcard/Download/Scrcpy/
|
||||||
|
|
||||||
|
5. 横屏模式对左撇子不太友好
|
||||||
|
- 右上角有按钮可以对调方向
|
||||||
|
|
||||||
## 构建
|
## 构建
|
||||||
|
|
||||||
- JDK 17+
|
- JDK 17+
|
||||||
|
|||||||
5
TODO.md
5
TODO.md
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
## PARAMS
|
- 新的原创图标而非从 scrcpy 的图标派生
|
||||||
|
|
||||||
### LOWER PRIORITY
|
## PARAMS
|
||||||
|
|
||||||
- ~~orientation locking~~
|
- ~~orientation locking~~
|
||||||
|
|
||||||
@@ -14,5 +14,4 @@
|
|||||||
|
|
||||||
顺序无关
|
顺序无关
|
||||||
|
|
||||||
- 横屏布局
|
|
||||||
- I18N
|
- I18N
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ android {
|
|||||||
applicationId = "io.github.miuzarte.scrcpyforandroid"
|
applicationId = "io.github.miuzarte.scrcpyforandroid"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 37
|
targetSdk = 37
|
||||||
versionCode = 20
|
versionCode = 21
|
||||||
versionName = "0.2.4"
|
versionName = "0.2.5"
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
@@ -120,6 +120,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.compose.ui)
|
implementation(libs.androidx.compose.ui)
|
||||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
implementation(libs.androidx.compose.material3)
|
implementation(libs.androidx.compose.material3)
|
||||||
|
implementation(libs.androidx.compose.material3.window.size)
|
||||||
implementation(libs.androidx.compose.material.icons.extended)
|
implementation(libs.androidx.compose.material.icons.extended)
|
||||||
implementation(libs.androidx.navigation3.runtime)
|
implementation(libs.androidx.navigation3.runtime)
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package io.github.miuzarte.scrcpyforandroid
|
package io.github.miuzarte.scrcpyforandroid
|
||||||
|
|
||||||
|
import android.content.pm.ActivityInfo
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
@@ -16,6 +19,7 @@ import kotlinx.coroutines.runBlocking
|
|||||||
class MainActivity : FragmentActivity() {
|
class MainActivity : FragmentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
applyMainOrientationPolicy()
|
||||||
|
|
||||||
AppRuntime.init(applicationContext)
|
AppRuntime.init(applicationContext)
|
||||||
AppScreenOn.register(window)
|
AppScreenOn.register(window)
|
||||||
@@ -39,6 +43,7 @@ class MainActivity : FragmentActivity() {
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
applyMainOrientationPolicy()
|
||||||
StreamActivity.dismissActivePictureInPicture()
|
StreamActivity.dismissActivePictureInPicture()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,4 +51,30 @@ class MainActivity : FragmentActivity() {
|
|||||||
AppScreenOn.unregister(window)
|
AppScreenOn.unregister(window)
|
||||||
super.onDestroy()
|
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 mainHandler = Handler(Looper.getMainLooper())
|
||||||
private val bootstrapLock = Any()
|
private val bootstrapLock = Any()
|
||||||
private val bootstrapPackets = ArrayDeque<CachedPacket>()
|
private val bootstrapPackets = ArrayDeque<CachedPacket>()
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var recordingSurfaceAttached = false
|
private var recordingSurfaceAttached = false
|
||||||
|
|
||||||
@@ -111,9 +112,6 @@ object NativeCoreFacade {
|
|||||||
Log.i(TAG, "detachVideoSurface(): releasing decoder with destroyed surface")
|
Log.i(TAG, "detachVideoSurface(): releasing decoder with destroyed surface")
|
||||||
decoder?.release()
|
decoder?.release()
|
||||||
decoder = null
|
decoder = null
|
||||||
} else if (activeSurfaceId == null && !recordingSurfaceAttached) {
|
|
||||||
decoder?.release()
|
|
||||||
decoder = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,7 +180,6 @@ object NativeCoreFacade {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
val currentDecoder = decoder ?: return@attachVideoConsumer
|
val currentDecoder = decoder ?: return@attachVideoConsumer
|
||||||
if (activeSurfaceId == null) return@attachVideoConsumer
|
|
||||||
runCatching {
|
runCatching {
|
||||||
currentDecoder.feedAnnexB(
|
currentDecoder.feedAnnexB(
|
||||||
packet.data,
|
packet.data,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package io.github.miuzarte.scrcpyforandroid.nativecore
|
package io.github.miuzarte.scrcpyforandroid.nativecore
|
||||||
|
|
||||||
import android.graphics.SurfaceTexture
|
import android.graphics.SurfaceTexture
|
||||||
import android.opengl.EGLExt
|
|
||||||
import android.opengl.EGL14
|
import android.opengl.EGL14
|
||||||
import android.opengl.EGLConfig
|
import android.opengl.EGLConfig
|
||||||
import android.opengl.EGLContext
|
import android.opengl.EGLContext
|
||||||
import android.opengl.EGLDisplay
|
import android.opengl.EGLDisplay
|
||||||
|
import android.opengl.EGLExt
|
||||||
import android.opengl.EGLSurface
|
import android.opengl.EGLSurface
|
||||||
import android.opengl.GLES11Ext
|
import android.opengl.GLES11Ext
|
||||||
import android.opengl.GLES20
|
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.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
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.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
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 taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
|
||||||
val pullToRefreshState = rememberPullToRefreshState()
|
val pullToRefreshState = rememberPullToRefreshState()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
val layoutDirection = LocalLayoutDirection.current
|
||||||
val asBundle by appSettings.bundleState.collectAsState()
|
val asBundle by appSettings.bundleState.collectAsState()
|
||||||
|
|
||||||
val pathStack = remember {
|
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(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
BlurredBar(backdrop = blurBackdrop) {
|
BlurredBar(backdrop = blurBackdrop) {
|
||||||
@@ -546,104 +633,78 @@ fun FileManagerScreen(
|
|||||||
top = pagePadding.calculateTopPadding() + 12.dp,
|
top = pagePadding.calculateTopPadding() + 12.dp,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
BoxWithConstraints(Modifier.fillMaxWidth()) {
|
||||||
contentPadding = pagePadding,
|
val fileCardMinWidth = 220.dp
|
||||||
bottomInnerPadding = bottomInnerPadding,
|
val listHorizontalPadding =
|
||||||
state = listState,
|
pagePadding.calculateLeftPadding(layoutDirection) +
|
||||||
) {
|
pagePadding.calculateRightPadding(layoutDirection) +
|
||||||
if (loading && cachedEntries == null) {
|
UiSpacing.PageHorizontal * 2
|
||||||
item { FileManagerStatusCard("加载中") }
|
val availableListWidth =
|
||||||
} else if (errorText != null && cachedEntries == null) {
|
(maxWidth - listHorizontalPadding).coerceAtLeast(fileCardMinWidth)
|
||||||
item { FileManagerStatusCard("加载失败: $errorText") }
|
val columns = (
|
||||||
} else if (displayedEntries.isEmpty()) {
|
(availableListWidth.value + UiSpacing.PageItem.value) /
|
||||||
item { FileManagerStatusCard("空目录") }
|
(fileCardMinWidth.value + UiSpacing.PageItem.value)
|
||||||
} else {
|
).toInt().coerceAtLeast(1)
|
||||||
items(displayedEntries) { entry ->
|
val fileRows = remember(displayedEntries, columns) {
|
||||||
FileManagerItemCard(
|
displayedEntries.chunked(columns)
|
||||||
entry = entry,
|
}
|
||||||
summary = FileManagerService.formatSummary(entry),
|
|
||||||
onClick = {
|
|
||||||
when {
|
|
||||||
entry.isDirectory -> {
|
|
||||||
rememberCurrentScrollPosition()
|
|
||||||
pathStack.add(normalizePath(entry.fullPath))
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.kind == RemoteFileKind.Link || entry.symlinkTarget != null -> {
|
@Composable
|
||||||
taskScope.launch {
|
fun FileStateContent() {
|
||||||
val targetPath = resolveLinkTarget(entry)
|
when {
|
||||||
if (targetPath == null) {
|
loading && cachedEntries == null ->
|
||||||
withContext(Dispatchers.Main) {
|
FileManagerStatusCard(
|
||||||
snackbar.show("链接目标不可用,长按查看信息")
|
message = "加载中",
|
||||||
}
|
modifier = Modifier.fillMaxWidth(),
|
||||||
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("长按可查看文件详情")
|
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),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
repeat(columns - rowEntries.size) {
|
||||||
onLongClick = {
|
Box(Modifier.weight(1f))
|
||||||
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}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -719,8 +780,11 @@ fun FileManagerScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun FileManagerStatusCard(message: String) {
|
private fun FileManagerStatusCard(
|
||||||
Card {
|
message: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Card(modifier = modifier) {
|
||||||
Text(
|
Text(
|
||||||
text = message,
|
text = message,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -737,10 +801,10 @@ private fun FileManagerItemCard(
|
|||||||
summary: String,
|
summary: String,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
@@ -749,7 +813,8 @@ private fun FileManagerItemCard(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
.fillMaxHeight()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.content.Context
|
|||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.hardware.display.DisplayManager
|
import android.hardware.display.DisplayManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.Surface
|
import android.view.Surface
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.LocalActivity
|
import androidx.activity.compose.LocalActivity
|
||||||
|
|||||||
@@ -58,12 +58,11 @@ import com.kyant.backdrop.backdrops.layerBackdrop
|
|||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import io.github.miuzarte.scrcpyforandroid.BuildConfig
|
import io.github.miuzarte.scrcpyforandroid.BuildConfig
|
||||||
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade
|
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade
|
||||||
import io.github.miuzarte.scrcpyforandroid.constants.ThemeModes
|
|
||||||
import io.github.miuzarte.scrcpyforandroid.constants.UiMotion
|
import io.github.miuzarte.scrcpyforandroid.constants.UiMotion
|
||||||
import io.github.miuzarte.scrcpyforandroid.nativecore.NativeAdbService
|
import io.github.miuzarte.scrcpyforandroid.nativecore.NativeAdbService
|
||||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
|
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.AppScreenOn
|
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
|
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.AppUpdateChecker
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.ConnectionController
|
import io.github.miuzarte.scrcpyforandroid.services.ConnectionController
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.ConnectionStateStore
|
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.Settings
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
|
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage.quickDevices
|
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.BlurredBar
|
||||||
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
|
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
|
||||||
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableFloatingBottomBar
|
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableFloatingBottomBar
|
||||||
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableFloatingBottomBarBlur
|
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableFloatingBottomBarBlur
|
||||||
import io.github.miuzarte.scrcpyforandroid.ui.component.FloatingBottomBar
|
import io.github.miuzarte.scrcpyforandroid.ui.component.FloatingBottomBar
|
||||||
import io.github.miuzarte.scrcpyforandroid.ui.component.FloatingBottomBarItem
|
import io.github.miuzarte.scrcpyforandroid.ui.component.FloatingBottomBarItem
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.createThemeController
|
||||||
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
|
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
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.basic.Text
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
||||||
import top.yukonga.miuix.kmp.blur.layerBackdrop as miuixLayerBackdrop
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import top.yukonga.miuix.kmp.blur.layerBackdrop as miuixLayerBackdrop
|
||||||
|
|
||||||
private const val TERMINAL_FONT_RELATIVE_PATH = "terminal/font.ttf"
|
private const val TERMINAL_FONT_RELATIVE_PATH = "terminal/font.ttf"
|
||||||
|
|
||||||
@@ -191,6 +190,7 @@ fun MainScreen() {
|
|||||||
var fileTabCanNavigateUp by remember { mutableStateOf(false) }
|
var fileTabCanNavigateUp by remember { mutableStateOf(false) }
|
||||||
var fileTabNavigateUp by remember { mutableStateOf<(() -> Boolean)?>(null) }
|
var fileTabNavigateUp by remember { mutableStateOf<(() -> Boolean)?>(null) }
|
||||||
var terminalGestureLock by remember { mutableStateOf(false) }
|
var terminalGestureLock by remember { mutableStateOf(false) }
|
||||||
|
var devicePreviewGestureLock by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Scroll behaviors
|
// Scroll behaviors
|
||||||
val devicesPageScrollBehavior = MiuixScrollBehavior(
|
val devicesPageScrollBehavior = MiuixScrollBehavior(
|
||||||
@@ -538,6 +538,12 @@ fun MainScreen() {
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.then(if (blurBackdrop != null) Modifier.miuixLayerBackdrop(blurBackdrop) else Modifier),
|
.then(if (blurBackdrop != null) Modifier.miuixLayerBackdrop(blurBackdrop) else Modifier),
|
||||||
) {
|
) {
|
||||||
|
val pagerGestureLocked =
|
||||||
|
selectedTabIndex == MainBottomTabDestination.Terminal.ordinal
|
||||||
|
&& terminalGestureLock
|
||||||
|
|| selectedTabIndex == MainBottomTabDestination.Devices.ordinal
|
||||||
|
&& devicePreviewGestureLock
|
||||||
|
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -550,7 +556,7 @@ fun MainScreen() {
|
|||||||
),
|
),
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
beyondViewportPageCount = 1,
|
beyondViewportPageCount = 1,
|
||||||
userScrollEnabled = !(selectedTabIndex == MainBottomTabDestination.Terminal.ordinal && terminalGestureLock),
|
userScrollEnabled = !pagerGestureLocked,
|
||||||
) { page ->
|
) { page ->
|
||||||
val tab = tabs[page]
|
val tab = tabs[page]
|
||||||
saveableStateHolder.SaveableStateProvider(tab.name) {
|
saveableStateHolder.SaveableStateProvider(tab.name) {
|
||||||
@@ -561,6 +567,9 @@ fun MainScreen() {
|
|||||||
connectionServices = deviceConnectionServices,
|
connectionServices = deviceConnectionServices,
|
||||||
bottomInnerPadding = bottomInnerPadding,
|
bottomInnerPadding = bottomInnerPadding,
|
||||||
onOpenReorderDevices = { showReorderDevices = true },
|
onOpenReorderDevices = { showReorderDevices = true },
|
||||||
|
onPreviewGestureLockChanged = { locked ->
|
||||||
|
devicePreviewGestureLock = locked
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
MainBottomTabDestination.Terminal -> TerminalScreen(
|
MainBottomTabDestination.Terminal -> TerminalScreen(
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import io.github.miuzarte.scrcpyforandroid.StreamActivity
|
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.AppRuntime
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController
|
import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.SnackbarController
|
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.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
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.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
@@ -33,6 +38,8 @@ fun LazyColumn(
|
|||||||
verticalPadding: Dp = UiSpacing.PageVertical,
|
verticalPadding: Dp = UiSpacing.PageVertical,
|
||||||
bottomInnerPadding: Dp? = null,
|
bottomInnerPadding: Dp? = null,
|
||||||
clearFocusOnTap: Boolean = true,
|
clearFocusOnTap: Boolean = true,
|
||||||
|
limitLandscapeWidth: Boolean = true,
|
||||||
|
landscapeMaxWidth: Dp = 640.dp,
|
||||||
content: LazyListScope.() -> Unit,
|
content: LazyListScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
val layoutDirection = LocalLayoutDirection.current
|
val layoutDirection = LocalLayoutDirection.current
|
||||||
@@ -45,30 +52,45 @@ fun LazyColumn(
|
|||||||
bottom = contentPadding.calculateBottomPadding() + verticalPadding,
|
bottom = contentPadding.calculateBottomPadding() + verticalPadding,
|
||||||
)
|
)
|
||||||
|
|
||||||
LazyColumn(
|
BoxWithConstraints(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.then(
|
.then(
|
||||||
if (clearFocusOnTap)
|
if (clearFocusOnTap)
|
||||||
Modifier.pointerInput(Unit) {
|
Modifier.pointerInput(Unit) {
|
||||||
detectTapGestures(onTap = { focusManager.clearFocus() })
|
detectTapGestures(onTap = { focusManager.clearFocus() })
|
||||||
} else Modifier
|
}
|
||||||
)
|
|
||||||
.overScrollVertical()
|
|
||||||
.then(
|
|
||||||
if (scrollBehavior != null)
|
|
||||||
Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
|
|
||||||
else Modifier
|
else Modifier
|
||||||
),
|
),
|
||||||
state = state,
|
|
||||||
contentPadding = mergedContentPadding,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(itemSpacing),
|
|
||||||
) {
|
) {
|
||||||
content()
|
val contentWidthModifier =
|
||||||
bottomInnerPadding?.let { padding ->
|
if (limitLandscapeWidth && maxWidth > maxHeight) Modifier.widthIn(max = landscapeMaxWidth)
|
||||||
(padding - itemSpacing)
|
else Modifier.fillMaxWidth()
|
||||||
.takeIf { it > 0.dp }
|
|
||||||
?.let { item { Spacer(Modifier.height(it)) } }
|
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
|
@Volatile
|
||||||
private var audioPlayer: ScrcpyAudioPlayer? = null
|
private var audioPlayer: ScrcpyAudioPlayer? = null
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var mp4Recorder: NativeMp4Recorder? = null
|
private var mp4Recorder: NativeMp4Recorder? = null
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var wavRecorder: NativeWavRecorder? = null
|
private var wavRecorder: NativeWavRecorder? = null
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var aacRecorder: NativeAacRecorder? = null
|
private var aacRecorder: NativeAacRecorder? = null
|
||||||
|
|
||||||
@@ -228,8 +231,7 @@ class Scrcpy(
|
|||||||
val recordFile = RecordingFileResolver.resolve(options, info)
|
val recordFile = RecordingFileResolver.resolve(options, info)
|
||||||
when (options.recordFormat) {
|
when (options.recordFormat) {
|
||||||
ClientOptions.RecordFormat.MP4,
|
ClientOptions.RecordFormat.MP4,
|
||||||
ClientOptions.RecordFormat.M4A,
|
ClientOptions.RecordFormat.M4A -> {
|
||||||
-> {
|
|
||||||
val recorder = NativeMp4Recorder(
|
val recorder = NativeMp4Recorder(
|
||||||
outputFile = recordFile,
|
outputFile = recordFile,
|
||||||
includeVideo = options.video,
|
includeVideo = options.video,
|
||||||
|
|||||||
@@ -330,7 +330,15 @@ class TouchEventHandler(
|
|||||||
justPressedPointerIds += pointerId
|
justPressedPointerIds += pointerId
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
runCatching {
|
runCatching {
|
||||||
onInjectTouch(UiMotionActions.DOWN, pointerId.toLong(), x, y, pressure, 0, 0)
|
onInjectTouch(
|
||||||
|
UiMotionActions.DOWN,
|
||||||
|
pointerId.toLong(),
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
pressure,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
)
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
Log.w(
|
Log.w(
|
||||||
FULLSCREEN_TOUCH_LOG_TAG,
|
FULLSCREEN_TOUCH_LOG_TAG,
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ internal class DeviceAdbAutoReconnectManager(
|
|||||||
isForeground = isForeground,
|
isForeground = isForeground,
|
||||||
intervalMs = intervalMs,
|
intervalMs = intervalMs,
|
||||||
keepAliveCheck = { _, _ -> controller.keepAliveCheck(keepAliveTimeoutMs) },
|
keepAliveCheck = { _, _ -> controller.keepAliveCheck(keepAliveTimeoutMs) },
|
||||||
reconnect = { host, port -> controller.connectWithTimeout(host, port, connectTimeoutMs) },
|
reconnect = { host, port ->
|
||||||
|
controller.connectWithTimeout(host, port, connectTimeoutMs)
|
||||||
|
},
|
||||||
onReconnectSuccess = { host, port ->
|
onReconnectSuccess = { host, port ->
|
||||||
controller.markKeepAliveReconnectSuccess(host, port)
|
controller.markKeepAliveReconnectSuccess(host, port)
|
||||||
onReconnectSuccess(host, port)
|
onReconnectSuccess(host, port)
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ class NativeAudioRecorder(
|
|||||||
muxer.start()
|
muxer.start()
|
||||||
muxerStarted = true
|
muxerStarted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
if (index < 0) continue
|
if (index < 0) continue
|
||||||
val outputBuffer = currentEncoder.getOutputBuffer(index)
|
val outputBuffer = currentEncoder.getOutputBuffer(index)
|
||||||
|
|||||||
@@ -294,6 +294,7 @@ class NativeMp4Recorder(
|
|||||||
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
|
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
|
||||||
registerTrackLocked(TrackType.AUDIO, currentEncoder.outputFormat)
|
registerTrackLocked(TrackType.AUDIO, currentEncoder.outputFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
if (index < 0) continue
|
if (index < 0) continue
|
||||||
val outputBuffer = currentEncoder.getOutputBuffer(index)
|
val outputBuffer = currentEncoder.getOutputBuffer(index)
|
||||||
@@ -329,6 +330,7 @@ class NativeMp4Recorder(
|
|||||||
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
|
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
|
||||||
registerTrackLocked(TrackType.VIDEO, currentEncoder.outputFormat)
|
registerTrackLocked(TrackType.VIDEO, currentEncoder.outputFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
if (index < 0) continue
|
if (index < 0) continue
|
||||||
val outputBuffer = currentEncoder.getOutputBuffer(index)
|
val outputBuffer = currentEncoder.getOutputBuffer(index)
|
||||||
@@ -362,6 +364,7 @@ class NativeMp4Recorder(
|
|||||||
if (audioTrackIndex >= 0) return
|
if (audioTrackIndex >= 0) return
|
||||||
audioTrackIndex = muxer.addTrack(format)
|
audioTrackIndex = muxer.addTrack(format)
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackType.VIDEO -> {
|
TrackType.VIDEO -> {
|
||||||
if (videoTrackIndex >= 0) return
|
if (videoTrackIndex >= 0) return
|
||||||
videoTrackIndex = muxer.addTrack(format)
|
videoTrackIndex = muxer.addTrack(format)
|
||||||
@@ -451,6 +454,7 @@ class NativeMp4Recorder(
|
|||||||
val normalized = (presentationTimeUs - base).coerceAtLeast(0L)
|
val normalized = (presentationTimeUs - base).coerceAtLeast(0L)
|
||||||
normalized.coerceAtLeast(lastAudioPtsUs).also { lastAudioPtsUs = it }
|
normalized.coerceAtLeast(lastAudioPtsUs).also { lastAudioPtsUs = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackType.VIDEO -> {
|
TrackType.VIDEO -> {
|
||||||
val base = videoBasePtsUs ?: presentationTimeUs.also {
|
val base = videoBasePtsUs ?: presentationTimeUs.also {
|
||||||
videoBasePtsUs = it
|
videoBasePtsUs = it
|
||||||
|
|||||||
@@ -122,6 +122,10 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
|
|||||||
intPreferencesKey("device_preview_card_height_dp"),
|
intPreferencesKey("device_preview_card_height_dp"),
|
||||||
1080 / 3,
|
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(
|
val FULLSCREEN_CONTROL_IGNORE_SYSTEM_ROTATION_LOCK = Pair(
|
||||||
booleanPreferencesKey("fullscreen_control_ignore_system_rotation_lock"),
|
booleanPreferencesKey("fullscreen_control_ignore_system_rotation_lock"),
|
||||||
true,
|
true,
|
||||||
@@ -274,6 +278,7 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
|
|||||||
val fullscreenDebugInfo: Boolean,
|
val fullscreenDebugInfo: Boolean,
|
||||||
val hideSimpleConfigItems: Boolean,
|
val hideSimpleConfigItems: Boolean,
|
||||||
val devicePreviewCardHeightDp: Int,
|
val devicePreviewCardHeightDp: Int,
|
||||||
|
val deviceTwoPaneConfigOnRight: Boolean,
|
||||||
val fullscreenControlIgnoreSystemRotationLock: Boolean,
|
val fullscreenControlIgnoreSystemRotationLock: Boolean,
|
||||||
val showFullscreenVirtualButtons: Boolean,
|
val showFullscreenVirtualButtons: Boolean,
|
||||||
val fullscreenVirtualButtonHeightDp: Int,
|
val fullscreenVirtualButtonHeightDp: Int,
|
||||||
@@ -330,6 +335,7 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
|
|||||||
bundleField(FULLSCREEN_DEBUG_INFO) { it.fullscreenDebugInfo },
|
bundleField(FULLSCREEN_DEBUG_INFO) { it.fullscreenDebugInfo },
|
||||||
bundleField(HIDE_SIMPLE_CONFIG_ITEMS) { it.hideSimpleConfigItems },
|
bundleField(HIDE_SIMPLE_CONFIG_ITEMS) { it.hideSimpleConfigItems },
|
||||||
bundleField(DEVICE_PREVIEW_CARD_HEIGHT_DP) { it.devicePreviewCardHeightDp },
|
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(FULLSCREEN_CONTROL_IGNORE_SYSTEM_ROTATION_LOCK) { it.fullscreenControlIgnoreSystemRotationLock },
|
||||||
bundleField(SHOW_FULLSCREEN_VIRTUAL_BUTTONS) { it.showFullscreenVirtualButtons },
|
bundleField(SHOW_FULLSCREEN_VIRTUAL_BUTTONS) { it.showFullscreenVirtualButtons },
|
||||||
bundleField(FULLSCREEN_VIRTUAL_BUTTON_HEIGHT_DP) { it.fullscreenVirtualButtonHeightDp },
|
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),
|
fullscreenDebugInfo = preferences.read(FULLSCREEN_DEBUG_INFO),
|
||||||
hideSimpleConfigItems = preferences.read(HIDE_SIMPLE_CONFIG_ITEMS),
|
hideSimpleConfigItems = preferences.read(HIDE_SIMPLE_CONFIG_ITEMS),
|
||||||
devicePreviewCardHeightDp = preferences.read(DEVICE_PREVIEW_CARD_HEIGHT_DP),
|
devicePreviewCardHeightDp = preferences.read(DEVICE_PREVIEW_CARD_HEIGHT_DP),
|
||||||
|
deviceTwoPaneConfigOnRight = preferences.read(DEVICE_TWO_PANE_CONFIG_ON_RIGHT),
|
||||||
fullscreenControlIgnoreSystemRotationLock =
|
fullscreenControlIgnoreSystemRotationLock =
|
||||||
preferences.read(FULLSCREEN_CONTROL_IGNORE_SYSTEM_ROTATION_LOCK),
|
preferences.read(FULLSCREEN_CONTROL_IGNORE_SYSTEM_ROTATION_LOCK),
|
||||||
showFullscreenVirtualButtons = preferences.read(SHOW_FULLSCREEN_VIRTUAL_BUTTONS),
|
showFullscreenVirtualButtons = preferences.read(SHOW_FULLSCREEN_VIRTUAL_BUTTONS),
|
||||||
|
|||||||
@@ -306,67 +306,250 @@ class ScrcpyProfiles(context: Context) : Settings(context, "ScrcpyProfiles") {
|
|||||||
val defaults = ScrcpyOptions.defaultBundle()
|
val defaults = ScrcpyOptions.defaultBundle()
|
||||||
val json = bundleJson ?: return defaults
|
val json = bundleJson ?: return defaults
|
||||||
return defaults.copy(
|
return defaults.copy(
|
||||||
crop = json.optStringOrDefault("crop", defaults.crop),
|
crop = json.optStringOrDefault(
|
||||||
recordFilename = json.optStringOrDefault("recordFilename", defaults.recordFilename),
|
"crop",
|
||||||
videoCodecOptions = json.optStringOrDefault("videoCodecOptions", defaults.videoCodecOptions),
|
defaults.crop,
|
||||||
audioCodecOptions = json.optStringOrDefault("audioCodecOptions", defaults.audioCodecOptions),
|
),
|
||||||
videoEncoder = json.optStringOrDefault("videoEncoder", defaults.videoEncoder),
|
recordFilename = json.optStringOrDefault(
|
||||||
audioEncoder = json.optStringOrDefault("audioEncoder", defaults.audioEncoder),
|
"recordFilename",
|
||||||
cameraId = json.optStringOrDefault("cameraId", defaults.cameraId),
|
defaults.recordFilename,
|
||||||
cameraSize = json.optStringOrDefault("cameraSize", defaults.cameraSize),
|
),
|
||||||
cameraSizeCustom = json.optStringOrDefault("cameraSizeCustom", defaults.cameraSizeCustom),
|
videoCodecOptions = json.optStringOrDefault(
|
||||||
cameraSizeUseCustom = json.optBooleanOrDefault("cameraSizeUseCustom", defaults.cameraSizeUseCustom),
|
"videoCodecOptions",
|
||||||
cameraAr = json.optStringOrDefault("cameraAr", defaults.cameraAr),
|
defaults.videoCodecOptions,
|
||||||
cameraFps = json.optIntOrDefault("cameraFps", defaults.cameraFps),
|
),
|
||||||
logLevel = json.optStringOrDefault("logLevel", defaults.logLevel),
|
audioCodecOptions = json.optStringOrDefault(
|
||||||
videoCodec = json.optStringOrDefault("videoCodec", defaults.videoCodec),
|
"audioCodecOptions",
|
||||||
audioCodec = json.optStringOrDefault("audioCodec", defaults.audioCodec),
|
defaults.audioCodecOptions,
|
||||||
videoSource = json.optStringOrDefault("videoSource", defaults.videoSource),
|
),
|
||||||
audioSource = json.optStringOrDefault("audioSource", defaults.audioSource),
|
videoEncoder = json.optStringOrDefault(
|
||||||
recordFormat = json.optStringOrDefault("recordFormat", defaults.recordFormat),
|
"videoEncoder",
|
||||||
cameraFacing = json.optStringOrDefault("cameraFacing", defaults.cameraFacing),
|
defaults.videoEncoder,
|
||||||
maxSize = json.optIntOrDefault("maxSize", defaults.maxSize),
|
),
|
||||||
videoBitRate = json.optIntOrDefault("videoBitRate", defaults.videoBitRate),
|
audioEncoder = json.optStringOrDefault(
|
||||||
audioBitRate = json.optIntOrDefault("audioBitRate", defaults.audioBitRate),
|
"audioEncoder",
|
||||||
maxFps = json.optStringOrDefault("maxFps", defaults.maxFps),
|
defaults.audioEncoder,
|
||||||
angle = json.optStringOrDefault("angle", defaults.angle),
|
),
|
||||||
captureOrientation = json.optIntOrDefault("captureOrientation", defaults.captureOrientation),
|
cameraId = json.optStringOrDefault(
|
||||||
captureOrientationLock = json.optStringOrDefault("captureOrientationLock", defaults.captureOrientationLock),
|
"cameraId",
|
||||||
displayOrientation = json.optIntOrDefault("displayOrientation", defaults.displayOrientation),
|
defaults.cameraId,
|
||||||
recordOrientation = json.optIntOrDefault("recordOrientation", defaults.recordOrientation),
|
),
|
||||||
displayImePolicy = json.optStringOrDefault("displayImePolicy", defaults.displayImePolicy),
|
cameraSize = json.optStringOrDefault(
|
||||||
displayId = json.optIntOrDefault("displayId", defaults.displayId),
|
"cameraSize",
|
||||||
screenOffTimeout = json.optLongOrDefault("screenOffTimeout", defaults.screenOffTimeout),
|
defaults.cameraSize,
|
||||||
showTouches = json.optBooleanOrDefault("showTouches", defaults.showTouches),
|
),
|
||||||
fullscreen = json.optBooleanOrDefault("fullscreen", defaults.fullscreen),
|
cameraSizeCustom = json.optStringOrDefault(
|
||||||
control = json.optBooleanOrDefault("control", defaults.control),
|
"cameraSizeCustom",
|
||||||
videoPlayback = json.optBooleanOrDefault("videoPlayback", defaults.videoPlayback),
|
defaults.cameraSizeCustom,
|
||||||
audioPlayback = json.optBooleanOrDefault("audioPlayback", defaults.audioPlayback),
|
),
|
||||||
turnScreenOff = json.optBooleanOrDefault("turnScreenOff", defaults.turnScreenOff),
|
cameraSizeUseCustom = json.optBooleanOrDefault(
|
||||||
keyInjectMode = json.optStringOrDefault("keyInjectMode", defaults.keyInjectMode),
|
"cameraSizeUseCustom",
|
||||||
forwardKeyRepeat = json.optBooleanOrDefault("forwardKeyRepeat", defaults.forwardKeyRepeat),
|
defaults.cameraSizeUseCustom,
|
||||||
stayAwake = json.optBooleanOrDefault("stayAwake", defaults.stayAwake),
|
),
|
||||||
disableScreensaver = json.optBooleanOrDefault("disableScreensaver", defaults.disableScreensaver),
|
cameraAr = json.optStringOrDefault(
|
||||||
powerOffOnClose = json.optBooleanOrDefault("powerOffOnClose", defaults.powerOffOnClose),
|
"cameraAr",
|
||||||
legacyPaste = json.optBooleanOrDefault("legacyPaste", defaults.legacyPaste),
|
defaults.cameraAr,
|
||||||
clipboardAutosync = json.optBooleanOrDefault("clipboardAutosync", defaults.clipboardAutosync),
|
),
|
||||||
downsizeOnError = json.optBooleanOrDefault("downsizeOnError", defaults.downsizeOnError),
|
cameraFps = json.optIntOrDefault(
|
||||||
mouseHover = json.optBooleanOrDefault("mouseHover", defaults.mouseHover),
|
"cameraFps",
|
||||||
cleanup = json.optBooleanOrDefault("cleanup", defaults.cleanup),
|
defaults.cameraFps,
|
||||||
powerOn = json.optBooleanOrDefault("powerOn", defaults.powerOn),
|
),
|
||||||
video = json.optBooleanOrDefault("video", defaults.video),
|
logLevel = json.optStringOrDefault(
|
||||||
audio = json.optBooleanOrDefault("audio", defaults.audio),
|
"logLevel",
|
||||||
requireAudio = json.optBooleanOrDefault("requireAudio", defaults.requireAudio),
|
defaults.logLevel,
|
||||||
killAdbOnClose = json.optBooleanOrDefault("killAdbOnClose", defaults.killAdbOnClose),
|
),
|
||||||
cameraHighSpeed = json.optBooleanOrDefault("cameraHighSpeed", defaults.cameraHighSpeed),
|
videoCodec = json.optStringOrDefault(
|
||||||
list = json.optStringOrDefault("list", defaults.list),
|
"videoCodec",
|
||||||
audioDup = json.optBooleanOrDefault("audioDup", defaults.audioDup),
|
defaults.videoCodec,
|
||||||
newDisplay = json.optStringOrDefault("newDisplay", defaults.newDisplay),
|
),
|
||||||
startApp = json.optStringOrDefault("startApp", defaults.startApp),
|
audioCodec = json.optStringOrDefault(
|
||||||
startAppCustom = json.optStringOrDefault("startAppCustom", defaults.startAppCustom),
|
"audioCodec",
|
||||||
startAppUseCustom = json.optBooleanOrDefault("startAppUseCustom", defaults.startAppUseCustom),
|
defaults.audioCodec,
|
||||||
vdDestroyContent = json.optBooleanOrDefault("vdDestroyContent", defaults.vdDestroyContent),
|
),
|
||||||
vdSystemDecorations = json.optBooleanOrDefault("vdSystemDecorations", defaults.vdSystemDecorations),
|
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.annotation.SuppressLint
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
|
import android.view.MotionEvent
|
||||||
import android.view.Surface
|
import android.view.Surface
|
||||||
import android.view.SurfaceHolder
|
import android.view.SurfaceHolder
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
@@ -41,6 +42,7 @@ import androidx.compose.runtime.DisposableEffect
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
@@ -51,14 +53,19 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.focus.FocusDirection
|
import androidx.compose.ui.focus.FocusDirection
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
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.LocalFocusManager
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
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.Defaults
|
||||||
import io.github.miuzarte.scrcpyforandroid.constants.ScrcpyPresets
|
import io.github.miuzarte.scrcpyforandroid.constants.ScrcpyPresets
|
||||||
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
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.models.DeviceShortcut
|
||||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlider
|
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlider
|
||||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperTextField
|
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperTextField
|
||||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
|
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
|
||||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec
|
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.services.LocalSnackbarController
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions
|
import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Settings
|
import io.github.miuzarte.scrcpyforandroid.storage.Settings
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage
|
import io.github.miuzarte.scrcpyforandroid.storage.Storage
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions
|
import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.contextClick
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
@@ -255,16 +263,35 @@ internal fun PreviewCard(
|
|||||||
sessionInfo: Scrcpy.Session.SessionInfo?,
|
sessionInfo: Scrcpy.Session.SessionInfo?,
|
||||||
previewHeightDp: Int,
|
previewHeightDp: Int,
|
||||||
onOpenFullscreen: () -> Unit,
|
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,
|
imeRequestToken: Int = 0,
|
||||||
onImeCommitText: (suspend (String) -> Unit)? = null,
|
onImeCommitText: (suspend (String) -> Unit)? = null,
|
||||||
onImeDeleteSurroundingText: (suspend (beforeLength: Int, afterLength: Int) -> Unit)? = null,
|
onImeDeleteSurroundingText: (suspend (beforeLength: Int, afterLength: Int) -> Unit)? = null,
|
||||||
onImeKeyEvent: (suspend (KeyEvent) -> Boolean)? = null,
|
onImeKeyEvent: (suspend (KeyEvent) -> Boolean)? = null,
|
||||||
autoBringIntoView: Boolean = false,
|
autoBringIntoView: Boolean = false,
|
||||||
onAutoBringIntoViewConsumed: () -> Unit = {},
|
onAutoBringIntoViewConsumed: () -> Unit = {},
|
||||||
|
onTouchActiveChanged: (Boolean) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
var previewControlsVisible by rememberSaveable { mutableStateOf(false) }
|
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(
|
val alpha by animateFloatAsState(
|
||||||
if (previewControlsVisible) 1f else 0f,
|
if (previewControlsVisible) 1f else 0f,
|
||||||
label = "preview-controls"
|
label = "preview-controls"
|
||||||
@@ -278,6 +305,34 @@ internal fun PreviewCard(
|
|||||||
onAutoBringIntoViewConsumed()
|
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) {
|
DisposableEffect(lifecycleOwner) {
|
||||||
if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
|
if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
|
||||||
VideoOutputTargetState.set(VideoOutputTarget.PREVIEW)
|
VideoOutputTargetState.set(VideoOutputTarget.PREVIEW)
|
||||||
@@ -294,6 +349,7 @@ internal fun PreviewCard(
|
|||||||
}
|
}
|
||||||
lifecycleOwner.lifecycle.addObserver(observer)
|
lifecycleOwner.lifecycle.addObserver(observer)
|
||||||
onDispose {
|
onDispose {
|
||||||
|
onTouchActiveChanged(false)
|
||||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||||
if (VideoOutputTargetState.current.value == VideoOutputTarget.PREVIEW) {
|
if (VideoOutputTargetState.current.value == VideoOutputTarget.PREVIEW) {
|
||||||
VideoOutputTargetState.set(VideoOutputTarget.NONE)
|
VideoOutputTargetState.set(VideoOutputTarget.NONE)
|
||||||
@@ -310,11 +366,25 @@ internal fun PreviewCard(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(previewHeightDp.coerceAtLeast(120).dp)
|
.height(previewHeightDp.coerceAtLeast(120).dp)
|
||||||
.pointerInput(sessionInfo) {
|
.then(
|
||||||
detectTapGestures(onTap = {
|
if (directControlEnabled && touchEventHandler != null) {
|
||||||
previewControlsVisible = !previewControlsVisible
|
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()) {
|
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
|
||||||
val sessionAspect =
|
val sessionAspect =
|
||||||
@@ -379,12 +449,14 @@ internal fun PreviewCard(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun VirtualButtonCard(
|
internal fun VirtualButtonCard(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
busy: Boolean,
|
busy: Boolean,
|
||||||
outsideActions: List<VirtualButtonAction>,
|
outsideActions: List<VirtualButtonAction>,
|
||||||
moreActions: List<VirtualButtonAction>,
|
moreActions: List<VirtualButtonAction>,
|
||||||
showText: Boolean,
|
showText: Boolean,
|
||||||
onAction: (VirtualButtonAction) -> Unit,
|
onAction: (VirtualButtonAction) -> Unit,
|
||||||
passwordPopupContent: (@Composable (onDismissRequest: () -> Unit) -> Unit)? = null,
|
passwordPopupContent: (@Composable (onDismissRequest: () -> Unit) -> Unit)? = null,
|
||||||
|
popupBottomPadding: Dp = 0.dp,
|
||||||
) {
|
) {
|
||||||
val bar = remember(outsideActions, moreActions) {
|
val bar = remember(outsideActions, moreActions) {
|
||||||
VirtualButtonBar(
|
VirtualButtonBar(
|
||||||
@@ -393,12 +465,13 @@ internal fun VirtualButtonCard(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Card {
|
Card(modifier = modifier) {
|
||||||
bar.Preview(
|
bar.Preview(
|
||||||
enabled = !busy,
|
enabled = true,
|
||||||
showText = showText,
|
showText = showText,
|
||||||
onAction = onAction,
|
onAction = { if (!busy) onAction(it) },
|
||||||
passwordPopupContent = passwordPopupContent,
|
passwordPopupContent = passwordPopupContent,
|
||||||
|
popupBottomPadding = popupBottomPadding,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(UiSpacing.ContentVertical),
|
.padding(UiSpacing.ContentVertical),
|
||||||
@@ -427,6 +500,9 @@ internal fun ConfigPanel(
|
|||||||
onStop: () -> Unit,
|
onStop: () -> Unit,
|
||||||
sessionInfo: Scrcpy.Session.SessionInfo?,
|
sessionInfo: Scrcpy.Session.SessionInfo?,
|
||||||
onDisconnect: () -> Unit = {},
|
onDisconnect: () -> Unit = {},
|
||||||
|
showFullscreenAction: Boolean = false,
|
||||||
|
onOpenFullscreen: () -> Unit = {},
|
||||||
|
reverseSideActions: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
|
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
|
||||||
|
|
||||||
@@ -646,34 +722,71 @@ internal fun ConfigPanel(
|
|||||||
.padding(all = UiSpacing.ContentVertical),
|
.padding(all = UiSpacing.ContentVertical),
|
||||||
horizontalArrangement = Arrangement.spacedBy(UiSpacing.Medium),
|
horizontalArrangement = Arrangement.spacedBy(UiSpacing.Medium),
|
||||||
) {
|
) {
|
||||||
if (isQuickConnected) TextButton(
|
val sideButtonWeight = 1f / 4f
|
||||||
text = "断开",
|
val mainButtonWeight = 1f -
|
||||||
onClick = {
|
if (isQuickConnected || showFullscreenAction)
|
||||||
onStartStopHaptic()
|
sideButtonWeight * listOf(isQuickConnected, showFullscreenAction)
|
||||||
onDisconnect()
|
.count { it }
|
||||||
},
|
else 0f
|
||||||
modifier = Modifier.weight(1f / 4f),
|
|
||||||
enabled = !busy,
|
@Composable
|
||||||
)
|
fun DisconnectButton() {
|
||||||
if (!sessionStarted) TextButton(
|
if (isQuickConnected) TextButton(
|
||||||
text = "启动",
|
text = "断开",
|
||||||
onClick = {
|
onClick = {
|
||||||
onStartStopHaptic()
|
onStartStopHaptic()
|
||||||
onStart()
|
onDisconnect()
|
||||||
},
|
},
|
||||||
modifier = Modifier.weight(if (isQuickConnected) 3f / 4f else 1f),
|
modifier = Modifier.weight(sideButtonWeight),
|
||||||
enabled = !busy,
|
enabled = !busy,
|
||||||
colors = ButtonDefaults.textButtonColorsPrimary(),
|
)
|
||||||
)
|
}
|
||||||
if (sessionStarted) TextButton(
|
|
||||||
text = "停止",
|
@Composable
|
||||||
onClick = {
|
fun MainButton() {
|
||||||
onStartStopHaptic()
|
if (!sessionStarted) TextButton(
|
||||||
onStop()
|
text = "启动",
|
||||||
},
|
onClick = {
|
||||||
modifier = Modifier.weight(if (isQuickConnected) 3f / 4f else 1f),
|
onStartStopHaptic()
|
||||||
enabled = !busy,
|
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.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
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.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
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.BasicComponent
|
||||||
import top.yukonga.miuix.kmp.basic.BasicComponentColors
|
import top.yukonga.miuix.kmp.basic.BasicComponentColors
|
||||||
import top.yukonga.miuix.kmp.basic.BasicComponentDefaults
|
import top.yukonga.miuix.kmp.basic.BasicComponentDefaults
|
||||||
import top.yukonga.miuix.kmp.basic.DropdownArrowEndAction
|
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.DropdownImpl
|
||||||
import top.yukonga.miuix.kmp.basic.HorizontalDivider
|
import top.yukonga.miuix.kmp.basic.HorizontalDivider
|
||||||
import top.yukonga.miuix.kmp.basic.ListPopupColumn
|
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.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.IntOffset
|
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 androidx.compose.ui.unit.dp
|
||||||
import io.github.miuzarte.scrcpyforandroid.constants.UiAndroidKeycodes
|
import io.github.miuzarte.scrcpyforandroid.constants.UiAndroidKeycodes
|
||||||
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
||||||
@@ -255,6 +258,7 @@ class VirtualButtonBar(
|
|||||||
onAction: (VirtualButtonAction) -> Unit,
|
onAction: (VirtualButtonAction) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
passwordPopupContent: (@Composable (onDismissRequest: () -> Unit) -> Unit)? = null,
|
passwordPopupContent: (@Composable (onDismissRequest: () -> Unit) -> Unit)? = null,
|
||||||
|
popupBottomPadding: Dp = 0.dp,
|
||||||
) {
|
) {
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
|
|
||||||
@@ -321,6 +325,7 @@ class VirtualButtonBar(
|
|||||||
},
|
},
|
||||||
passwordPopupContent = passwordPopupContent,
|
passwordPopupContent = passwordPopupContent,
|
||||||
renderInRootScaffold = false,
|
renderInRootScaffold = false,
|
||||||
|
popupBottomPadding = popupBottomPadding,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -329,7 +334,8 @@ class VirtualButtonBar(
|
|||||||
) {
|
) {
|
||||||
OverlayListPopup(
|
OverlayListPopup(
|
||||||
show = showPasswordPopup,
|
show = showPasswordPopup,
|
||||||
popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider,
|
popupPositionProvider =
|
||||||
|
rememberBottomSafeContextMenuPositionProvider(popupBottomPadding),
|
||||||
alignment = PopupPositionProvider.Align.TopEnd,
|
alignment = PopupPositionProvider.Align.TopEnd,
|
||||||
onDismissRequest = { showPasswordPopup = false },
|
onDismissRequest = { showPasswordPopup = false },
|
||||||
renderInRootScaffold = false,
|
renderInRootScaffold = false,
|
||||||
@@ -667,6 +673,7 @@ class VirtualButtonBar(
|
|||||||
passwordPopupContent: (@Composable (onDismissRequest: () -> Unit) -> Unit)? = null,
|
passwordPopupContent: (@Composable (onDismissRequest: () -> Unit) -> Unit)? = null,
|
||||||
renderInRootScaffold: Boolean,
|
renderInRootScaffold: Boolean,
|
||||||
popupAlignment: PopupPositionProvider.Align = PopupPositionProvider.Align.TopEnd,
|
popupAlignment: PopupPositionProvider.Align = PopupPositionProvider.Align.TopEnd,
|
||||||
|
popupBottomPadding: Dp = 0.dp,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
@@ -692,6 +699,7 @@ class VirtualButtonBar(
|
|||||||
popupAlignment = popupAlignment,
|
popupAlignment = popupAlignment,
|
||||||
onDismiss = onDismiss,
|
onDismiss = onDismiss,
|
||||||
renderInRootScaffold = renderInRootScaffold,
|
renderInRootScaffold = renderInRootScaffold,
|
||||||
|
popupBottomPadding = popupBottomPadding,
|
||||||
) { destination, navigateTo, dismiss ->
|
) { destination, navigateTo, dismiss ->
|
||||||
ListPopupColumn {
|
ListPopupColumn {
|
||||||
if (destination == ActionPopupDestination.Actions)
|
if (destination == ActionPopupDestination.Actions)
|
||||||
@@ -733,6 +741,7 @@ class VirtualButtonBar(
|
|||||||
popupAlignment: PopupPositionProvider.Align,
|
popupAlignment: PopupPositionProvider.Align,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
renderInRootScaffold: Boolean,
|
renderInRootScaffold: Boolean,
|
||||||
|
popupBottomPadding: Dp = 0.dp,
|
||||||
content: @Composable (
|
content: @Composable (
|
||||||
destination: Destination,
|
destination: Destination,
|
||||||
navigateTo: (Destination) -> Unit,
|
navigateTo: (Destination) -> Unit,
|
||||||
@@ -742,7 +751,8 @@ class VirtualButtonBar(
|
|||||||
var destination by remember(show, startDestination) { mutableStateOf(startDestination) }
|
var destination by remember(show, startDestination) { mutableStateOf(startDestination) }
|
||||||
OverlayListPopup(
|
OverlayListPopup(
|
||||||
show = show,
|
show = show,
|
||||||
popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider,
|
popupPositionProvider =
|
||||||
|
rememberBottomSafeContextMenuPositionProvider(popupBottomPadding),
|
||||||
alignment = popupAlignment,
|
alignment = popupAlignment,
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
renderInRootScaffold = renderInRootScaffold,
|
renderInRootScaffold = renderInRootScaffold,
|
||||||
@@ -751,4 +761,39 @@ class VirtualButtonBar(
|
|||||||
content(destination, { destination = it }, onDismiss)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-toolin
|
|||||||
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||||
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
|
androidx-compose-material3-window-size = { group = "androidx.compose.material3", name = "material3-window-size-class" }
|
||||||
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
|
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
|
||||||
androidx-navigation3-runtime = { group = "androidx.navigation3", name = "navigation3-runtime-android", version.ref = "androidxNavigation3" }
|
androidx-navigation3-runtime = { group = "androidx.navigation3", name = "navigation3-runtime-android", version.ref = "androidxNavigation3" }
|
||||||
bcpkix-jdk18on = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bcpkixJdk18on" }
|
bcpkix-jdk18on = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bcpkixJdk18on" }
|
||||||
|
|||||||
Reference in New Issue
Block a user