This commit is contained in:
Evan You
2025-05-04 16:47:34 +08:00
parent 33378c28dd
commit 93c8c06dc2
13 changed files with 126 additions and 113 deletions

View File

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

View File

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

View File

@@ -1,16 +1,17 @@
<!DOCTYPE html>
<html lang="chs">
<head>
<meta charset="UTF-8"/>
<link rel="icon" href="/icon.png"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta content="width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1"
name="viewport"/>
<title>MixFile</title>
<script type="module" crossorigin src="/assets/index-22ohecZ7.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Ub7aM_kp.css">
</head>
<body>
<div id="app"></div>
</body>
<!DOCTYPE html>
<html lang="chs">
<head>
<meta charset="UTF-8"/>
<link rel="icon" href="/icon.png"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta content="width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1"
name="viewport"/>
<title>MixFile</title>
<script type="module" crossorigin src="/assets/index-BCd7FO_p.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Ub7aM_kp.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

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

View File

@@ -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<T>(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)
}
)
}

View File

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

View File

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

View File

@@ -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> T?.default(value: T) = this ?: value
val RoutingContext.decodedPath: String get() = call.request.path().decodeURLQueryComponent()

View File

@@ -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"
}

View File

@@ -93,16 +93,16 @@ fun getAppVersion(context: Context): Pair<String, Long> {
class CachedDelegate<T>(val getKeys: () -> Array<Any?>, private val initializer: () -> T) {
private var cache: T? = null
private var keys: Array<Any?> = arrayOf()
private var cache: T = initializer()
private var keys: Array<Any?> = 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<T>(val getKeys: () -> Array<Any?>, 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()@:%_+.~#?&/=]*)\$")

View File

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

View File

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

View File

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