mirror of
https://github.com/InvertGeek/MixFile.git
synced 2026-06-01 17:10:49 +08:00
增加收藏分类以及文件列表分享功能
This commit is contained in:
@@ -14,8 +14,8 @@ android {
|
||||
applicationId = "com.donut.mixfile"
|
||||
minSdk = 24
|
||||
targetSdk = 34
|
||||
versionCode = 22
|
||||
versionName = "1.1.3"
|
||||
versionCode = 23
|
||||
versionName = "1.2.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@
|
||||
<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-9BLoK6H2.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-DVXpF9eB.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BuZ9x1Ok.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.donut.mixfile.ui.component.common
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -163,6 +164,7 @@ fun SingleSelectItemList(
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun <T> SingleSelectItemList(
|
||||
items: List<T>,
|
||||
@@ -182,6 +184,7 @@ fun <T> SingleSelectItemList(
|
||||
onClick = {
|
||||
onSelect(currentItem)
|
||||
},
|
||||
|
||||
selected = selected,
|
||||
leadingIcon = if (selected) {
|
||||
{
|
||||
|
||||
@@ -95,6 +95,7 @@ fun NavComponent() {
|
||||
}
|
||||
}
|
||||
Scaffold(
|
||||
floatingActionButton = currentFloatingButtons,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
modifier = Modifier.clickable {
|
||||
|
||||
@@ -17,6 +17,8 @@ import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -33,6 +35,9 @@ import com.donut.mixfile.util.genRandomString
|
||||
import com.donut.mixfile.util.isNotNull
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
|
||||
var currentFloatingButtons: @Composable () -> Unit by mutableStateOf({})
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class MixNavPage(
|
||||
val name: String = genRandomString(),
|
||||
@@ -40,6 +45,7 @@ class MixNavPage(
|
||||
val modifier: Modifier = Modifier,
|
||||
val useTransition: Boolean = false,
|
||||
val horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||
val floatingButton: @Composable () -> Unit = {},
|
||||
val content: @Composable (NavBackStackEntry) -> Unit,
|
||||
) {
|
||||
|
||||
@@ -63,6 +69,7 @@ class MixNavPage(
|
||||
horizontalAlignment = horizontalAlignment
|
||||
) {
|
||||
content(it)
|
||||
currentFloatingButtons = floatingButton
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,19 @@ package com.donut.mixfile.ui.routes
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -20,14 +27,199 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.donut.mixfile.server.localClient
|
||||
import com.donut.mixfile.ui.component.common.MixDialogBuilder
|
||||
import com.donut.mixfile.ui.component.common.SingleSelectItemList
|
||||
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.decompressGzip
|
||||
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.objects.ProgressContent
|
||||
import com.donut.mixfile.util.showToast
|
||||
import com.donut.mixfile.util.toJsonString
|
||||
import com.google.gson.Gson
|
||||
import io.ktor.client.plugins.onDownload
|
||||
import io.ktor.client.plugins.timeout
|
||||
import io.ktor.client.request.prepareGet
|
||||
import io.ktor.client.request.url
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.contentLength
|
||||
import io.ktor.http.isSuccess
|
||||
import io.ktor.utils.io.core.readBytes
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
fun openCategorySelect(default: String = "", onSelect: (String) -> Unit) {
|
||||
MixDialogBuilder("收藏分类").apply {
|
||||
setContent {
|
||||
SingleSelectItemList(favCategories.toList(), default) {
|
||||
onSelect(it)
|
||||
closeDialog()
|
||||
}
|
||||
}
|
||||
setPositiveButton("添加分类") {
|
||||
createCategory()
|
||||
}
|
||||
if (default.isNotEmpty()) {
|
||||
setNegativeButton("删除分类") {
|
||||
if (default.contentEquals("默认")) {
|
||||
showToast("不能删除默认分类")
|
||||
}
|
||||
deleteCategory(default)
|
||||
}
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteCategory(name: String) {
|
||||
MixDialogBuilder("确定删除分类?").apply {
|
||||
setContent {
|
||||
Text(text = "分类: ${name}")
|
||||
Text(text = "删除后将会移除此分类下所有文件!")
|
||||
}
|
||||
setDefaultNegative()
|
||||
setPositiveButton("确定") {
|
||||
favCategories -= name
|
||||
favorites = favorites.filter {
|
||||
it.category != name
|
||||
}
|
||||
showToast("删除分类成功")
|
||||
closeDialog()
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
fun createCategory() {
|
||||
MixDialogBuilder("新建分类").apply {
|
||||
var name by mutableStateOf("")
|
||||
setContent {
|
||||
OutlinedTextField(value = name, onValueChange = {
|
||||
name = it.substring(0, minOf(it.length, 20)).trim()
|
||||
}, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
setPositiveButton("确认") {
|
||||
favCategories += name
|
||||
showToast("添加分类成功")
|
||||
closeDialog()
|
||||
}
|
||||
setDefaultNegative()
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
fun exportFileList(fileList: List<FileDataLog>) {
|
||||
val strData = fileList.toJsonString()
|
||||
val compressedData = compressGzip(strData)
|
||||
doUploadFile(compressedData, "__mixfile_list")
|
||||
}
|
||||
|
||||
fun showFileList(fileList: List<FileDataLog>) {
|
||||
MixDialogBuilder("文件列表").apply {
|
||||
setContent {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
modifier = Modifier.padding(0.dp)
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(0.dp, 1000.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(0.dp)
|
||||
) {
|
||||
items(fileList.size) { index ->
|
||||
FileCard(fileList[index]) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setPositiveButton("导入文件") {
|
||||
val prevSize = favorites.size
|
||||
fileList.forEach {
|
||||
favCategories += it.category
|
||||
}
|
||||
favorites += fileList
|
||||
favorites = favorites.distinct()
|
||||
showToast("导入了 ${favorites.size - prevSize} 个文件")
|
||||
closeDialog()
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
fun importFileList(url: String) {
|
||||
val progress = ProgressContent()
|
||||
MixDialogBuilder("解析中").apply {
|
||||
setContent {
|
||||
UseEffect {
|
||||
val fileList = loadFileList(url, progress)
|
||||
if (fileList == null) {
|
||||
showToast("解析分享列表失败!")
|
||||
closeDialog()
|
||||
return@UseEffect
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
showFileList(fileList.toList())
|
||||
closeDialog()
|
||||
}
|
||||
}
|
||||
progress.LoadingContent()
|
||||
}
|
||||
setDefaultNegative()
|
||||
show()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
suspend fun loadFileList(url: String, progressContent: ProgressContent): Array<FileDataLog>? {
|
||||
return ignoreError {
|
||||
localClient.prepareGet {
|
||||
timeout {
|
||||
requestTimeoutMillis = 1000 * 60 * 60 * 24 * 30L
|
||||
}
|
||||
url(url)
|
||||
onDownload(progressContent.ktorListener)
|
||||
}.execute {
|
||||
if (!it.status.isSuccess()) {
|
||||
val text = if ((it.contentLength()
|
||||
?: (1024 * 1024)) < 1024 * 500
|
||||
) it.bodyAsText() else "未知错误"
|
||||
throw Exception("下载失败: ${text}")
|
||||
}
|
||||
if ((it.contentLength() ?: 0) > 1024 * 1024 * 50) {
|
||||
throw Exception("文件过大")
|
||||
}
|
||||
val data = it.bodyAsChannel().readRemaining(1024 * 1024 * 50).readBytes()
|
||||
val extractedData = decompressGzip(data)
|
||||
return@execute Gson().fromJson(extractedData, Array<FileDataLog>::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var currentCategory: String by mutableStateOf("")
|
||||
|
||||
val Favorites = MixNavPage(
|
||||
gap = 10.dp,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
floatingButton = {
|
||||
FloatingActionButton(onClick = {
|
||||
selectAndUploadFile()
|
||||
}, modifier = Modifier.padding(10.dp, 50.dp)) {
|
||||
Icon(Icons.Filled.Add, "Upload File")
|
||||
}
|
||||
}
|
||||
) {
|
||||
|
||||
var searchVal by remember {
|
||||
@@ -38,16 +230,6 @@ val Favorites = MixNavPage(
|
||||
mutableStateOf(favorites.reversed())
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = searchVal) {
|
||||
if (searchVal.trim().isNotEmpty()) {
|
||||
result = favorites.filter {
|
||||
it.name.contains(searchVal)
|
||||
}.reversed()
|
||||
} else {
|
||||
result = favorites.reversed()
|
||||
}
|
||||
}
|
||||
|
||||
if (favorites.isEmpty()) {
|
||||
Text(
|
||||
text = "暂未收藏文件",
|
||||
@@ -62,6 +244,67 @@ val Favorites = MixNavPage(
|
||||
searchVal = it
|
||||
}, label = { Text(text = "搜索") }, modifier = Modifier.fillMaxWidth())
|
||||
|
||||
|
||||
LaunchedEffect(key1 = searchVal, currentCategory, favorites) {
|
||||
result = if (searchVal.trim().isNotEmpty()) {
|
||||
favorites.filter {
|
||||
it.name.contains(searchVal)
|
||||
}.reversed()
|
||||
} else {
|
||||
favorites.reversed()
|
||||
}
|
||||
result = result.filter {
|
||||
currentCategory.isEmpty() || it.category == currentCategory
|
||||
}
|
||||
}
|
||||
Row {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
openCategorySelect(currentCategory) {
|
||||
currentCategory = if (it.contentEquals(currentCategory)) {
|
||||
""
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}, modifier = Modifier
|
||||
.weight(1.0f)
|
||||
.padding(10.dp, 0.dp)
|
||||
) {
|
||||
Text(text = "筛选分类: ${currentCategory.ifEmpty { "全部" }}")
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
MixDialogBuilder("确定导出?").apply {
|
||||
setContent {
|
||||
Text(text = "将会导出当前筛选的文件列表上传为一键分享链接")
|
||||
}
|
||||
setDefaultNegative()
|
||||
setPositiveButton("确定") {
|
||||
exportFileList(result)
|
||||
closeDialog()
|
||||
}
|
||||
show()
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1.0f)
|
||||
.padding(10.dp, 0.dp),
|
||||
) {
|
||||
Text(text = "导出文件")
|
||||
}
|
||||
}
|
||||
if (result.isEmpty()) {
|
||||
Text(
|
||||
text = "没有搜索到文件",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = colorScheme.primary
|
||||
)
|
||||
return@MixNavPage
|
||||
}
|
||||
|
||||
ElevatedCard(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
|
||||
@@ -15,11 +15,13 @@ import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
@@ -51,14 +53,23 @@ import com.donut.mixfile.util.file.uploadLogs
|
||||
import com.donut.mixfile.util.formatFileSize
|
||||
import com.donut.mixfile.util.formatTime
|
||||
import com.donut.mixfile.util.getIpAddressInLocalNetwork
|
||||
import com.donut.mixfile.util.isFalse
|
||||
import com.donut.mixfile.util.readClipBoardText
|
||||
import com.donut.mixfile.util.showToast
|
||||
|
||||
var serverAddress by mutableStateOf("http://${getIpAddressInLocalNetwork()}:${serverPort}")
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class, ExperimentalFoundationApi::class)
|
||||
val Home = MixNavPage(
|
||||
gap = 10.dp,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
floatingButton = {
|
||||
FloatingActionButton(onClick = {
|
||||
selectAndUploadFile()
|
||||
}, modifier = Modifier.padding(10.dp, 50.dp)) {
|
||||
Icon(Icons.Filled.Add, "Upload File")
|
||||
}
|
||||
}
|
||||
) {
|
||||
var text by remember {
|
||||
mutableStateOf("")
|
||||
@@ -120,13 +131,15 @@ val Home = MixNavPage(
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
selectAndUploadFile()
|
||||
tryResolveFile(text.trim()).isFalse {
|
||||
showToast("解析失败!")
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1.0f)
|
||||
.padding(10.dp, 0.dp),
|
||||
) {
|
||||
Text(text = "上传文件")
|
||||
Text(text = "解析文件")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +181,11 @@ val Home = MixNavPage(
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun FileCard(fileDataLog: FileDataLog, showDate: Boolean = true, longClick: () -> Unit = {}) {
|
||||
fun FileCard(
|
||||
fileDataLog: FileDataLog,
|
||||
showDate: Boolean = true,
|
||||
longClick: () -> Unit = {},
|
||||
) {
|
||||
HorizontalDivider()
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
|
||||
@@ -50,6 +50,7 @@ import com.donut.mixfile.util.showToast
|
||||
|
||||
|
||||
var useShortCode by cachedMutableOf(true, "use_short_code")
|
||||
var autoAddFavorite by cachedMutableOf(true, "auto_add_favorite")
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
@@ -145,6 +146,9 @@ val MixSettings = MixNavPage(
|
||||
CommonSwitch(checked = useShortCode, text = "使用短分享码(空白字符编码信息):") {
|
||||
useShortCode = it
|
||||
}
|
||||
CommonSwitch(checked = autoAddFavorite, text = "上传后自动添加文件到默认收藏:") {
|
||||
autoAddFavorite = it
|
||||
}
|
||||
HorizontalDivider()
|
||||
ElevatedButton(onClick = {
|
||||
MixDialogBuilder("确定清除记录?").apply {
|
||||
|
||||
@@ -19,7 +19,7 @@ val LightColorScheme = lightColorScheme(
|
||||
secondary = Color(0xFF625b71),
|
||||
tertiary = Color(0xFF7D5260),
|
||||
// tertiaryContainer = Color(0xFFF0004E),
|
||||
// primaryContainer = Color(0xFFF0004E),
|
||||
primaryContainer = Color(0xFF99CEFC),
|
||||
secondaryContainer = Color(0x3662B5E8),
|
||||
background = Color(0xFFE6DFEB),
|
||||
surface = Color(0xFFE6DFEB),
|
||||
|
||||
@@ -5,7 +5,8 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.donut.mixfile.kv
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
|
||||
fun <T> constructCachedMutableValue(
|
||||
value: T,
|
||||
@@ -52,21 +53,19 @@ fun cachedMutableOf(value: Parcelable, key: String) =
|
||||
{ kv.encode(key, it) },
|
||||
{ kv.decodeParcelable(key, value.javaClass) })
|
||||
|
||||
@Parcelize
|
||||
data class ParcelableItemList<T : Parcelable>(
|
||||
val items: List<T>,
|
||||
) : Parcelable
|
||||
|
||||
inline fun <reified T : Parcelable> cachedMutableOf(value: List<T>, key: String) =
|
||||
inline fun <reified T> cachedMutableOf(value: List<T>, key: String) =
|
||||
constructCachedMutableValue(
|
||||
value,
|
||||
key,
|
||||
{ kv.encode(key, ParcelableItemList(it)) },
|
||||
{ kv.encode(key, it.toJsonString()) },
|
||||
getter@{
|
||||
val data =
|
||||
kv.decodeParcelable(key, ParcelableItemList::class.java) ?: return@getter value
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return@getter data.items as List<T>
|
||||
var result = listOf<T>()
|
||||
val type = object : TypeToken<List<T>>() {}.type
|
||||
ignoreError {
|
||||
val json: List<T> = Gson().fromJson(kv.decodeString(key), type)
|
||||
result = json
|
||||
}
|
||||
return@getter result
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -114,7 +113,7 @@ fun TipText(content: String, onClick: () -> Unit = {}) {
|
||||
|
||||
@Composable
|
||||
fun OnResume(block: () -> Unit) {
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
|
||||
val lifecycleObserver = remember {
|
||||
LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
|
||||
@@ -1,26 +1,42 @@
|
||||
package com.donut.mixfile.util.file
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.material3.Text
|
||||
import com.donut.mixfile.server.utils.bean.MixShareInfo
|
||||
import com.donut.mixfile.ui.component.common.MixDialogBuilder
|
||||
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 kotlinx.parcelize.Parcelize
|
||||
import java.util.Date
|
||||
|
||||
@Parcelize
|
||||
data class FileDataLog(
|
||||
val shareInfoData: String,
|
||||
val name: String,
|
||||
val size: Long,
|
||||
val time: Date = Date(),
|
||||
) : Parcelable {
|
||||
var category: String = "默认",
|
||||
) {
|
||||
|
||||
init {
|
||||
//限制category长度
|
||||
if (category.length > 20) {
|
||||
category = category.substring(0, 20)
|
||||
}
|
||||
category = category.trim()
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return shareInfoData.hashCode()
|
||||
}
|
||||
|
||||
fun updateCategory(category: String) {
|
||||
favorites = favorites.toMutableList().apply {
|
||||
remove(this@FileDataLog)
|
||||
}
|
||||
this.category = category
|
||||
favorites = favorites + this.copy()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is FileDataLog) return false
|
||||
return shareInfoData.contentEquals(other.shareInfoData)
|
||||
@@ -31,20 +47,29 @@ var uploadLogs by cachedMutableOf(listOf<FileDataLog>(), "upload_file_logs")
|
||||
|
||||
var favorites by cachedMutableOf(listOf<FileDataLog>(), "favorite_file_logs")
|
||||
|
||||
var favCategories by cachedMutableOf(setOf("默认"), "fav_categories")
|
||||
|
||||
fun isFavorite(shareInfo: MixShareInfo): Boolean {
|
||||
return favorites.contains(shareInfo.toDataLog())
|
||||
}
|
||||
|
||||
fun addFavoriteLog(shareInfo: MixShareInfo) {
|
||||
fun addFavoriteLog(
|
||||
shareInfo: MixShareInfo,
|
||||
category: String = currentCategory.ifEmpty { "默认" },
|
||||
): Boolean {
|
||||
if (favorites.size > 10000) {
|
||||
showToast("收藏已达到限制!")
|
||||
return false
|
||||
}
|
||||
val favoriteLog = shareInfo.toDataLog()
|
||||
favoriteLog.category = category
|
||||
favCategories += category
|
||||
if (favorites.any { it == favoriteLog }) {
|
||||
favorites = favorites.filter { it != favoriteLog } + favoriteLog
|
||||
return
|
||||
}
|
||||
if (favorites.size > 1000) {
|
||||
favorites = favorites.drop(1)
|
||||
return true
|
||||
}
|
||||
favorites = favorites + favoriteLog
|
||||
return true
|
||||
}
|
||||
|
||||
fun MixShareInfo.toDataLog(): FileDataLog {
|
||||
@@ -56,6 +81,9 @@ fun MixShareInfo.toDataLog(): FileDataLog {
|
||||
}
|
||||
|
||||
fun addUploadLog(shareInfo: MixShareInfo) {
|
||||
if (autoAddFavorite) {
|
||||
addFavoriteLog(shareInfo)
|
||||
}
|
||||
val uploadLog = shareInfo.toDataLog()
|
||||
if (uploadLogs.size > 1000) {
|
||||
uploadLogs = uploadLogs.drop(1)
|
||||
|
||||
@@ -33,6 +33,8 @@ import com.donut.mixfile.server.localClient
|
||||
import com.donut.mixfile.server.utils.bean.MixShareInfo
|
||||
import com.donut.mixfile.ui.component.common.MixDialogBuilder
|
||||
import com.donut.mixfile.ui.routes.getLocalServerAddress
|
||||
import com.donut.mixfile.ui.routes.importFileList
|
||||
import com.donut.mixfile.ui.routes.openCategorySelect
|
||||
import com.donut.mixfile.ui.routes.tryResolveFile
|
||||
import com.donut.mixfile.ui.theme.colorScheme
|
||||
import com.donut.mixfile.util.UseEffect
|
||||
@@ -62,64 +64,68 @@ import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
|
||||
fun doUploadFile(data: Any?, name: String) {
|
||||
MixDialogBuilder(
|
||||
"上传中", properties = DialogProperties(
|
||||
dismissOnClickOutside = false,
|
||||
dismissOnBackPress = false
|
||||
)
|
||||
).apply {
|
||||
setContent {
|
||||
val progressContent = remember {
|
||||
ProgressContent("上传中")
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
progressContent.LoadingContent()
|
||||
}
|
||||
UseEffect {
|
||||
errorDialog("上传失败") {
|
||||
val response = localClient.put {
|
||||
timeout {
|
||||
requestTimeoutMillis = 1000 * 60 * 60 * 24 * 30L
|
||||
}
|
||||
url("${getLocalServerAddress()}/api/upload")
|
||||
onUpload(progressContent.ktorListener)
|
||||
parameter("name", name)
|
||||
setBody(data)
|
||||
}
|
||||
val message = response.bodyAsText()
|
||||
if (!response.status.isSuccess()) {
|
||||
throw Exception("上传失败: $message")
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
tryResolveFile(message)
|
||||
}
|
||||
showToast("上传成功!")
|
||||
}
|
||||
closeDialog()
|
||||
}
|
||||
}
|
||||
setNegativeButton("取消") {
|
||||
showToast("上传已取消")
|
||||
closeDialog()
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("Recycle")
|
||||
fun selectAndUploadFile() {
|
||||
MainActivity.mixFileSelector.openSelect { uri ->
|
||||
MixDialogBuilder(
|
||||
"上传中", properties = DialogProperties(
|
||||
dismissOnClickOutside = false,
|
||||
dismissOnBackPress = false
|
||||
)
|
||||
).apply {
|
||||
setContent {
|
||||
val progressContent = remember {
|
||||
ProgressContent("上传中")
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
progressContent.LoadingContent()
|
||||
}
|
||||
UseEffect {
|
||||
val resolver = app.contentResolver
|
||||
val fileDescriptor: AssetFileDescriptor? =
|
||||
resolver.openAssetFileDescriptor(uri, "r")
|
||||
val fileSize = fileDescriptor?.length ?: 0
|
||||
errorDialog("上传失败") {
|
||||
val response = localClient.put {
|
||||
timeout {
|
||||
requestTimeoutMillis = 1000 * 60 * 60 * 24 * 30L
|
||||
}
|
||||
url("${getLocalServerAddress()}/api/upload")
|
||||
onUpload(progressContent.ktorListener)
|
||||
val fileStream = resolver.openInputStream(uri)
|
||||
if (fileStream == null) {
|
||||
showToast("打开文件失败")
|
||||
return@put
|
||||
}
|
||||
parameter("name", uri.getFileName())
|
||||
setBody(StreamContent(fileStream, fileSize))
|
||||
}
|
||||
val message = response.bodyAsText()
|
||||
if (!response.status.isSuccess()) {
|
||||
throw Exception("上传失败: $message")
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
tryResolveFile(message)
|
||||
}
|
||||
showToast("上传成功!")
|
||||
}
|
||||
closeDialog()
|
||||
}
|
||||
}
|
||||
setNegativeButton("取消") {
|
||||
showToast("上传已取消")
|
||||
closeDialog()
|
||||
}
|
||||
show()
|
||||
val resolver = app.contentResolver
|
||||
val fileDescriptor: AssetFileDescriptor? =
|
||||
resolver.openAssetFileDescriptor(uri, "r")
|
||||
val fileSize = fileDescriptor?.length ?: 0
|
||||
val fileStream = resolver.openInputStream(uri)
|
||||
if (fileStream == null) {
|
||||
showToast("打开文件失败")
|
||||
return@openSelect
|
||||
}
|
||||
doUploadFile(StreamContent(fileStream, fileSize), uri.getFileName())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +201,13 @@ fun showFileShareDialog(shareInfo: MixShareInfo, onDismiss: () -> Unit = {}) {
|
||||
}, label = {
|
||||
Text(text = "复制分享码", color = colorScheme.primary)
|
||||
})
|
||||
if (shareInfo.fileName.contentEquals("__mixfile_list")) {
|
||||
AssistChip(onClick = {
|
||||
importFileList(shareInfo.downloadUrl)
|
||||
}, label = {
|
||||
Text(text = "文件列表", color = colorScheme.primary)
|
||||
})
|
||||
}
|
||||
if (!isFavorite(shareInfo)) {
|
||||
AssistChip(onClick = {
|
||||
addFavoriteLog(shareInfo)
|
||||
@@ -202,11 +215,24 @@ fun showFileShareDialog(shareInfo: MixShareInfo, onDismiss: () -> Unit = {}) {
|
||||
Text(text = "收藏", color = colorScheme.primary)
|
||||
})
|
||||
} else {
|
||||
val dataLog = remember(shareInfo) {
|
||||
favorites.firstOrNull { it == shareInfo.toDataLog() }
|
||||
}
|
||||
AssistChip(onClick = {
|
||||
deleteFavoriteLog(shareInfo.toDataLog())
|
||||
}, label = {
|
||||
Text(text = "取消收藏", color = colorScheme.primary)
|
||||
})
|
||||
AssistChip(onClick = {
|
||||
openCategorySelect(dataLog?.category ?: "默认") {
|
||||
dataLog?.updateCategory(it)
|
||||
}
|
||||
}, label = {
|
||||
Text(
|
||||
text = "分类: ${dataLog?.category}",
|
||||
color = colorScheme.primary
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (shareInfo.contentType().startsWith("video/")) {
|
||||
|
||||
Reference in New Issue
Block a user