mirror of
https://github.com/InvertGeek/MixFile.git
synced 2026-06-01 09:01:05 +08:00
增加批量操作,下载功能
增加批量操作,下载功能
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -168,7 +168,7 @@ fun SingleSelectItemList(
|
||||
@Composable
|
||||
fun <T> SingleSelectItemList(
|
||||
items: List<T>,
|
||||
currentOption: T?,
|
||||
currentOption: T? = null,
|
||||
getLabel: (option: T) -> String,
|
||||
onSelect: (option: T) -> Unit,
|
||||
) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<FileDataLog>())
|
||||
}
|
||||
|
||||
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<Pair<String, () -> Unit>>) {
|
||||
MixDialogBuilder("编辑文件").apply {
|
||||
setContent {
|
||||
SingleSelectItemList(
|
||||
items = options,
|
||||
getLabel = {
|
||||
it.first
|
||||
},
|
||||
) { option ->
|
||||
option.second()
|
||||
closeDialog()
|
||||
}
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
@@ -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<DownloadTask>())
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<FileDataLog>, longClick: (FileDataLog) -> Unit = {}) {
|
||||
fun FileCardList(
|
||||
cardList: List<FileDataLog>,
|
||||
selected: Set<FileDataLog> = 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<FileDataLog>, 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<FileDataLog>, 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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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<FileDataLog>, name: String) {
|
||||
fun exportFileList(fileList: Collection<FileDataLog>, name: String) {
|
||||
val strData = fileList.toJsonString()
|
||||
val compressedData = compressGzip(strData)
|
||||
doUploadFile(
|
||||
@@ -33,6 +44,31 @@ fun exportFileList(fileList: List<FileDataLog>, name: String) {
|
||||
)
|
||||
}
|
||||
|
||||
fun showExportFileListDialog(fileList: Collection<FileDataLog>) {
|
||||
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<FileDataLog>) {
|
||||
val fileTotalSize = fileList.sumOf { it.size }
|
||||
MixDialogBuilder(
|
||||
|
||||
@@ -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<Job>()
|
||||
|
||||
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<suspend () -> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user