diff --git a/app/build.gradle.kts b/app/build.gradle.kts index de9cfad..939d2e7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,8 +14,8 @@ android { applicationId = "com.donut.mixfile" minSdk = 24 targetSdk = 34 - versionCode = 23 - versionName = "1.2.0" + versionCode = 26 + versionName = "1.2.3" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/com/donut/mixfile/server/routes/UploadRoute.kt b/app/src/main/java/com/donut/mixfile/server/routes/UploadRoute.kt index bd6dab6..1190ba7 100644 --- a/app/src/main/java/com/donut/mixfile/server/routes/UploadRoute.kt +++ b/app/src/main/java/com/donut/mixfile/server/routes/UploadRoute.kt @@ -30,6 +30,7 @@ fun getUploadRoute(): suspend PipelineContext.(Unit) -> U return route@{ val key = generateRandomByteArray(16) val name = call.request.queryParameters["name"] + val add = call.request.queryParameters["add"] ?: "true" if (name.isNullOrEmpty()) { call.respondText("需要文件名称", status = HttpStatusCode.InternalServerError) return@route @@ -55,7 +56,9 @@ fun getUploadRoute(): suspend PipelineContext.(Unit) -> U key = MixShareInfo.ENCODER.encode(key), referer = uploader.referer ) - addUploadLog(mixShareInfo) + if (add.toBoolean()) { + addUploadLog(mixShareInfo) + } call.respondText(mixShareInfo.toString()) } } diff --git a/app/src/main/java/com/donut/mixfile/ui/routes/Favorites.kt b/app/src/main/java/com/donut/mixfile/ui/routes/Favorites.kt index fdf4103..3b1a99c 100644 --- a/app/src/main/java/com/donut/mixfile/ui/routes/Favorites.kt +++ b/app/src/main/java/com/donut/mixfile/ui/routes/Favorites.kt @@ -34,17 +34,22 @@ import com.donut.mixfile.ui.nav.MixNavPage import com.donut.mixfile.ui.theme.colorScheme import com.donut.mixfile.util.UseEffect import com.donut.mixfile.util.compressGzip +import com.donut.mixfile.util.decodeHex import com.donut.mixfile.util.decompressGzip +import com.donut.mixfile.util.encodeToBase64 import com.donut.mixfile.util.file.FileDataLog import com.donut.mixfile.util.file.deleteFavoriteLog import com.donut.mixfile.util.file.doUploadFile import com.donut.mixfile.util.file.favCategories import com.donut.mixfile.util.file.favorites import com.donut.mixfile.util.file.selectAndUploadFile -import com.donut.mixfile.util.ignoreError +import com.donut.mixfile.util.formatFileSize +import com.donut.mixfile.util.hashSHA256 import com.donut.mixfile.util.objects.ProgressContent +import com.donut.mixfile.util.showErrorDialog import com.donut.mixfile.util.showToast import com.donut.mixfile.util.toJsonString +import com.donut.mixfile.util.truncate import com.google.gson.Gson import io.ktor.client.plugins.onDownload import io.ktor.client.plugins.timeout @@ -69,19 +74,55 @@ fun openCategorySelect(default: String = "", onSelect: (String) -> Unit) { setPositiveButton("添加分类") { createCategory() } - if (default.isNotEmpty()) { - setNegativeButton("删除分类") { - if (default.contentEquals("默认")) { - showToast("不能删除默认分类") + if (favCategories.contains(default)) { + setNegativeButton("编辑分类") { + editCategory(default) { + closeDialog() + openCategorySelect(it, onSelect) } - deleteCategory(default) } } show() } } -fun deleteCategory(name: String) { +fun editCategory(name: String, callback: (String) -> Unit = {}) { + MixDialogBuilder("编辑分类").apply { + var newName by mutableStateOf(name) + + setContent { + OutlinedTextField(value = newName, onValueChange = { + newName = it.substring(0, minOf(it.length, 20)).trim() + }, modifier = Modifier.fillMaxWidth()) + } + setNegativeButton("删除分类") { + deleteCategory(name) { + callback(name) + closeDialog() + } + } + setPositiveButton("确定") { + if (newName.trim().isEmpty()) { + showToast("分类名不能为空") + return@setPositiveButton + } + favCategories -= name + favCategories += newName + currentCategory = newName + showToast("修改分类名称成功") + favorites.forEach { + if (it.category.contentEquals(name)) { + it.updateCategory(newName) + } + } + closeDialog() + callback(newName) + } + show() + } +} + +fun deleteCategory(name: String, callback: (String) -> Unit = {}) { MixDialogBuilder("确定删除分类?").apply { setContent { Text(text = "分类: ${name}") @@ -95,6 +136,7 @@ fun deleteCategory(name: String) { } showToast("删除分类成功") closeDialog() + callback(name) } show() } @@ -109,6 +151,10 @@ fun createCategory() { }, modifier = Modifier.fillMaxWidth()) } setPositiveButton("确认") { + if (name.trim().isEmpty()) { + showToast("分类名不能为空") + return@setPositiveButton + } favCategories += name showToast("添加分类成功") closeDialog() @@ -121,7 +167,11 @@ fun createCategory() { fun exportFileList(fileList: List) { val strData = fileList.toJsonString() val compressedData = compressGzip(strData) - doUploadFile(compressedData, "__mixfile_list") + doUploadFile( + compressedData, + "__mixfile_list_${compressedData.hashSHA256().decodeHex().encodeToBase64().take(8)}", + false + ) } fun showFileList(fileList: List) { @@ -184,8 +234,8 @@ fun importFileList(url: String) { } suspend fun loadFileList(url: String, progressContent: ProgressContent): Array? { - return ignoreError { - localClient.prepareGet { + try { + return localClient.prepareGet { timeout { requestTimeoutMillis = 1000 * 60 * 60 * 24 * 30L } @@ -205,7 +255,12 @@ suspend fun loadFileList(url: String, progressContent: ProgressContent): Array::class.java) } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + showErrorDialog(e, "解析分享列表失败!") + } } + return null } var currentCategory: String by mutableStateOf("") @@ -244,6 +299,10 @@ val Favorites = MixNavPage( searchVal = it }, label = { Text(text = "搜索") }, modifier = Modifier.fillMaxWidth()) + Text( + text = "文件总大小: ${formatFileSize(result.sumOf { it.size })}", + color = colorScheme.primary + ) LaunchedEffect(key1 = searchVal, currentCategory, favorites) { result = if (searchVal.trim().isNotEmpty()) { @@ -271,7 +330,7 @@ val Favorites = MixNavPage( .weight(1.0f) .padding(10.dp, 0.dp) ) { - Text(text = "筛选分类: ${currentCategory.ifEmpty { "全部" }}") + Text(text = "分类: ${currentCategory.ifEmpty { "全部" }.truncate(3)}") } Button( onClick = { @@ -335,6 +394,4 @@ val Favorites = MixNavPage( } } } - - } \ No newline at end of file diff --git a/app/src/main/java/com/donut/mixfile/util/file/FileDataLog.kt b/app/src/main/java/com/donut/mixfile/util/file/FileDataLog.kt index 3786095..6336714 100644 --- a/app/src/main/java/com/donut/mixfile/util/file/FileDataLog.kt +++ b/app/src/main/java/com/donut/mixfile/util/file/FileDataLog.kt @@ -7,12 +7,40 @@ import com.donut.mixfile.ui.routes.autoAddFavorite import com.donut.mixfile.ui.routes.currentCategory import com.donut.mixfile.util.cachedMutableOf import com.donut.mixfile.util.showToast +import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter +import java.io.IOException import java.util.Date + +class TimestampAdapter : TypeAdapter() { + @Throws(IOException::class) + override fun write(out: JsonWriter, value: Date?) { + if (value == null) { + out.nullValue() + return + } + out.value(value.time) + } + + @Throws(IOException::class) + override fun read(`in`: JsonReader): Date? { + if (`in`.peek() === JsonToken.NULL) { + `in`.nextNull() + return null + } + return Date(`in`.nextLong()) + } +} + data class FileDataLog( val shareInfoData: String, val name: String, val size: Long, + @JsonAdapter(TimestampAdapter::class) val time: Date = Date(), var category: String = "默认", ) { 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 7f24303..7343090 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 @@ -64,7 +64,7 @@ import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.coroutineContext -fun doUploadFile(data: Any?, name: String) { +fun doUploadFile(data: Any?, name: String, add: Boolean = true) { MixDialogBuilder( "上传中", properties = DialogProperties( dismissOnClickOutside = false, @@ -91,6 +91,7 @@ fun doUploadFile(data: Any?, name: String) { url("${getLocalServerAddress()}/api/upload") onUpload(progressContent.ktorListener) parameter("name", name) + parameter("add", add) setBody(data) } val message = response.bodyAsText() @@ -201,7 +202,7 @@ fun showFileShareDialog(shareInfo: MixShareInfo, onDismiss: () -> Unit = {}) { }, label = { Text(text = "复制分享码", color = colorScheme.primary) }) - if (shareInfo.fileName.contentEquals("__mixfile_list")) { + if (shareInfo.fileName.startsWith("__mixfile_list")) { AssistChip(onClick = { importFileList(shareInfo.downloadUrl) }, label = { diff --git a/app/src/test/java/com/donut/mixfile/ExampleUnitTest.kt b/app/src/test/java/com/donut/mixfile/ExampleUnitTest.kt index 5d46e50..8758dfd 100644 --- a/app/src/test/java/com/donut/mixfile/ExampleUnitTest.kt +++ b/app/src/test/java/com/donut/mixfile/ExampleUnitTest.kt @@ -1,6 +1,7 @@ package com.donut.mixfile -import com.donut.mixfile.util.formatFileSize +import com.donut.mixfile.util.file.FileDataLog +import com.donut.mixfile.util.toJsonString import kotlinx.coroutines.runBlocking import org.junit.Test