From 93c8c06dc2da6c29c06b64252a634a02ca2fe093 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 4 May 2025 16:47:34 +0800 Subject: [PATCH] 1.16.7 --- app/build.gradle.kts | 4 +- .../{index-22ohecZ7.js => index-BCd7FO_p.js} | 2 +- app/src/main/assets/index.html | 33 ++++---- .../activity/video/player/CenterControl.kt | 31 ++++++-- .../activity/video/player/PlayerView.kt | 76 +++++++++++-------- .../mixfile/server/core/routes/Routes.kt | 5 +- .../core/routes/api/webdav/WebDavRoute.kt | 4 +- .../mixfile/server/core/utils/Extension.kt | 14 +++- .../donut/mixfile/server/core/utils/Util.kt | 14 ++-- .../java/com/donut/mixfile/util/CommonUtil.kt | 9 +-- .../com/donut/mixfile/util/file/FileDialog.kt | 14 ++-- .../com/donut/mixfile/util/file/FileUtil.kt | 11 ++- .../java/com/donut/mixfile/ExampleUnitTest.kt | 22 +----- 13 files changed, 126 insertions(+), 113 deletions(-) rename app/src/main/assets/assets/{index-22ohecZ7.js => index-BCd7FO_p.js} (99%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c9ffc88..eaaab23 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,8 +16,8 @@ android { applicationId = "com.donut.mixfile" minSdk = 26 targetSdk = 35 - versionCode = 124 - versionName = "1.16.6" + versionCode = 125 + versionName = "1.16.7" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/assets/assets/index-22ohecZ7.js b/app/src/main/assets/assets/index-BCd7FO_p.js similarity index 99% rename from app/src/main/assets/assets/index-22ohecZ7.js rename to app/src/main/assets/assets/index-BCd7FO_p.js index 378589c..8992bcb 100644 --- a/app/src/main/assets/assets/index-22ohecZ7.js +++ b/app/src/main/assets/assets/index-BCd7FO_p.js @@ -341,7 +341,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho button { font-size: max(.6rem, 14px); } -`;function qx({fileList:e}){const[t,r]=st(!1),[n,i]=st(`文件分享-${tP()}`);return D(hU,{className:"shadow",children:[D("h4",{children:"导出文件列表"}),D("div",{class:"content",children:D(iC,{label:"文件列表名称",variant:"outlined",value:n,onChange:a=>{i(a.target.value.trim)}})}),D(si,{variant:"contained",disabled:t,onClick:async()=>{const a=e.map(({name:c,size:l,shareInfoData:u})=>({name:c,size:l,category:"",time:new Date().getTime(),shareInfoData:u})),o=tx.gzip(JSON.stringify(a)),s=`${zo}api/upload?name=${encodeURIComponent(`${n}.mix_list`)}&add=false`;r(!0);try{let c=await Bt.put(s,o,{});ud(c.data)}finally{r(!1)}},children:"确认导出"})]})}const pU=bn.div` +`;function qx({fileList:e}){const[t,r]=st(!1),[n,i]=st(`文件分享-${tP()}`);return D(hU,{className:"shadow",children:[D("h4",{children:"导出文件列表"}),D("div",{class:"content",children:D(iC,{label:"文件列表名称",variant:"outlined",value:n,onChange:a=>{i(a.target.value.trim())}})}),D(si,{variant:"contained",disabled:t,onClick:async()=>{const a=e.map(({name:c,size:l,shareInfoData:u})=>({name:c,size:l,category:"",time:new Date().getTime(),shareInfoData:u})),o=tx.gzip(JSON.stringify(a)),s=`${zo}api/upload?name=${encodeURIComponent(`${n}.mix_list`)}&add=false`;r(!0);try{let c=await Bt.put(s,o,{});ud(c.data)}finally{r(!1)}},children:"确认导出"})]})}const pU=bn.div` display: flex; flex-direction: column; gap: 10px; diff --git a/app/src/main/assets/index.html b/app/src/main/assets/index.html index e44161b..7580c8e 100644 --- a/app/src/main/assets/index.html +++ b/app/src/main/assets/index.html @@ -1,16 +1,17 @@ - - - - - - - - MixFile - - - - -
- - + + + + + + + + MixFile + + + + +
+ + + diff --git a/app/src/main/java/com/donut/mixfile/activity/video/player/CenterControl.kt b/app/src/main/java/com/donut/mixfile/activity/video/player/CenterControl.kt index 0132084..9946e1e 100644 --- a/app/src/main/java/com/donut/mixfile/activity/video/player/CenterControl.kt +++ b/app/src/main/java/com/donut/mixfile/activity/video/player/CenterControl.kt @@ -4,6 +4,8 @@ import android.annotation.SuppressLint import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.size @@ -35,8 +37,8 @@ fun CenterControl( visible: Boolean, modifier: Modifier, player: ExoPlayer, - onPause: () -> Unit = {}, - onMediaChange: () -> Unit = {} + onClick: () -> Unit = {}, + onMediaChange: () -> Unit = {}, ) { var isPlaying by remember { mutableStateOf(true) } var isBuffering by remember { mutableStateOf(true) } @@ -48,9 +50,9 @@ fun CenterControl( override fun onPlaybackStateChanged(playbackState: Int) { isBuffering = listOf(Player.STATE_BUFFERING, Player.STATE_IDLE).contains(playbackState) + super.onPlaybackStateChanged(playbackState) } - override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { onMediaChange() super.onMediaItemTransition(mediaItem, reason) @@ -59,22 +61,28 @@ fun CenterControl( override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) { isPlaying = playWhenReady + super.onPlayWhenReadyChanged(playWhenReady, reason) } } player.addListener(listener) - // 清理资源 onDispose { player.removeListener(listener) - player.release() } } - AnimatedVisibility(isBuffering, modifier = modifier) { + + AnimatedVisibility( + isBuffering, + modifier = modifier, + enter = scaleIn(), + exit = scaleOut(), + ) { CircularProgressIndicator( strokeWidth = 2.dp, modifier = Modifier.size(40.dp) ) } + AnimatedVisibility( visible = visible, enter = fadeIn(), @@ -86,6 +94,7 @@ fun CenterControl( modifier = Modifier.scale(1.5f), onClick = { player.seekBack() + onClick() }, ) { Icon( @@ -103,10 +112,15 @@ fun CenterControl( } else { player.play() } - onPause() + onClick() }, ) { - AnimatedVisibility(!isBuffering) { + AnimatedVisibility( + !isBuffering, + modifier = Modifier, + enter = scaleIn(), + exit = scaleOut(), + ) { Icon( modifier = Modifier.size(100.dp), imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, @@ -120,6 +134,7 @@ fun CenterControl( modifier = Modifier.scale(1.5f), onClick = { player.seekForward() + onClick() }, ) { Icon( diff --git a/app/src/main/java/com/donut/mixfile/activity/video/player/PlayerView.kt b/app/src/main/java/com/donut/mixfile/activity/video/player/PlayerView.kt index 1f9aff7..9a0aa13 100644 --- a/app/src/main/java/com/donut/mixfile/activity/video/player/PlayerView.kt +++ b/app/src/main/java/com/donut/mixfile/activity/video/player/PlayerView.kt @@ -6,7 +6,6 @@ import android.net.Uri import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.ExperimentalMaterial3Api @@ -38,7 +37,6 @@ import com.donut.mixfile.util.showToast import kotlinx.coroutines.delay import java.util.Locale - fun formatTime(milliseconds: Long): String { val seconds = (milliseconds / 1000) % 60 val minutes = (milliseconds / (1000 * 60)) % 60 @@ -58,6 +56,19 @@ val playerColorScheme onSecondaryContainer = colorScheme.primary.copy(0.8f) ) + +class ForceUpdateMutable(value: T) { + private var value by mutableStateOf(value) + var inc by mutableLongStateOf(0) + + val get get() = value + + fun set(value: T) { + this.value = value + inc++ + } +} + @SuppressLint("PrivateResource") @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -87,15 +98,17 @@ fun VideoPlayerScreen( var currentMediaItem by remember { mutableIntStateOf(player.currentMediaItemIndex) } - var controlsVisible by remember { mutableStateOf(true) } + var controlsVisible = remember { ForceUpdateMutable(true) } + // 控制栏自动隐藏 - LaunchedEffect(controlsVisible) { - if (controlsVisible) { - delay(3000) // 3秒后隐藏 - controlsVisible = false + LaunchedEffect(controlsVisible.inc) { + if (controlsVisible.get) { + delay(3000) + controlsVisible.set(false) } } + val lifecycleOwner = LocalLifecycleOwner.current @@ -132,24 +145,20 @@ fun VideoPlayerScreen( modifier = modifier .fillMaxSize() .background(Color.Black) - .clickable( - onClick = { - if (System.currentTimeMillis() - lastClick < 300L) { - if (player.isPlaying) { - player.pause() - controlsVisible = true - } else { - player.play() - controlsVisible = false - } - return@clickable + .clickable { + if (System.currentTimeMillis() - lastClick < 300L) { + if (player.isPlaying) { + player.pause() + controlsVisible.set(true) + } else { + player.play() + controlsVisible.set(false) } - lastClick = System.currentTimeMillis() - controlsVisible = !controlsVisible - }, - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) + return@clickable + } + lastClick = System.currentTimeMillis() + controlsVisible.set(!controlsVisible.get) + } ) { AndroidView( factory = { @@ -164,21 +173,26 @@ fun VideoPlayerScreen( TopControl( title = if (videoUris.size > 1) "${currentMediaItem + 1} - ${currentMediaTitle}" else currentMediaTitle, - visible = controlsVisible, + visible = controlsVisible.get, modifier = Modifier.align(Alignment.TopCenter) ) - CenterControl(controlsVisible, Modifier.align(Alignment.Center), player, onPause = { - controlsVisible = true - }) { + CenterControl( + controlsVisible.get, + Modifier.align(Alignment.Center), + player, + onClick = { + controlsVisible.set(true) + } + ) { currentMediaItem = player.currentMediaItemIndex - controlsVisible = true + controlsVisible.set(true) } BottomControl( - visible = controlsVisible, + visible = controlsVisible.get, modifier = Modifier.align(Alignment.BottomCenter), player = player, videos = videoUris, @@ -193,7 +207,7 @@ fun VideoPlayerScreen( } }, onTrackTimeChange = { - controlsVisible = true + controlsVisible.set(true) } ) } diff --git a/app/src/main/java/com/donut/mixfile/server/core/routes/Routes.kt b/app/src/main/java/com/donut/mixfile/server/core/routes/Routes.kt index 14770d2..228d0d0 100644 --- a/app/src/main/java/com/donut/mixfile/server/core/routes/Routes.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/routes/Routes.kt @@ -2,10 +2,9 @@ package com.donut.mixfile.server.core.routes import com.donut.mixfile.server.core.MixFileServer import com.donut.mixfile.server.core.routes.api.getAPIRoute +import com.donut.mixfile.server.core.utils.decodedPath import com.donut.mixfile.server.core.utils.parseFileMimeType import io.ktor.http.HttpStatusCode -import io.ktor.http.decodeURLQueryComponent -import io.ktor.server.request.path import io.ktor.server.response.respond import io.ktor.server.response.respondBytesWriter import io.ktor.server.routing.Routing @@ -18,7 +17,7 @@ fun MixFileServer.getRoutes(): Routing.() -> Unit { return { get("{param...}") { - val file = call.request.path().decodeURLQueryComponent().substring(1).ifEmpty { + val file = decodedPath.substring(1).ifEmpty { "index.html" } val fileStream = diff --git a/app/src/main/java/com/donut/mixfile/server/core/routes/api/webdav/WebDavRoute.kt b/app/src/main/java/com/donut/mixfile/server/core/routes/api/webdav/WebDavRoute.kt index 32bcf83..520480a 100644 --- a/app/src/main/java/com/donut/mixfile/server/core/routes/api/webdav/WebDavRoute.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/routes/api/webdav/WebDavRoute.kt @@ -11,6 +11,7 @@ import com.donut.mixfile.server.core.routes.api.webdav.objects.WebDavFile import com.donut.mixfile.server.core.routes.api.webdav.objects.WebDavManager import com.donut.mixfile.server.core.routes.api.webdav.objects.normalizePath import com.donut.mixfile.server.core.routes.api.webdav.objects.toDavPath +import com.donut.mixfile.server.core.utils.decodedPath import com.donut.mixfile.server.core.utils.decompressGzip import com.donut.mixfile.server.core.utils.getHeader import com.donut.mixfile.server.core.utils.mb @@ -24,7 +25,6 @@ import io.ktor.http.encodeURLParameter import io.ktor.http.withCharset import io.ktor.server.application.ApplicationCall import io.ktor.server.request.contentLength -import io.ktor.server.request.path import io.ktor.server.request.receiveChannel import io.ktor.server.response.header import io.ktor.server.response.respond @@ -40,8 +40,6 @@ import kotlinx.io.readByteArray const val API_PATH = "/api/webdav" -val RoutingContext.decodedPath: String get() = call.request.path().decodeURLQueryComponent() - val RoutingContext.davPath: String get() = normalizePath(decodedPath.substringAfter(API_PATH)) diff --git a/app/src/main/java/com/donut/mixfile/server/core/utils/Extension.kt b/app/src/main/java/com/donut/mixfile/server/core/utils/Extension.kt index ed56cc4..ec6eb9b 100644 --- a/app/src/main/java/com/donut/mixfile/server/core/utils/Extension.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/utils/Extension.kt @@ -1,5 +1,8 @@ package com.donut.mixfile.server.core.utils +import io.ktor.http.decodeURLQueryComponent +import io.ktor.server.request.path +import io.ktor.server.routing.RoutingContext import java.nio.ByteBuffer import kotlin.streams.toList @@ -75,9 +78,14 @@ fun Boolean?.toInt(): Int { return 0 } -val Long.mb get() = this * 1024 * 1024 +val Int.kb get() = this * 1024 + +val Long.kb get() = this * 1024 + +val Long.mb get() = this * 1024.kb + +val Int.mb get() = this * 1024.kb -val Int.mb get() = this * 1024 * 1024 fun Int.negative(): Int { return -this @@ -165,3 +173,5 @@ fun ByteArray.toInt(): Int = infix fun T?.default(value: T) = this ?: value + +val RoutingContext.decodedPath: String get() = call.request.path().decodeURLQueryComponent() diff --git a/app/src/main/java/com/donut/mixfile/server/core/utils/Util.kt b/app/src/main/java/com/donut/mixfile/server/core/utils/Util.kt index 79890b0..7c748c6 100644 --- a/app/src/main/java/com/donut/mixfile/server/core/utils/Util.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/utils/Util.kt @@ -29,10 +29,6 @@ import java.util.zip.GZIPInputStream import java.util.zip.GZIPOutputStream import kotlin.random.Random -fun String.getFileExtension(): String { - return substringAfterLast(".", "") -} - fun String.sanitizeFileName(): String { // 定义非法字符,包括控制字符、文件系统非法字符、路径遍历等 val illegalChars = "[\\x00-\\x1F\\x7F/\\\\:*?\"<>|]".toRegex() @@ -47,13 +43,13 @@ fun String.sanitizeFileName(): String { var sanitized = this // 替换非法字符为下划线 .replace(illegalChars, "_") - // 移除路径遍历序列 - .replace("..", "_") .trim() - // 检查是否为 Windows 保留文件名 - val baseName = sanitized.substringBeforeLast(".").uppercase() - if (baseName in reservedNames) { + if (sanitized.all { it == '.' }) { + sanitized = "unnamed_file" + } + + if (sanitized.uppercase() in reservedNames) { sanitized = "_$sanitized" } diff --git a/app/src/main/java/com/donut/mixfile/util/CommonUtil.kt b/app/src/main/java/com/donut/mixfile/util/CommonUtil.kt index ca12e43..283ce79 100644 --- a/app/src/main/java/com/donut/mixfile/util/CommonUtil.kt +++ b/app/src/main/java/com/donut/mixfile/util/CommonUtil.kt @@ -93,16 +93,16 @@ fun getAppVersion(context: Context): Pair { class CachedDelegate(val getKeys: () -> Array, private val initializer: () -> T) { - private var cache: T? = null - private var keys: Array = arrayOf() + private var cache: T = initializer() + private var keys: Array = getKeys() operator fun getValue(thisRef: Any?, property: Any?): T { val newKeys = getKeys() - if (cache == null || !keys.contentEquals(newKeys)) { + if (!keys.contentEquals(newKeys)) { keys = newKeys cache = initializer() } - return cache!! + return cache } operator fun setValue(thisRef: Any?, property: Any?, value: T) { @@ -110,7 +110,6 @@ class CachedDelegate(val getKeys: () -> Array, private val initializer: } } - inline fun String.isUrl(block: (URL) -> Unit = {}): Boolean { val urlPattern = Regex("^https?://(www\\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)\$") diff --git a/app/src/main/java/com/donut/mixfile/util/file/FileDialog.kt b/app/src/main/java/com/donut/mixfile/util/file/FileDialog.kt index a63a314..86c666a 100644 --- a/app/src/main/java/com/donut/mixfile/util/file/FileDialog.kt +++ b/app/src/main/java/com/donut/mixfile/util/file/FileDialog.kt @@ -23,6 +23,7 @@ import com.donut.mixfile.server.core.objects.FileDataLog import com.donut.mixfile.server.core.objects.isImage import com.donut.mixfile.server.core.objects.isVideo import com.donut.mixfile.server.core.utils.hashSHA256 +import com.donut.mixfile.server.core.utils.isTrue import com.donut.mixfile.server.core.utils.resolveMixShareInfo import com.donut.mixfile.server.core.utils.shareCode import com.donut.mixfile.server.core.utils.toHex @@ -33,6 +34,7 @@ import com.donut.mixfile.ui.routes.home.showDownloadTaskWindow import com.donut.mixfile.ui.routes.useShortCode import com.donut.mixfile.ui.routes.useSystemPlayer import com.donut.mixfile.ui.theme.colorScheme +import com.donut.mixfile.util.CachedDelegate import com.donut.mixfile.util.copyToClipboard import com.donut.mixfile.util.formatFileSize import com.donut.mixfile.util.showToast @@ -43,12 +45,12 @@ fun showFileInfoDialog( dataLog: FileDataLog, onDismiss: () -> Unit = {} ) { - val log = if (favorites.contains(dataLog)) { - dataLog - } else { - favorites.firstOrNull { it.isSimilar(dataLog) } - ?: dataLog + var isFav = false + + val log by CachedDelegate({ arrayOf(favorites) }) { + favorites.firstOrNull { it.isSimilar(dataLog).isTrue { isFav = true } } ?: dataLog } + val shareInfo = resolveMixShareInfo(log.shareInfoData) if (shareInfo == null) { showToast("解析文件分享码失败") @@ -84,7 +86,7 @@ fun showFileInfoDialog( Text(text = "查看文件", color = colorScheme.primary) }) } - if (!favorites.any { it.isSimilar(log) }) { + if (!isFav) { AssistChip(onClick = { addFavoriteLog(log) }, label = { diff --git a/app/src/main/java/com/donut/mixfile/util/file/FileUtil.kt b/app/src/main/java/com/donut/mixfile/util/file/FileUtil.kt index 17833c3..dc53ebb 100644 --- a/app/src/main/java/com/donut/mixfile/util/file/FileUtil.kt +++ b/app/src/main/java/com/donut/mixfile/util/file/FileUtil.kt @@ -22,6 +22,7 @@ import com.donut.mixfile.MainActivity import com.donut.mixfile.app import com.donut.mixfile.appScope import com.donut.mixfile.server.core.utils.StreamContent +import com.donut.mixfile.server.core.utils.kb import com.donut.mixfile.server.core.utils.mb import com.donut.mixfile.server.core.utils.sanitizeFileName import com.donut.mixfile.server.mixFileServer @@ -244,9 +245,8 @@ suspend fun loadDataWithMaxSize( onDownload(progressContent.ktorListener) }.execute { if (!it.status.isSuccess()) { - val text = if ((it.contentLength() - ?: (1024 * 1024)) < 1024 * 500 - ) it.bodyAsText() else "未知错误" + val text = + if ((it.contentLength() ?: 1024L.kb) < 500L.kb) it.bodyAsText() else "未知错误" throw Exception("下载失败: ${text}") } if ((it.contentLength() ?: 0) > limit) { @@ -293,9 +293,8 @@ suspend fun saveFileToStorage( onDownload(progress.ktorListener) }.execute { if (!it.status.isSuccess()) { - val text = if ((it.contentLength() - ?: (1024 * 1024)) < 1024 * 500 - ) it.bodyAsText() else "未知错误" + val text = + if ((it.contentLength() ?: 1024L.kb) < 500L.kb) it.bodyAsText() else "未知错误" throw Exception("下载失败: ${text}") } resolver.openOutputStream(fileUri)?.use { output -> diff --git a/app/src/test/java/com/donut/mixfile/ExampleUnitTest.kt b/app/src/test/java/com/donut/mixfile/ExampleUnitTest.kt index bff5cff..854684c 100644 --- a/app/src/test/java/com/donut/mixfile/ExampleUnitTest.kt +++ b/app/src/test/java/com/donut/mixfile/ExampleUnitTest.kt @@ -1,26 +1,7 @@ package com.donut.mixfile -import com.alibaba.fastjson2.JSONReader -import com.alibaba.fastjson2.JSONWriter -import com.alibaba.fastjson2.annotation.JSONType -import com.alibaba.fastjson2.to -import com.alibaba.fastjson2.toJSONString -import com.donut.mixfile.server.core.objects.FileDataLog +import com.donut.mixfile.server.core.utils.sanitizeFileName import org.junit.Test - -//appScope.launch(Dispatchers.IO) { -// repeat(100) { -// favorites += List(1000) { -// FileDataLog( -// genRandomString(32), -// "test-data", -// 1, -// category = "test" -// ) -// } -// } -//} - /** * Example local unit test, which will execute on the development machine (host). * @@ -30,7 +11,6 @@ import org.junit.Test class ExampleUnitTest { - @Test fun main() {