diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7cfc0ff..f503e50 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 = 59 - versionName = "1.7.0" + versionCode = 60 + versionName = "1.8.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/com/donut/mixfile/server/uploaders/CustomUploader.kt b/app/src/main/java/com/donut/mixfile/server/uploaders/CustomUploader.kt index aec362e..cc13571 100644 --- a/app/src/main/java/com/donut/mixfile/server/uploaders/CustomUploader.kt +++ b/app/src/main/java/com/donut/mixfile/server/uploaders/CustomUploader.kt @@ -8,7 +8,6 @@ import io.ktor.client.request.put import io.ktor.client.request.setBody import io.ktor.client.request.url import io.ktor.client.statement.bodyAsText -import io.ktor.client.statement.readBytes import io.ktor.client.statement.readRawBytes import io.ktor.http.isSuccess import kotlinx.coroutines.Dispatchers @@ -22,15 +21,15 @@ object CustomUploader : Uploader("自定义") { override suspend fun genHead(): ByteArray { return uploadClient.get { - url(CUSTOM_UPLOAD_URL) - }.also { - val referer = it.headers["referer"] - if (!referer.isNullOrEmpty()) { - withContext(Dispatchers.Main) { - CUSTOM_REFERER = referer - } + url(CUSTOM_UPLOAD_URL) + }.also { + val referer = it.headers["referer"] + if (!referer.isNullOrEmpty()) { + withContext(Dispatchers.Main) { + CUSTOM_REFERER = referer } - }.readRawBytes() + } + }.readRawBytes() } override val referer: String diff --git a/app/src/main/java/com/donut/mixfile/ui/component/common/Common.kt b/app/src/main/java/com/donut/mixfile/ui/component/common/Common.kt index 8e0b762..ab45ce8 100644 --- a/app/src/main/java/com/donut/mixfile/ui/component/common/Common.kt +++ b/app/src/main/java/com/donut/mixfile/ui/component/common/Common.kt @@ -168,7 +168,7 @@ fun SingleSelectItemList( @Composable fun SingleSelectItemList( items: List, - currentOption: T?, + currentOption: T? = null, getLabel: (option: T) -> String, onSelect: (option: T) -> Unit, ) { diff --git a/app/src/main/java/com/donut/mixfile/ui/routes/About.kt b/app/src/main/java/com/donut/mixfile/ui/routes/About.kt index 7c187a6..b187d10 100644 --- a/app/src/main/java/com/donut/mixfile/ui/routes/About.kt +++ b/app/src/main/java/com/donut/mixfile/ui/routes/About.kt @@ -103,14 +103,14 @@ val About = MixNavPage( } Text( color = colorScheme.primary, - text = "项目地址: https://gitlab.com/ivgeek/MixFile", + text = "项目地址: https://github.com/InvertGeek/MixMessage", modifier = Modifier.clickable { MixDialogBuilder("确定打开?").apply { setPositiveButton("确定") { val intent = Intent( Intent.ACTION_VIEW, - Uri.parse("https://gitlab.com/ivgeek/MixFile") + Uri.parse("https://github.com/InvertGeek/MixMessage") ).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK } diff --git a/app/src/main/java/com/donut/mixfile/ui/routes/UploadDialog.kt b/app/src/main/java/com/donut/mixfile/ui/routes/UploadDialog.kt index d019186..7288cd7 100644 --- a/app/src/main/java/com/donut/mixfile/ui/routes/UploadDialog.kt +++ b/app/src/main/java/com/donut/mixfile/ui/routes/UploadDialog.kt @@ -19,13 +19,13 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.donut.mixfile.ui.component.common.MixDialogBuilder -import com.donut.mixfile.ui.routes.home.TaskCard +import com.donut.mixfile.ui.routes.home.UploadTaskCard import com.donut.mixfile.ui.routes.home.uploadTasks import com.donut.mixfile.ui.theme.colorScheme import com.donut.mixfile.util.file.cancelAllMultiUpload -import com.donut.mixfile.util.file.successFileCount -import com.donut.mixfile.util.file.totalFileCount +import com.donut.mixfile.util.file.totalUploadFileCount import com.donut.mixfile.util.file.uploadQueue +import com.donut.mixfile.util.file.uploadSuccessFileCount import com.donut.mixfile.util.objects.AnimatedLoadingBar import com.donut.mixfile.util.showConfirmDialog import com.donut.mixfile.util.showToast @@ -41,11 +41,11 @@ fun showUploadTaskWindow() { Column( modifier = Modifier.fillMaxSize(), ) { - if (totalFileCount > 1) { - val progress = successFileCount.toFloat() / totalFileCount + if (totalUploadFileCount > 1) { + val progress = uploadSuccessFileCount.toFloat() / totalUploadFileCount AnimatedLoadingBar( progress = progress, - label = "总进度: ${successFileCount}/${totalFileCount} " + + label = "总进度: ${uploadSuccessFileCount}/${totalUploadFileCount} " + "正在上传: ${uploadTasks.filter { it.uploading }.size} " + "排队中: $uploadQueue" ) @@ -54,8 +54,8 @@ fun showUploadTaskWindow() { verticalArrangement = Arrangement.spacedBy(0.dp), modifier = Modifier.padding(0.dp) ) { - uploadTasks.forEach { - TaskCard(uploadTask = it) { + uploadTasks.take(10).forEach { + UploadTaskCard(uploadTask = it) { it.delete() } } diff --git a/app/src/main/java/com/donut/mixfile/ui/routes/favorites/Favorites.kt b/app/src/main/java/com/donut/mixfile/ui/routes/favorites/Favorites.kt index ff23540..a153a82 100644 --- a/app/src/main/java/com/donut/mixfile/ui/routes/favorites/Favorites.kt +++ b/app/src/main/java/com/donut/mixfile/ui/routes/favorites/Favorites.kt @@ -1,19 +1,23 @@ package com.donut.mixfile.ui.routes.favorites +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource 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.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.outlined.Close import androidx.compose.material3.Button 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 import androidx.compose.material3.OutlinedTextField @@ -30,18 +34,25 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp 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.routes.UploadDialogCard +import com.donut.mixfile.ui.routes.home.DownloadDialogCard +import com.donut.mixfile.ui.routes.home.showDownloadTaskWindow +import com.donut.mixfile.ui.routes.home.tryResolveFile import com.donut.mixfile.ui.theme.colorScheme import com.donut.mixfile.util.cachedMutableOf import com.donut.mixfile.util.file.FileCardList -import com.donut.mixfile.util.file.deleteFavoriteLog -import com.donut.mixfile.util.file.exportFileList +import com.donut.mixfile.util.file.FileDataLog +import com.donut.mixfile.util.file.downloadFile import com.donut.mixfile.util.file.favorites +import com.donut.mixfile.util.file.resolveMixShareInfo import com.donut.mixfile.util.file.selectAndUploadFile +import com.donut.mixfile.util.file.showExportFileListDialog import com.donut.mixfile.util.formatFileSize -import com.donut.mixfile.util.getCurrentTime import com.donut.mixfile.util.parseSortNum +import com.donut.mixfile.util.showConfirmDialog +import com.donut.mixfile.util.showToast import com.donut.mixfile.util.truncate import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -52,6 +63,7 @@ var currentCategory: String by mutableStateOf("") private var favoriteSort by cachedMutableOf("最新", "mix_favorite_sort") +@OptIn(ExperimentalFoundationApi::class) val Favorites = MixNavPage( gap = 10.dp, horizontalAlignment = Alignment.CenterHorizontally, @@ -116,9 +128,9 @@ val Favorites = MixNavPage( result = if (searchVal.trim().isNotEmpty()) { favorites.filter { it.name.contains(searchVal) - }.reversed() + }.asReversed() } else { - favorites.reversed() + favorites.asReversed() } result = result.filter { currentCategory.isEmpty() || it.category == currentCategory @@ -159,28 +171,7 @@ val Favorites = MixNavPage( } Button( onClick = { - MixDialogBuilder("确定导出?").apply { - var listName by mutableStateOf("文件列表-${getCurrentTime()}") - setContent { - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(5.dp) - ) { - OutlinedTextField(value = listName, onValueChange = { - listName = it - }, modifier = Modifier.fillMaxWidth(), label = { - Text(text = "列表名称") - }) - Text(text = "将会导出当前筛选的文件列表上传为一键分享链接") - } - } - setDefaultNegative() - setPositiveButton("确定") { - exportFileList(result, listName) - closeDialog() - } - show() - } + showExportFileListDialog(result) }, modifier = Modifier .weight(1.0f) @@ -190,6 +181,7 @@ val Favorites = MixNavPage( } } UploadDialogCard() + DownloadDialogCard() if (result.isEmpty()) { Text( text = "没有搜索到文件", @@ -201,6 +193,100 @@ val Favorites = MixNavPage( return@MixNavPage } + var multiSelect by remember { + mutableStateOf(false) + } + + var selected by remember { + mutableStateOf(setOf()) + } + + LaunchedEffect(multiSelect) { + if (!multiSelect) { + selected = setOf() + } + } + + LaunchedEffect(selected) { + if (selected.isEmpty()) { + multiSelect = false + } + } + + AnimatedVisibility(visible = multiSelect) { + ElevatedCard( + modifier = Modifier + .fillMaxWidth(), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .combinedClickable(onLongClick = { + selected = emptySet() + }) { + showFileListActionDialog( + listOf( + Pair("删除") { + showConfirmDialog("确定删除?") { + favorites = favorites - selected + showToast("删除成功") + selected = emptySet() + } + }, + Pair("全选") { + selected += result + }, + Pair("取消选择") { + selected = emptySet() + }, + Pair("导出文件") { + showExportFileListDialog(selected) + selected = emptySet() + }, + Pair("全部下载") { + selected.forEach { + val shareInfo = resolveMixShareInfo(it.shareInfoData) + if (shareInfo != null) { + downloadFile(shareInfo) + } + } + showDownloadTaskWindow() + selected = emptySet() + }, + Pair("移动分类") { + openCategorySelect(selected.first().category) { category -> + favorites = favorites.map { + if (selected.contains(it)) + it.copy(category = category) + else + it + } + selected = emptySet() + } + }, + ), + ) + } + .fillMaxSize() + .padding(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "已选择 ${selected.size} 个文件", + modifier = Modifier, + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = colorScheme.primary + ) + Icon( + Icons.Filled.Edit, + contentDescription = "selected", + tint = colorScheme.primary + ) + } + } + } + ElevatedCard( modifier = Modifier.fillMaxSize(), ) { @@ -218,8 +304,40 @@ val Favorites = MixNavPage( fontWeight = FontWeight.Bold, color = colorScheme.primary, ) - FileCardList(cardList = result) { - deleteFavoriteLog(it) + HorizontalDivider() + FileCardList( + cardList = result, + selected = selected, + onClick = { + if (!multiSelect) { + tryResolveFile(it.shareInfoData) + return@FileCardList + } + if (!selected.contains(it)) { + selected += it + return@FileCardList + } + selected -= it + }) { + multiSelect = true + selected += it } } +} + +fun showFileListActionDialog(options: List Unit>>) { + MixDialogBuilder("编辑文件").apply { + setContent { + SingleSelectItemList( + items = options, + getLabel = { + it.first + }, + ) { option -> + option.second() + closeDialog() + } + } + show() + } } \ No newline at end of file diff --git a/app/src/main/java/com/donut/mixfile/ui/routes/home/DownloadTask.kt b/app/src/main/java/com/donut/mixfile/ui/routes/home/DownloadTask.kt new file mode 100644 index 0000000..e46107e --- /dev/null +++ b/app/src/main/java/com/donut/mixfile/ui/routes/home/DownloadTask.kt @@ -0,0 +1,303 @@ +package com.donut.mixfile.ui.routes.home + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.donut.mixfile.appScope +import com.donut.mixfile.ui.component.common.MixDialogBuilder +import com.donut.mixfile.ui.theme.colorScheme +import com.donut.mixfile.util.file.InfoText +import com.donut.mixfile.util.file.saveFileToStorage +import com.donut.mixfile.util.formatFileSize +import com.donut.mixfile.util.objects.AnimatedLoadingBar +import com.donut.mixfile.util.objects.ProgressContent +import com.donut.mixfile.util.showConfirmDialog +import com.donut.mixfile.util.showErrorDialog +import com.donut.mixfile.util.showToast +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.withContext + +@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) +@Composable +fun DownloadTaskCard( + downloadTask: DownloadTask, + longClick: () -> Unit = {}, +) { + HorizontalDivider() + Card( + colors = CardDefaults.cardColors( + containerColor = Color(107, 218, 246, 0), + ), + modifier = Modifier + .fillMaxWidth() + .combinedClickable( + onLongClick = { + longClick() + } + ) { + downloadTask.cancel() + } + ) { + Column( + modifier = Modifier.padding(10.dp), + verticalArrangement = Arrangement.spacedBy(5.dp) + ) { + Text( + text = downloadTask.fileName, + color = colorScheme.primary, + fontSize = 16.sp, + ) + FlowRow( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + InfoText(key = "大小: ", value = formatFileSize(downloadTask.fileSize)) + downloadTask.State() + } + if (downloadTask.started && !downloadTask.stopped) { + downloadTask.progress.LoadingContent() + } + } + } +} + +var downloadTasks by mutableStateOf(listOf()) + +var downloadSemaphore = Semaphore(3) + +class DownloadTask( + val fileName: String, + val fileSize: Long, + val url: String, +) { + var progress = ProgressContent("下载中", 14.sp, colorScheme.secondary, false) + + var job: Job? = null + + + var stopped by mutableStateOf(false) + var started by mutableStateOf(false) + + var error: Throwable? by mutableStateOf(null) + + init { + appScope.launch { + downloadTasks += this@DownloadTask + } + totalDownloadFileCount++ + progress.contentLength = fileSize + } + + @Composable + fun State() { + if (stopped) { + if (error is CancellationException) { + return Text(text = "下载取消", color = colorScheme.error) + } + if (error != null) { + return Text(text = "下载失败", color = colorScheme.error) + } + return Text(text = "下载成功", color = colorScheme.primary) + } + if (started) { + return Text(text = "下载中", color = colorScheme.primary) + } + return Text(text = "等待中", color = colorScheme.primary) + } + + + fun delete() { + MixDialogBuilder("删除记录?").apply { + setContent { + Text(text = "文件: ${fileName}") + } + val currentError = error + if (currentError != null) { + setNegativeButton("查看错误信息") { + showErrorDialog(currentError, "错误信息") + } + } + setPositiveButton("确定") { + stop() + downloadTasks -= this@DownloadTask + closeDialog() + } + show() + } + } + + fun stop() { + if (stopped) { + return + } + job?.cancel("下载取消") + stopped = true + } + + fun start() { + downloadQueue++ + job = appScope.launch(Dispatchers.IO) { + downloadSemaphore.acquire() + withContext(Dispatchers.Main) { + downloadQueue-- + } + if (stopped) { + return@launch + } + started = true + saveFileToStorage( + url, + displayName = fileName, + progress = progress + ) + downloadSuccessFileCount++ + } + job?.invokeOnCompletion { + error = it + stopped = true + downloadSemaphore.release() + } + + } + + fun cancel() { + if (stopped) { + delete() + return + } + MixDialogBuilder("取消下载?").apply { + setContent { + Text(text = "文件: ${fileName}") + } + setPositiveButton("确定") { + stop() + closeDialog() + showToast("下载已取消") + } + show() + } + } +} + +var downloadQueue by mutableIntStateOf(0) +var totalDownloadFileCount by mutableIntStateOf(0) +var downloadSuccessFileCount by mutableIntStateOf(0) + +fun cancelAllDownloads() { + downloadQueue = 0 + downloadTasks.forEach { it.stop() } + totalDownloadFileCount = 0 + downloadSuccessFileCount = 0 +} + +fun showDownloadTaskWindow() { + MixDialogBuilder("下载中的文件").apply { + setContent { + if (downloadTasks.isEmpty()) { + Text(text = "没有下载中的文件") + return@setContent + } + Column( + modifier = Modifier.fillMaxSize(), + ) { + if (totalDownloadFileCount > 1) { + val progress = downloadSuccessFileCount.toFloat() / totalDownloadFileCount + AnimatedLoadingBar( + progress = progress, + label = "总进度: $downloadSuccessFileCount/$totalDownloadFileCount " + + "正在下载: ${downloadTasks.filter { it.started && !it.stopped }.size} " + + "排队中: $downloadQueue" + ) + } + Column( + verticalArrangement = Arrangement.spacedBy(0.dp), + modifier = Modifier.padding(0.dp) + ) { + downloadTasks.sortedBy { + if (it.started && !it.stopped) 0 else 1 + }.take(5).forEach { + DownloadTaskCard(it) { + it.delete() + } + } + } + } + } + setPositiveButton("清除已完成任务") { + downloadTasks = downloadTasks.filter { !it.stopped } + showToast("清除成功") + } + setNegativeButton("全部取消") { + showConfirmDialog("确定取消全部下载任务?") { + cancelAllDownloads() + showToast("取消成功") + } + } + show() + } +} + +@Composable +fun DownloadDialogCard() { + val downloading = downloadTasks.filter { !it.stopped } + AnimatedVisibility(visible = downloading.isNotEmpty()) { + ElevatedCard( + modifier = Modifier + .fillMaxWidth(), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier + .clickable { + showDownloadTaskWindow() + } + .fillMaxSize() + .padding(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (downloading.isNotEmpty()) { + Text( + text = "${downloading.size} 个文件正在下载中", + modifier = Modifier, + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = colorScheme.primary + ) + CircularProgressIndicator(modifier = Modifier.size(20.dp)) + } + } + } + } +} diff --git a/app/src/main/java/com/donut/mixfile/ui/routes/home/Home.kt b/app/src/main/java/com/donut/mixfile/ui/routes/home/Home.kt index b731019..874aa1c 100644 --- a/app/src/main/java/com/donut/mixfile/ui/routes/home/Home.kt +++ b/app/src/main/java/com/donut/mixfile/ui/routes/home/Home.kt @@ -14,6 +14,7 @@ import androidx.compose.material.icons.outlined.Close import androidx.compose.material3.Button 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 import androidx.compose.material3.OutlinedTextField @@ -132,6 +133,7 @@ val Home = MixNavPage( } } UploadDialogCard() + DownloadDialogCard() if (uploadLogs.isNotEmpty()) { ElevatedCard( modifier = Modifier.fillMaxSize(), @@ -145,7 +147,8 @@ val Home = MixNavPage( fontWeight = FontWeight.Bold, color = colorScheme.primary ) - FileCardList(cardList = uploadLogs.reversed()) { + HorizontalDivider() + FileCardList(cardList = uploadLogs.asReversed()) { deleteUploadLog(it) } } diff --git a/app/src/main/java/com/donut/mixfile/ui/routes/home/UploadTask.kt b/app/src/main/java/com/donut/mixfile/ui/routes/home/UploadTask.kt index 03fccd6..884d759 100644 --- a/app/src/main/java/com/donut/mixfile/ui/routes/home/UploadTask.kt +++ b/app/src/main/java/com/donut/mixfile/ui/routes/home/UploadTask.kt @@ -37,11 +37,10 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.util.Date @OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) @Composable -fun TaskCard( +fun UploadTaskCard( uploadTask: UploadTask, longClick: () -> Unit = {}, ) { @@ -95,7 +94,6 @@ class UploadTask( val fileName: String, val fileSize: Long, val add: Boolean = true, - val time: Date = Date(), ) { var progress = ProgressContent("上传中", 14.sp, colorScheme.secondary, false) 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 28d92d7..a0672f7 100644 --- a/app/src/main/java/com/donut/mixfile/util/CommonUtil.kt +++ b/app/src/main/java/com/donut/mixfile/util/CommonUtil.kt @@ -351,7 +351,7 @@ inline fun errorDialog(title: String, block: () -> Unit) { when (e) { is CancellationException, is EOFException, - -> return + -> return } appScope.launch(Dispatchers.Main) { showErrorDialog(e, title) diff --git a/app/src/main/java/com/donut/mixfile/util/file/FileCard.kt b/app/src/main/java/com/donut/mixfile/util/file/FileCard.kt index fdf3796..bca5982 100644 --- a/app/src/main/java/com/donut/mixfile/util/file/FileCard.kt +++ b/app/src/main/java/com/donut/mixfile/util/file/FileCard.kt @@ -1,11 +1,14 @@ package com.donut.mixfile.util.file import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -14,13 +17,16 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.material3.Card +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material3.CardDefaults import androidx.compose.material3.ElevatedCard import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale @@ -119,7 +125,14 @@ fun PreviewCard( } @Composable -fun FileCardList(cardList: List, longClick: (FileDataLog) -> Unit = {}) { +fun FileCardList( + cardList: List, + selected: Set = setOf(), + onClick: (FileDataLog) -> Unit = { + tryResolveFile(it.shareInfoData) + }, + longClick: (FileDataLog) -> Unit = {}, +) { if (filePreview.contentEquals("开启") || (filePreview.contentEquals("仅Wifi") && NetworkChangeReceiver.isWifi) @@ -149,7 +162,16 @@ fun FileCardList(cardList: List, longClick: (FileDataLog) -> Unit = verticalArrangement = Arrangement.spacedBy(0.dp) ) { items(cardList.size) { index -> - FileCard(cardList[index], longClick = longClick) + val log = cardList[index] + if (index > 0) { + HorizontalDivider() + } + FileCard( + log, + longClick = longClick, + selected = selected.contains(log), + onClick = onClick + ) } } } @@ -160,35 +182,52 @@ fun FileCardList(cardList: List, longClick: (FileDataLog) -> Unit = fun FileCard( fileDataLog: FileDataLog, showDate: Boolean = true, + onClick: (FileDataLog) -> Unit, + selected: Boolean = false, longClick: (FileDataLog) -> Unit = {}, ) { LaunchedEffect(favorites) { } - HorizontalDivider() - Card( - colors = CardDefaults.cardColors( - containerColor = Color(107, 218, 246, 0), - ), + val color = remember(selected) { + if (selected) + Color(107, 184, 242, 84) + else + Color(107, 218, 246, 0) + } + Box( modifier = Modifier .fillMaxWidth() + .background(color) .combinedClickable( onLongClick = { longClick(fileDataLog) } ) { - tryResolveFile(fileDataLog.shareInfoData) + onClick(fileDataLog) } ) { Column( modifier = Modifier.padding(10.dp), verticalArrangement = Arrangement.spacedBy(5.dp) ) { - Text( - text = fileDataLog.name.trim(), - color = colorScheme.primary, - fontSize = 16.sp, - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + if (selected) { + Icon( + Icons.Filled.CheckCircle, + contentDescription = "selected", + tint = colorScheme.primary + ) + } + Text( + text = fileDataLog.name.trim(), + color = colorScheme.primary, + fontSize = 16.sp, + ) + } FlowRow( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth() 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 d88547f..54c0984 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 @@ -16,7 +16,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.DialogProperties import com.donut.mixfile.activity.VideoActivity import com.donut.mixfile.app import com.donut.mixfile.currentActivity @@ -24,13 +23,11 @@ import com.donut.mixfile.server.utils.bean.MixShareInfo import com.donut.mixfile.ui.component.common.MixDialogBuilder import com.donut.mixfile.ui.routes.favorites.importFileList import com.donut.mixfile.ui.routes.favorites.openCategorySelect +import com.donut.mixfile.ui.routes.home.DownloadTask +import com.donut.mixfile.ui.routes.home.showDownloadTaskWindow import com.donut.mixfile.ui.theme.colorScheme -import com.donut.mixfile.util.UseEffect import com.donut.mixfile.util.copyToClipboard -import com.donut.mixfile.util.errorDialog import com.donut.mixfile.util.formatFileSize -import com.donut.mixfile.util.objects.ProgressContent -import com.donut.mixfile.util.showToast @OptIn(ExperimentalLayoutApi::class) fun showFileInfoDialog(shareInfo: MixShareInfo, onDismiss: () -> Unit = {}) { @@ -126,6 +123,8 @@ fun showFileInfoDialog(shareInfo: MixShareInfo, onDismiss: () -> Unit = {}) { } setPositiveButton("下载文件") { downloadFile(shareInfo) + closeDialog() + showDownloadTaskWindow() } show() } @@ -146,38 +145,6 @@ fun InfoText(key: String, value: String) { } fun downloadFile(shareInfo: MixShareInfo) { - MixDialogBuilder( - "文件下载中", properties = DialogProperties( - dismissOnClickOutside = false, - dismissOnBackPress = false - ) - ).apply { - setContent { - Column( - verticalArrangement = Arrangement.spacedBy(10.dp), - ) { - val progressContent = remember { - ProgressContent() - } - progressContent.LoadingContent() - - UseEffect { - errorDialog("下载失败") { - saveFileToStorage( - shareInfo.downloadUrl, - displayName = shareInfo.fileName, - progress = progressContent - ) - showToast("文件已保存到下载目录") - } - closeDialog() - } - } - } - setNegativeButton("取消") { - showToast("下载已取消") - closeDialog() - } - show() - } + val task = DownloadTask(shareInfo.fileName, shareInfo.fileSize, shareInfo.downloadUrl) + task.start() } diff --git a/app/src/main/java/com/donut/mixfile/util/file/FileImport.kt b/app/src/main/java/com/donut/mixfile/util/file/FileImport.kt index d88d871..6b6e11e 100644 --- a/app/src/main/java/com/donut/mixfile/util/file/FileImport.kt +++ b/app/src/main/java/com/donut/mixfile/util/file/FileImport.kt @@ -1,10 +1,21 @@ package com.donut.mixfile.util.file +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import com.donut.mixfile.server.localClient import com.donut.mixfile.ui.component.common.MixDialogBuilder import com.donut.mixfile.util.compressGzip import com.donut.mixfile.util.decompressGzip import com.donut.mixfile.util.formatFileSize +import com.donut.mixfile.util.getCurrentTime import com.donut.mixfile.util.objects.ProgressContent import com.donut.mixfile.util.showErrorDialog import com.donut.mixfile.util.showToast @@ -23,7 +34,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.io.readByteArray -fun exportFileList(fileList: List, name: String) { +fun exportFileList(fileList: Collection, name: String) { val strData = fileList.toJsonString() val compressedData = compressGzip(strData) doUploadFile( @@ -33,6 +44,31 @@ fun exportFileList(fileList: List, name: String) { ) } +fun showExportFileListDialog(fileList: Collection) { + MixDialogBuilder("确定导出?").apply { + var listName by mutableStateOf("文件列表-${getCurrentTime()}") + setContent { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(5.dp) + ) { + OutlinedTextField(value = listName, onValueChange = { + listName = it + }, modifier = Modifier.fillMaxWidth(), label = { + Text(text = "列表名称") + }) + Text(text = "将会导出当前筛选的文件列表上传为一键分享链接") + } + } + setDefaultNegative() + setPositiveButton("确定") { + exportFileList(fileList, listName) + closeDialog() + } + show() + } +} + fun showFileList(fileList: List) { val fileTotalSize = fileList.sumOf { it.size } MixDialogBuilder( 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 7863735..7dfd691 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 @@ -139,8 +139,8 @@ var multiUploadTaskCount by cachedMutableOf(5, "mix_file_multi_upload_task_count val uploadSemaphore = Semaphore(multiUploadTaskCount.toInt()) var uploadQueue by mutableIntStateOf(0) -var totalFileCount by mutableIntStateOf(0) -var successFileCount by mutableIntStateOf(0) +var totalUploadFileCount by mutableIntStateOf(0) +var uploadSuccessFileCount by mutableIntStateOf(0) private val multiUploadJobs = mutableListOf() fun cancelAllMultiUpload() { @@ -148,8 +148,8 @@ fun cancelAllMultiUpload() { multiUploadJobs.forEach { it.cancel() } multiUploadJobs.clear() uploadTasks.forEach { it.stop() } - totalFileCount = 0 - successFileCount = 0 + totalUploadFileCount = 0 + uploadSuccessFileCount = 0 } inline fun uploadUri(uri: Uri, uploader: (StreamContent, String) -> Unit) { @@ -170,7 +170,7 @@ fun selectAndUploadFile() { MainActivity.mixFileSelector.openSelect { uriList -> val taskList = mutableListOf Unit>() uploadQueue += uriList.size - totalFileCount += uriList.size + totalUploadFileCount += uriList.size uriList.forEach { uri -> taskList.add { uploadUri(uri) { stream, name -> @@ -191,7 +191,7 @@ fun selectAndUploadFile() { deferredList.add(async { catchError { task() - successFileCount++ + uploadSuccessFileCount++ } uploadSemaphore.release() }) @@ -199,8 +199,8 @@ fun selectAndUploadFile() { deferredList.awaitAll() withContext(Dispatchers.Main) { if (uploadQueue == 0) { - totalFileCount = 0 - successFileCount = 0 + totalUploadFileCount = 0 + uploadSuccessFileCount = 0 } } }