add edit download option

This commit is contained in:
AmirHossein Abdolmotallebi 2024-11-06 00:34:58 +03:30
parent ea8aa101f9
commit 5147ef9243
20 changed files with 1260 additions and 45 deletions

View File

@ -7,6 +7,7 @@ import com.abdownloadmanager.desktop.pages.addDownload.single.AddSingleDownloadC
import com.abdownloadmanager.desktop.pages.batchdownload.BatchDownloadComponent
import com.abdownloadmanager.desktop.pages.category.CategoryComponent
import com.abdownloadmanager.desktop.pages.category.CategoryDialogManager
import com.abdownloadmanager.desktop.pages.editdownload.EditDownloadComponent
import com.abdownloadmanager.desktop.pages.home.HomeComponent
import com.abdownloadmanager.desktop.pages.queue.QueuesComponent
import com.abdownloadmanager.desktop.pages.settings.SettingsComponent
@ -41,9 +42,11 @@ import com.abdownloadmanager.resources.*
import com.abdownloadmanager.utils.category.CategoryManager
import com.abdownloadmanager.utils.category.CategorySelectionMode
import ir.amirab.downloader.exception.TooManyErrorException
import ir.amirab.downloader.monitor.isDownloadActiveFlow
import ir.amirab.util.compose.StringSource
import ir.amirab.util.compose.asStringSource
import ir.amirab.util.compose.combineStringSources
import ir.amirab.util.flow.mapStateFlow
import ir.amirab.util.osfileutil.FileUtils
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -70,6 +73,7 @@ class AppComponent(
DownloadDialogManager,
AddDownloadDialogManager,
CategoryDialogManager,
EditDownloadDialogManager,
NotificationSender,
DownloadItemOpener,
ContainsEffects<AppEffects> by supportEffects(),
@ -112,6 +116,7 @@ class AppComponent(
addDownloadDialogManager = this,
categoryDialogManager = this,
notificationSender = this,
editDownloadDialogManager = this,
)
}
).subscribeAsStateFlow()
@ -150,6 +155,43 @@ class AppComponent(
}
).subscribeAsStateFlow()
private val editDownload = SlotNavigation<Long>()
val editDownloadSlot = childSlot(
editDownload,
serializer = null,
key = "editDownload",
childFactory = { editDownloadConfig: Long, componentContext: ComponentContext ->
EditDownloadComponent(
ctx = componentContext,
onRequestClose = {
closeEditDownloadDialog()
},
onEdited = {
scope.launch {
downloadSystem.editDownload(it)
closeEditDownloadDialog()
}
},
downloadId = editDownloadConfig,
acceptEdit = downloadSystem.downloadMonitor
.isDownloadActiveFlow(editDownloadConfig)
.mapStateFlow { !it },
)
}
).subscribeAsStateFlow()
override fun openEditDownloadDialog(id: Long) {
val currentComponent = editDownloadSlot.value.child?.instance
if (currentComponent != null && currentComponent.downloadId == id) {
currentComponent.bringToFront()
} else {
editDownload.activate(id)
}
}
override fun closeEditDownloadDialog() {
editDownload.dismiss()
}
fun openSettings() {
scope.launch {
@ -546,6 +588,18 @@ class AppComponent(
}
}
fun externalCredentialComingIntoApp(list: List<DownloadCredentials>) {
val editDownloadComponent = editDownloadSlot.value.child?.instance
if (editDownloadComponent != null) {
list.firstOrNull()?.let {
editDownloadComponent.importCredential(it)
editDownloadComponent.bringToFront()
}
} else {
openAddDownloadDialog(list)
}
}
override fun openAddDownloadDialog(
links: List<DownloadCredentials>,
) {
@ -776,6 +830,11 @@ interface DownloadDialogManager {
fun closeDownloadDialog(id: Long)
}
interface EditDownloadDialogManager {
fun openEditDownloadDialog(id: Long)
fun closeEditDownloadDialog()
}
interface AddDownloadDialogManager {
val openedAddDownloadDialogs: StateFlow<List<AddDownloadComponent>>
fun openAddDownloadDialog(

View File

@ -10,7 +10,7 @@ import org.koin.core.component.inject
class IntegrationHandlerImp: IntegrationHandler,KoinComponent{
val appComponent by inject<AppComponent>()
override suspend fun addDownload(list: List<NewDownloadInfoFromIntegration>) {
appComponent.openAddDownloadDialog(list.map {
appComponent.externalCredentialComingIntoApp(list.map {
DownloadCredentials(
link = it.link,
headers = it.headers,

View File

@ -1,6 +1,5 @@
package com.abdownloadmanager.desktop.pages.addDownload.shared
import com.abdownloadmanager.desktop.pages.addDownload.single.AddSingleDownloadComponent
import com.abdownloadmanager.desktop.pages.settings.configurable.widgets.RenderConfigurable
import com.abdownloadmanager.desktop.ui.customwindow.BaseOptionDialog
import com.abdownloadmanager.desktop.ui.theme.myColors
@ -22,11 +21,15 @@ import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.rememberDialogState
import com.abdownloadmanager.desktop.pages.settings.configurable.Configurable
import java.awt.Dimension
import java.awt.MouseInfo
@Composable
fun ExtraConfig(component: AddSingleDownloadComponent) {
fun ExtraConfig(
onDismiss: () -> Unit,
configurables: List<Configurable<*>>,
) {
val h = 250
val w = 300
val state = rememberDialogState(
@ -35,9 +38,7 @@ fun ExtraConfig(component: AddSingleDownloadComponent) {
width = w.dp,
),
)
BaseOptionDialog({
component.showMoreSettings = false
}, state) {
BaseOptionDialog(onDismiss, state) {
LaunchedEffect(window){
window.moveSafe(
MouseInfo.getPointerInfo().location.run {
@ -84,7 +85,6 @@ fun ExtraConfig(component: AddSingleDownloadComponent) {
Column(
Modifier.verticalScroll(scrollState)
) {
val configurables = component.configurables
for ((index, cfg) in configurables.withIndex()) {
RenderConfigurable(
cfg,

View File

@ -33,8 +33,6 @@ import androidx.compose.ui.window.*
import com.abdownloadmanager.desktop.pages.addDownload.shared.*
import com.abdownloadmanager.desktop.utils.mvi.HandleEffects
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import com.abdownloadmanager.utils.category.rememberIconPainter
import ir.amirab.util.compose.resources.myStringResource
import ir.amirab.downloader.utils.OnDuplicateStrategy
import ir.amirab.util.compose.asStringSource
@ -182,7 +180,10 @@ fun AddDownloadPage(
)
}
if (component.showMoreSettings) {
ExtraConfig(component)
ExtraConfig(
onDismiss = { component.showMoreSettings = false },
configurables = component.configurables,
)
}
}
}

View File

@ -0,0 +1,600 @@
package com.abdownloadmanager.desktop.pages.editdownload
import androidx.compose.runtime.Composable
import com.abdownloadmanager.utils.compose.WithContentAlpha
import ir.amirab.util.compose.IconSource
import com.abdownloadmanager.utils.compose.widget.MyIcon
import com.abdownloadmanager.desktop.ui.icon.MyIcons
import com.abdownloadmanager.desktop.ui.theme.myColors
import com.abdownloadmanager.desktop.ui.theme.myTextSizes
import com.abdownloadmanager.desktop.ui.widget.*
import com.abdownloadmanager.desktop.utils.*
import androidx.compose.animation.*
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.*
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.*
import androidx.compose.ui.window.*
import com.abdownloadmanager.desktop.pages.addDownload.shared.ExtraConfig
import com.abdownloadmanager.desktop.ui.customwindow.CustomWindow
import com.abdownloadmanager.desktop.ui.customwindow.WindowTitle
import com.abdownloadmanager.desktop.ui.util.ifThen
import com.abdownloadmanager.desktop.utils.mvi.HandleEffects
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.utils.FileIconProvider
import com.abdownloadmanager.utils.compose.WithContentColor
import ir.amirab.util.UrlUtils
import ir.amirab.util.compose.resources.myStringResource
import ir.amirab.util.compose.asStringSource
@Composable
fun EditDownloadWindow(
component: EditDownloadComponent,
) {
CustomWindow(
state = rememberWindowState(
size = DpSize(450.dp, 230.dp),
position = WindowPosition.Aligned(Alignment.Center)
),
alwaysOnTop = true,
onCloseRequest = {
component.onRequestClose()
},
) {
HandleEffects(component) {
when (it) {
EditDownloadPageEffects.BringToFront -> {
window.toFront()
}
}
}
EditDownloadPage(component)
}
}
@Composable
fun EditDownloadPage(
component: EditDownloadComponent,
) {
WindowTitle(myStringResource(Res.string.edit_download_title))
component.editDownloadUiChecker.collectAsState().value?.let { editDownloadUiChecker ->
Column(
Modifier
.padding(horizontal = 32.dp)
.padding(top = 8.dp, bottom = 16.dp)
) {
val canAddResult by editDownloadUiChecker.canEditDownloadResult.collectAsState()
val link by editDownloadUiChecker.link.collectAsState()
fun setLink(link: String) {
editDownloadUiChecker.setLink(link)
}
val linkFocus = remember { FocusRequester() }
LaunchedEffect(Unit) {
linkFocus.requestFocus()
}
UrlTextField(
text = link,
setText = {
setLink(it)
},
modifier = Modifier.focusRequester(linkFocus),
errorText = when (canAddResult) {
CanEditDownloadResult.InvalidURL -> Res.string.invalid_url
else -> null
}?.takeIf { link.isNotEmpty() }?.asStringSource()?.rememberString()
// ATTENTION DO NOT use composable functions in when branches
// it seems buggy (compose won't render ui properly)
// stranger part is that in this case if we use ? before takeIf then it will work! (`}.takeIf {` is buggy but `}?.takeIf {` works!)
// maybe there is a bug in compose compiler, or maybe I'm missed something. if you read this ,and you know why! please let me know!
)
Row {
Column(Modifier.weight(1f)) {
val name by editDownloadUiChecker.name.collectAsState()
Spacer(Modifier.size(8.dp))
NameTextField(
text = name,
setText = {
editDownloadUiChecker.setName(it)
},
errorText = when (canAddResult) {
CanEditDownloadResult.FileNameAlreadyExists -> Res.string.file_name_already_exists
CanEditDownloadResult.InvalidFileName -> Res.string.invalid_file_name
else -> null
}?.takeIf { name.isNotEmpty() }?.asStringSource()?.rememberString()
)
Spacer(Modifier.size(8.dp))
BrowserImportButton(component, editDownloadUiChecker)
}
Spacer(Modifier.size(24.dp))
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.align(Alignment.Top)
.width(IntrinsicSize.Max)
) {
RenderFileTypeAndSize(component.iconProvider, editDownloadUiChecker)
RenderResumeSupport(editDownloadUiChecker)
ConfigActionsButtons(editDownloadUiChecker)
}
}
Spacer(Modifier.weight(1f))
MainActionButtons(component, editDownloadUiChecker)
if (editDownloadUiChecker.showMoreSettings.collectAsState().value) {
ExtraConfig(
onDismiss = {
editDownloadUiChecker.setShowMoreSettings(false)
},
configurables = editDownloadUiChecker.configurables,
)
}
}
}
}
@Composable
fun BrowserImportButton(
component: EditDownloadComponent,
downloadUiState: EditDownloadState,
) {
val credentialsImportedFromExternal by component.credentialsImportedFromExternal.collectAsState()
val downloadPage = downloadUiState.currentDownloadItem.collectAsState().value.downloadPage
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
ActionButton(
myStringResource(Res.string.edit_download_update_from_download_page),
enabled = downloadPage != null,
onClick = {
downloadPage?.let {
UrlUtils.openUrl(it)
}
},
// borderColor = when (credentialsImportedFromExternal) {
// true -> SolidColor(myColors.success)
// false -> SolidColor(myColors.onBackground / 10)
// },
contentPadding = PaddingValues(
vertical = 6.dp,
horizontal = animateDpAsState(
if (credentialsImportedFromExternal) 8.dp
else 16.dp
).value,
),
end = {
AnimatedVisibility(credentialsImportedFromExternal) {
Row {
Spacer(Modifier.width(8.dp))
MyIcon(
MyIcons.check,
null,
Modifier.size(16.dp),
tint = myColors.success,
)
}
}
}
)
Spacer(Modifier.width(8.dp))
Help(myStringResource(Res.string.edit_download_update_from_download_page_description))
}
}
}
@Composable
private fun RenderResumeSupport(
editDownloadUiChecker: EditDownloadState,
) {
val fileInfo by editDownloadUiChecker.responseInfo.collectAsState()
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.height(16.dp)
) {
val lineModifier = Modifier.weight(1f)
.height(1.dp)
.background(myColors.onBackground / 10)
Box(lineModifier)
val canEditDownload by editDownloadUiChecker.canEdit.collectAsState()
AnimatedVisibility(
visible = canEditDownload && fileInfo != null,
) {
fileInfo?.let { fileInfo ->
if (fileInfo.resumeSupport) {
val iconModifier = Modifier
.padding(horizontal = 2.dp)
.size(10.dp)
if (fileInfo.resumeSupport) {
MyIcon(
icon = MyIcons.check,
contentDescription = null,
modifier = iconModifier,
tint = myColors.success
)
} else {
MyIcon(
icon = MyIcons.clear,
contentDescription = null,
modifier = iconModifier,
tint = myColors.error,
)
}
}
}
}
Box(lineModifier)
}
}
@Composable
private fun MainConfigActionButton(
text: String,
modifier: Modifier,
enabled: Boolean = true,
onClick: () -> Unit,
) {
ActionButton(text, modifier, enabled, onClick)
}
@Composable
private fun PrimaryMainConfigActionButton(
text: String,
modifier: Modifier,
enabled: Boolean,
onClick: () -> Unit,
) {
val backgroundColor = Brush.horizontalGradient(
myColors.primaryGradientColors.map {
it / 30
}
)
val borderColor = Brush.horizontalGradient(
myColors.primaryGradientColors
)
val disabledBorderColor = Brush.horizontalGradient(
myColors.primaryGradientColors.map {
it / 50
}
)
ActionButton(
text = text,
modifier = modifier,
enabled = enabled,
onClick = onClick,
backgroundColor = backgroundColor,
disabledBackgroundColor = backgroundColor,
borderColor = borderColor,
disabledBorderColor = disabledBorderColor,
)
}
@Composable
fun ConfigActionsButtons(
editDownloadUiChecker: EditDownloadState,
) {
val showMoreSettings by editDownloadUiChecker.showMoreSettings.collectAsState()
val requiresAuth = editDownloadUiChecker.responseInfo.collectAsState().value?.requireBasicAuth ?: false
Row {
IconActionButton(MyIcons.refresh, myStringResource(Res.string.refresh)) {
editDownloadUiChecker.refresh()
}
Spacer(Modifier.width(6.dp))
IconActionButton(
MyIcons.settings,
myStringResource(Res.string.settings),
indicateActive = showMoreSettings,
requiresAttention = requiresAuth
) {
editDownloadUiChecker.setShowMoreSettings(true)
}
}
}
@Composable
private fun MainActionButtons(
component: EditDownloadComponent,
editDownloadUiChecker: EditDownloadState,
) {
Row {
val canEditResult by editDownloadUiChecker.canEditDownloadResult.collectAsState()
val canEdit = run {
val canBeEdited = editDownloadUiChecker.canEdit.collectAsState().value
val componentAllowsEdit = component.acceptEdit.collectAsState().value
canBeEdited && componentAllowsEdit
}
val warnings = (canEditResult as? CanEditDownloadResult.CanEdit)?.warnings.orEmpty()
Spacer(Modifier.width(8.dp))
var showWarningPrompt by remember {
mutableStateOf(false)
}
Box {
if (showWarningPrompt) {
WarningPrompt(
warnings = warnings,
onClose = {
showWarningPrompt = false
},
onConfirm = {
if (canEdit) {
component.onRequestEdit()
}
}
)
}
PrimaryMainConfigActionButton(
text = myStringResource(Res.string.change),
modifier = Modifier,
enabled = canEdit,
onClick = {
if (warnings.isNotEmpty()) {
showWarningPrompt = true
} else {
component.onRequestEdit()
}
},
)
}
// Spacer(Modifier.weight(1f))
Spacer(Modifier.weight(1f))
MainConfigActionButton(
text = myStringResource(Res.string.cancel),
modifier = Modifier,
onClick = {
component.onRequestClose()
},
)
}
}
@Composable
fun WarningPrompt(
warnings: List<CanEditWarnings>,
onClose: () -> Unit,
onConfirm: () -> Unit,
) {
Popup(
popupPositionProvider = rememberComponentRectPositionProvider(
anchor = Alignment.TopStart,
alignment = Alignment.TopEnd,
),
onDismissRequest = onClose
) {
val shape = RoundedCornerShape(6.dp)
Box(
Modifier
.padding(vertical = 4.dp)
.widthIn(max = 240.dp)
.shadow(24.dp)
.clip(shape)
.border(1.dp, myColors.surface, shape)
.background(myColors.menuGradientBackground)
.padding(8.dp)
) {
WithContentColor(myColors.onSurface) {
Column {
Text(
myStringResource(Res.string.warning),
fontWeight = FontWeight.Bold,
color = myColors.warning
)
Spacer(Modifier.height(4.dp))
warnings.forEach {
Text(
it.asStringSource().rememberString(),
fontSize = myTextSizes.base,
)
}
Text(myStringResource(Res.string.warning_you_may_have_to_restart_the_download_later))
Spacer(Modifier.height(8.dp))
ActionButton(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = myStringResource(Res.string.change_anyway),
onClick = onConfirm,
borderColor = SolidColor(myColors.error),
contentColor = myColors.error,
)
}
}
}
}
}
@Composable
private fun RenderFileTypeAndSize(
iconProvider: FileIconProvider,
editDownloadUiChecker: EditDownloadState,
) {
val isLinkLoading by editDownloadUiChecker.isLinkLoading.collectAsState()
val fileInfo by editDownloadUiChecker.responseInfo.collectAsState()
val iconModifier = Modifier.size(16.dp)
Box(Modifier.padding(top = 16.dp)) {
AnimatedContent(
targetState = isLinkLoading,
transitionSpec = {
fadeIn() togetherWith fadeOut()
}
) { loading ->
if (loading) {
LoadingIndicator(iconModifier)
} else {
val icon = iconProvider.rememberIcon(editDownloadUiChecker.name.collectAsState().value)
AnimatedContent(
fileInfo,
) { fileInfo ->
Row(
verticalAlignment = Alignment.CenterVertically,
) {
WithContentAlpha(1f) {
if (fileInfo != null) {
if (fileInfo.requiresAuth) {
MyIcon(
MyIcons.lock,
null,
iconModifier,
tint = myColors.error
)
}
MyIcon(
icon,
null,
iconModifier
)
val size = fileInfo.totalLength?.let {
convertSizeToHumanReadable(it)
}.takeIf {
// this is a length of a html page (error)
fileInfo.isSuccessFul
} ?: Res.string.unknown.asStringSource()
Spacer(Modifier.width(8.dp))
Text(
size.rememberString(),
fontSize = myTextSizes.sm,
)
} else {
MyIcon(
icon = MyIcons.question,
contentDescription = null,
modifier = iconModifier,
)
}
}
}
}
}
}
}
}
@Composable
private fun MyTextFieldIcon(
icon: IconSource,
onClick: (() -> Unit)? = null,
) {
MyIcon(icon, null, Modifier
.fillMaxHeight()
.ifThen(onClick != null) {
pointerHoverIcon(PointerIcon.Default)
.clickable { onClick?.invoke() }
}
.wrapContentHeight()
.padding(horizontal = 8.dp)
.size(16.dp))
}
@Composable
private fun UrlTextField(
text: String,
setText: (String) -> Unit,
errorText: String? = null,
modifier: Modifier = Modifier,
) {
AddDownloadPageTextField(
text,
setText,
myStringResource(Res.string.download_link),
modifier = modifier.fillMaxWidth(),
start = {
MyTextFieldIcon(MyIcons.link)
},
end = {
MyTextFieldIcon(MyIcons.paste) {
setText(
ClipboardUtil.read()
.orEmpty()
)
}
},
errorText = errorText
)
}
@Composable
private fun NameTextField(
text: String,
setText: (String) -> Unit,
errorText: String? = null,
) {
AddDownloadPageTextField(
text,
setText,
myStringResource(Res.string.name),
modifier = Modifier.fillMaxWidth(),
errorText = errorText,
)
}
@Composable
private fun AddDownloadPageTextField(
text: String,
setText: (String) -> Unit,
placeHolder: String,
modifier: Modifier,
errorText: String? = null,
start: @Composable() (() -> Unit)? = null,
end: @Composable() (() -> Unit)? = null,
) {
val interactionSource = remember { MutableInteractionSource() }
val isFocused by interactionSource.collectIsFocusedAsState()
val dividerModifier = Modifier
.fillMaxHeight()
.padding(vertical = 1.dp)
//to not conflict with text-field border
.width(1.dp)
.background(if (isFocused) myColors.onBackground / 10 else Color.Transparent)
Column(modifier) {
MyTextField(
text,
setText,
placeHolder,
modifier = Modifier.fillMaxWidth(),
background = myColors.surface / 50,
interactionSource = interactionSource,
shape = RoundedCornerShape(6.dp),
start = start?.let {
{
WithContentAlpha(0.5f) {
it()
}
Spacer(dividerModifier)
}
},
end = end?.let {
{
Spacer(dividerModifier)
it()
}
}
)
AnimatedVisibility(errorText != null) {
if (errorText != null) {
Text(
errorText,
Modifier.padding(bottom = 4.dp, start = 4.dp),
fontSize = myTextSizes.sm,
color = myColors.error,
)
}
}
}
}

View File

@ -0,0 +1,100 @@
package com.abdownloadmanager.desktop.pages.editdownload
import androidx.compose.runtime.Immutable
import com.abdownloadmanager.desktop.utils.*
import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects
import com.abdownloadmanager.desktop.utils.mvi.ContainsScreenState
import com.abdownloadmanager.desktop.utils.mvi.SupportsScreenState
import com.abdownloadmanager.desktop.utils.mvi.supportEffects
import com.abdownloadmanager.utils.FileIconProvider
import com.arkivanov.decompose.ComponentContext
import ir.amirab.downloader.connection.DownloaderClient
import ir.amirab.downloader.downloaditem.DownloadCredentials
import ir.amirab.downloader.downloaditem.DownloadItem
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
sealed interface EditDownloadPageEffects {
data object BringToFront : EditDownloadPageEffects
}
class EditDownloadComponent(
ctx: ComponentContext,
val onRequestClose: () -> Unit,
val downloadId: Long,
val acceptEdit: StateFlow<Boolean>,
private val onEdited: (DownloadItem) -> Unit,
) : BaseComponent(ctx),
ContainsEffects<EditDownloadPageEffects> by supportEffects(),
KoinComponent {
private val downloaderClient: DownloaderClient by inject()
val iconProvider: FileIconProvider by inject()
val downloadSystem: DownloadSystem by inject()
val editDownloadUiChecker = MutableStateFlow(null as EditDownloadState?)
init {
scope.launch {
load(downloadId)
}
}
private var pendingCredential: DownloadCredentials? = null
private val _credentialsImportedFromExternal = MutableStateFlow(false)
val credentialsImportedFromExternal = _credentialsImportedFromExternal.asStateFlow()
fun importCredential(credentials: DownloadCredentials) {
editDownloadUiChecker.value?.let {
it.importCredentials(credentials)
} ?: run {
pendingCredential = credentials
}
_credentialsImportedFromExternal.value = true
}
private suspend fun load(id: Long) {
val downloadItem = downloadSystem.getDownloadItemById(id = id)
if (downloadItem == null) {
onRequestClose()
println("item with id $id not found")
return
}
val editDownloadState = EditDownloadState(
currentDownloadItem = MutableStateFlow(downloadItem),
editedDownloadItem = MutableStateFlow(downloadItem),
downloaderClient = downloaderClient,
conflictDetector = object : DownloadConflictDetector {
override fun checkAlreadyExists(current: DownloadItem, edited: DownloadItem): Boolean {
val editedDownloadFile = downloadSystem.getDownloadFile(edited)
val alreadyExists = editedDownloadFile.exists()
if (alreadyExists) {
return true
}
return downloadSystem
.getAllRegisteredDownloadFiles()
.contains(editedDownloadFile)
}
},
scope,
)
editDownloadUiChecker.value = editDownloadState
pendingCredential?.let { credentials ->
editDownloadState.importCredentials(credentials)
pendingCredential = null
}
}
fun onRequestEdit() {
if (!acceptEdit.value) {
return
}
editDownloadUiChecker.value?.let { editDownloadUiChecker ->
onEdited(editDownloadUiChecker.editedDownloadItem.value)
}
}
fun bringToFront() {
sendEffect(EditDownloadPageEffects.BringToFront)
}
}

View File

@ -0,0 +1,331 @@
package com.abdownloadmanager.desktop.pages.editdownload
import com.abdownloadmanager.desktop.pages.settings.configurable.IntConfigurable
import com.abdownloadmanager.desktop.pages.settings.configurable.SpeedLimitConfigurable
import com.abdownloadmanager.desktop.pages.settings.configurable.StringConfigurable
import com.abdownloadmanager.desktop.utils.FileNameValidator
import com.abdownloadmanager.desktop.utils.LinkChecker
import com.abdownloadmanager.desktop.utils.convertSpeedToHumanReadable
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.utils.isValidUrl
import ir.amirab.downloader.connection.DownloaderClient
import ir.amirab.downloader.downloaditem.DownloadCredentials
import ir.amirab.downloader.downloaditem.DownloadItem
import ir.amirab.downloader.downloaditem.DownloadItem.Companion.LENGTH_UNKNOWN
import ir.amirab.downloader.downloaditem.withCredentials
import ir.amirab.util.compose.StringSource
import ir.amirab.util.compose.asStringSource
import ir.amirab.util.compose.asStringSourceWithARgs
import ir.amirab.util.flow.createMutableStateFlowFromStateFlow
import ir.amirab.util.flow.mapStateFlow
import ir.amirab.util.flow.mapTwoWayStateFlow
import ir.amirab.util.flow.onEachLatest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.*
sealed interface CanEditWarnings {
fun asStringSource(): StringSource
data class FileSizeNotMatch(
val currentSize: Long,
val newSize: Long,
) : CanEditWarnings {
override fun asStringSource(): StringSource {
return "The saved item have size of $currentSize and now will change to $newSize".asStringSource()
}
}
}
sealed interface CanEditDownloadResult {
data object FileNameAlreadyExists : CanEditDownloadResult
data object InvalidURL : CanEditDownloadResult
data object InvalidFileName : CanEditDownloadResult
data object NothingChanged : CanEditDownloadResult
data object Waiting : CanEditDownloadResult
data class CanEdit(
val warnings: List<CanEditWarnings>,
) : CanEditDownloadResult
}
class EditDownloadChecker(
val currentDownloadItem: MutableStateFlow<DownloadItem>,
val editedDownloadItem: MutableStateFlow<DownloadItem>,
val newLengthFlow: StateFlow<Long?>,
val conflictDetector: DownloadConflictDetector,
scope: CoroutineScope,
) {
init {
editedDownloadItem
.onEach {
_canEditResult.value = CanEditDownloadResult.Waiting
}.launchIn(scope)
}
fun check() {
_canEditResult.value = CanEditDownloadResult.Waiting
_canEditResult.value = check(
current = currentDownloadItem.value,
edited = editedDownloadItem.value,
newLength = newLengthFlow.value,
)
}
private fun check(
current: DownloadItem,
edited: DownloadItem,
newLength: Long?,
): CanEditDownloadResult {
if (current == edited) {
return CanEditDownloadResult.NothingChanged
}
if (!isValidUrl(edited.link)) {
return CanEditDownloadResult.InvalidURL
}
if (edited.name != current.name) {
if (!FileNameValidator.isValidFileName(edited.name)) {
return CanEditDownloadResult.InvalidFileName
}
if (conflictDetector.checkAlreadyExists(current, edited)) {
return CanEditDownloadResult.FileNameAlreadyExists
}
}
val warnings = mutableListOf<CanEditWarnings>()
if (current.contentLength != newLength) {
warnings.add(
CanEditWarnings.FileSizeNotMatch(
currentSize = current.contentLength,
newSize = newLength ?: LENGTH_UNKNOWN,
)
)
}
return CanEditDownloadResult.CanEdit(warnings)
}
private val _canEditResult = MutableStateFlow<CanEditDownloadResult>(CanEditDownloadResult.NothingChanged)
val canEditResult = _canEditResult.asStateFlow()
val canEdit = canEditResult.mapStateFlow {
it is CanEditDownloadResult.CanEdit
}
}
interface DownloadConflictDetector {
fun checkAlreadyExists(
current: DownloadItem,
edited: DownloadItem,
): Boolean
}
class EditDownloadState(
val currentDownloadItem: MutableStateFlow<DownloadItem>,
val editedDownloadItem: MutableStateFlow<DownloadItem>,
val downloaderClient: DownloaderClient,
conflictDetector: DownloadConflictDetector,
scope: CoroutineScope,
) {
private val _showMoreSettings = MutableStateFlow(false)
val showMoreSettings = _showMoreSettings.asStateFlow()
fun setShowMoreSettings(showMoreSettings: Boolean) {
_showMoreSettings.value = showMoreSettings
}
val credentials = editedDownloadItem.mapTwoWayStateFlow(
map = {
DownloadCredentials.from(it)
},
unMap = {
copy().withCredentials(it)
}
)
val name = editedDownloadItem.mapTwoWayStateFlow(
map = {
it.name
},
unMap = {
copy(name = it)
}
)
val configurables = listOf(
SpeedLimitConfigurable(
Res.string.download_item_settings_speed_limit.asStringSource(),
Res.string.download_item_settings_speed_limit_description.asStringSource(),
backedBy = editedDownloadItem.mapTwoWayStateFlow(
map = {
it.speedLimit
},
unMap = {
copy(speedLimit = it)
}
),
describe = {
if (it == 0L) Res.string.unlimited.asStringSource()
else convertSpeedToHumanReadable(it).asStringSource()
}
),
IntConfigurable(
Res.string.settings_download_thread_count.asStringSource(),
Res.string.settings_download_thread_count_description.asStringSource(),
backedBy = editedDownloadItem.mapTwoWayStateFlow(
map = {
it.preferredConnectionCount ?: 0
},
unMap = {
copy(
preferredConnectionCount = it.takeIf { it > 1 }
)
}
),
range = 0..32,
describe = {
if (it == 0) Res.string.use_global_settings.asStringSource()
else Res.string.download_item_settings_thread_count_describe
.asStringSourceWithARgs(
Res.string.download_item_settings_thread_count_describe_createArgs(
count = it.toString()
)
)
}
),
StringConfigurable(
Res.string.username.asStringSource(),
Res.string.download_item_settings_username_description.asStringSource(),
backedBy = credentials.mapTwoWayStateFlow(
map = {
it.username.orEmpty()
},
unMap = {
copy(username = it.takeIf { it.isNotEmpty() })
}
),
describe = {
"".asStringSource()
}
),
StringConfigurable(
Res.string.password.asStringSource(),
Res.string.download_item_settings_password_description.asStringSource(),
backedBy = credentials.mapTwoWayStateFlow(
map = {
it.password.orEmpty()
},
unMap = {
copy(password = it.takeIf { it.isNotEmpty() })
}
),
describe = {
"".asStringSource()
}
),
StringConfigurable(
Res.string.download_item_settings_download_page.asStringSource(),
Res.string.download_item_settings_download_page_description.asStringSource(),
backedBy = credentials.mapTwoWayStateFlow(
map = {
it.downloadPage.orEmpty()
},
unMap = {
copy(downloadPage = it.takeIf { it.isNotEmpty() })
}
),
describe = {
"".asStringSource()
}
),
)
fun setName(name: String) {
this.name.value = name
}
val link = credentials.mapTwoWayStateFlow(
map = { it.link },
unMap = {
copy(link = it)
}
)
fun setLink(link: String) {
credentials.update {
it.copy(link = link)
}
}
fun importCredentials(importedCredentials: DownloadCredentials) {
this.credentials.update {
importedCredentials
}
}
private val linkChecker = LinkChecker(
initialCredentials = credentials.value,
client = downloaderClient,
)
val isLinkLoading = linkChecker.isLoading
val gettingResponseInfo = linkChecker.isLoading
val responseInfo = linkChecker.responseInfo
val length = linkChecker.length
private val editDownloadChecker = EditDownloadChecker(
currentDownloadItem = currentDownloadItem,
editedDownloadItem = editedDownloadItem,
newLengthFlow = length,
conflictDetector = conflictDetector,
scope = scope,
)
val canEditDownloadResult = editDownloadChecker.canEditResult
val canEdit = editDownloadChecker.canEdit
private val refreshResponseInfoImmediately = MutableSharedFlow<Unit>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_LATEST
)
private val scheduleRefreshResponseInfo = MutableSharedFlow<Unit>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_LATEST
)
private val scheduleRecheckEditDownloadIsPossible = MutableSharedFlow<Unit>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_LATEST
)
fun refresh() {
refreshResponseInfoImmediately.tryEmit(Unit)
}
private fun scheduleRefresh(
alsoRecheckLink: Boolean,
) {
if (alsoRecheckLink) {
scheduleRefreshResponseInfo.tryEmit(Unit)
}
scheduleRecheckEditDownloadIsPossible.tryEmit(Unit)
}
init {
merge(
scheduleRefreshResponseInfo.debounce(500),
refreshResponseInfoImmediately
).onEachLatest {
linkChecker.check()
}.launchIn(scope)
merge(
scheduleRecheckEditDownloadIsPossible.debounce(500),
// ...
).onEachLatest {
editDownloadChecker.check()
}.launchIn(scope)
credentials.onEach { credentials ->
linkChecker.credentials.update { credentials }
scheduleRefresh(alsoRecheckLink = true)
}.launchIn(scope)
editedDownloadItem.onEach {
scheduleRefresh(alsoRecheckLink = false)
}.launchIn(scope)
length.onEach {
scheduleRefresh(alsoRecheckLink = false)
}.launchIn(scope)
}
}

View File

@ -76,6 +76,7 @@ class DownloadActions(
private val scope: CoroutineScope,
downloadSystem: DownloadSystem,
downloadDialogManager: DownloadDialogManager,
editDownloadDialogManager: EditDownloadDialogManager,
val selections: StateFlow<List<IDownloadItemState>>,
private val mainItem: StateFlow<Long?>,
private val queueManager: QueueManager,
@ -192,6 +193,20 @@ class DownloadActions(
}
}
)
val editDownloadAction = simpleAction(
title = Res.string.edit.asStringSource(),
icon = MyIcons.edit,
checkEnable = defaultItem.mapStateFlow {
it ?: return@mapStateFlow false
it.statusOrFinished() !is DownloadJobStatus.IsActive
},
onActionPerformed = {
scope.launch {
val item = defaultItem.value ?: return@launch
editDownloadDialogManager.openEditDownloadDialog(item.id)
}
}
)
val copyDownloadLinkAction = simpleAction(
title = Res.string.copy_link.asStringSource(),
@ -268,6 +283,7 @@ class DownloadActions(
+moveToCategoryAction
separator()
+(copyDownloadLinkAction)
+editDownloadAction
+(openDownloadDialogAction)
}
}
@ -381,6 +397,7 @@ class HomeComponent(
ctx: ComponentContext,
private val downloadItemOpener: DownloadItemOpener,
private val downloadDialogManager: DownloadDialogManager,
private val editDownloadDialogManager: EditDownloadDialogManager,
private val addDownloadDialogManager: AddDownloadDialogManager,
private val categoryDialogManager: CategoryDialogManager,
private val notificationSender: NotificationSender,
@ -826,6 +843,7 @@ class HomeComponent(
scope = scope,
downloadSystem = downloadSystem,
downloadDialogManager = downloadDialogManager,
editDownloadDialogManager = editDownloadDialogManager,
selections = selectionListItems,
mainItem = mainItem,
queueManager = queueManager,
@ -882,6 +900,7 @@ class HomeComponent(
"DELETE" to downloadActions.deleteAction
"ctrl O" to downloadActions.openFileAction
"ctrl F" to downloadActions.openFolderAction
"ctrl E" to downloadActions.editDownloadAction
"ctrl P" to downloadActions.pauseAction
"ctrl R" to downloadActions.resumeAction
"DELETE" to downloadActions.deleteAction

View File

@ -29,6 +29,7 @@ import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.window.*
import com.abdownloadmanager.desktop.pages.batchdownload.BatchDownloadWindow
import com.abdownloadmanager.desktop.pages.category.ShowCategoryDialogs
import com.abdownloadmanager.desktop.pages.editdownload.EditDownloadWindow
import com.abdownloadmanager.desktop.pages.home.HomeWindow
import com.abdownloadmanager.desktop.pages.settings.ThemeManager
import com.abdownloadmanager.desktop.ui.widget.ProvideLanguageManager
@ -84,6 +85,10 @@ object Ui : KoinComponent {
batchDownloadSlot.child?.instance?.let {
BatchDownloadWindow(it)
}
val editDownloadSlot = appComponent.editDownloadSlot.collectAsState().value
editDownloadSlot.child?.instance?.let {
EditDownloadWindow(it)
}
ShowAddDownloadDialogs(appComponent)
ShowDownloadDialogs(appComponent)
ShowCategoryDialogs(appComponent)

View File

@ -20,6 +20,7 @@ object MyIcons : IMyIcons {
override val windowClose get() = "/icons/window_close.svg".asIconSource()
override val exit get() = "/icons/exit.svg".asIconSource()
override val edit get() = "/icons/edit.svg".asIconSource()
override val undo get() = "/icons/undo.svg".asIconSource()
override val openSource: IconSource get() = "/icons/open_source.svg".asIconSource()

View File

@ -9,8 +9,7 @@ import com.abdownloadmanager.desktop.utils.div
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -24,35 +23,44 @@ import androidx.compose.ui.unit.dp
@Composable
fun ActionButton(
text: String,
modifier: Modifier=Modifier,
enabled: Boolean=true,
modifier: Modifier = Modifier,
enabled: Boolean = true,
onClick: () -> Unit,
backgroundColor: Brush = SolidColor(myColors.surface),
disabledBackgroundColor: Brush = SolidColor(myColors.surface / 0.5f),
borderColor: Brush = SolidColor(myColors.onBackground / 10),
disabledBorderColor: Brush = SolidColor(myColors.onBackground / 10),
contentColor: Color = LocalContentColor.current,
contentPadding: PaddingValues = PaddingValues(vertical = 6.dp, horizontal = 24.dp),
start: (@Composable RowScope.() -> Unit)? = null,
end: (@Composable RowScope.() -> Unit)? = null,
) {
val shape = RoundedCornerShape(10.dp)
Box(
Row(
modifier
.border(1.dp, if (enabled)borderColor else disabledBorderColor, shape)
.border(1.dp, if (enabled) borderColor else disabledBorderColor, shape)
.clip(shape)
.background(if (enabled) backgroundColor else disabledBackgroundColor)
.clickable(enabled = enabled) {
onClick()
}
.padding(vertical = 6.dp, horizontal = 24.dp)
.padding(contentPadding)
) {
WithContentColor(contentColor){
WithContentColor(contentColor) {
WithContentAlpha(if (enabled) 1f else 0.5f) {
start?.let {
it()
}
Text(
text = text,
modifier = Modifier.align(Alignment.Center),
modifier = Modifier,
fontSize = myTextSizes.base,
maxLines = 1,
softWrap = false,
)
end?.let {
it()
}
}
}
}

View File

@ -5,15 +5,14 @@ import com.abdownloadmanager.utils.category.CategoryManager
import com.abdownloadmanager.utils.category.CategorySelectionMode
import ir.amirab.downloader.DownloadManager
import ir.amirab.downloader.db.IDownloadListDb
import ir.amirab.downloader.downloaditem.DownloadItem
import ir.amirab.downloader.downloaditem.DownloadItemContext
import ir.amirab.downloader.downloaditem.DownloadStatus
import ir.amirab.downloader.downloaditem.EmptyContext
import ir.amirab.downloader.downloaditem.*
import ir.amirab.downloader.downloaditem.contexts.RemovedBy
import ir.amirab.downloader.downloaditem.contexts.ResumedBy
import ir.amirab.downloader.downloaditem.contexts.StoppedBy
import ir.amirab.downloader.downloaditem.contexts.User
import ir.amirab.downloader.monitor.IDownloadMonitor
import ir.amirab.downloader.monitor.isDownloadActiveFlow
import ir.amirab.downloader.monitor.statusOrFinished
import ir.amirab.downloader.queue.QueueManager
import ir.amirab.downloader.utils.OnDuplicateStrategy
import kotlinx.coroutines.CoroutineScope
@ -223,4 +222,35 @@ class DownloadSystem(
it.id
}
}
fun getAllRegisteredDownloadFiles(): List<File> {
return downloadMonitor.run {
activeDownloadListFlow.value + completedDownloadListFlow.value
}.map {
File(it.folder, it.name)
}
}
suspend fun isDownloadActive(id: Long): Boolean {
return downloadMonitor.isDownloadActiveFlow(id).value
}
suspend fun editDownload(updatedItem: DownloadItem) {
val wasActive = isDownloadActive(updatedItem.id)
if (wasActive) {
manualPause(updatedItem.id)
}
downloadManager.updateDownloadItem(updatedItem.id) { currentItem ->
var shouldUpdate = true
if (currentItem.folder == updatedItem.folder && currentItem.name != updatedItem.name) {
val success = getDownloadFile(currentItem).renameTo(getDownloadFile(updatedItem))
shouldUpdate = success
}
if (shouldUpdate) {
currentItem.applyFrom(updatedItem)
}
}
if (wasActive) {
manualResume(updatedItem.id)
}
}
}

View File

@ -4,13 +4,14 @@ import ir.amirab.downloader.connection.DownloaderClient
import ir.amirab.downloader.connection.response.ResponseInfo
import ir.amirab.downloader.downloaditem.DownloadCredentials
import com.abdownloadmanager.utils.isValidUrl
import ir.amirab.downloader.downloaditem.IDownloadCredentials
import ir.amirab.util.UrlUtils
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
class LinkChecker(
initialCredentials: DownloadCredentials = DownloadCredentials.empty(),
initialCredentials: IDownloadCredentials = DownloadCredentials.empty(),
private val client: DownloaderClient,
) {
//input

View File

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 17.46V20.5C3 20.78 3.22 21 3.5 21H6.54C6.67 21 6.8 20.95 6.89 20.85L17.81 9.94L14.06 6.19L3.15 17.1C3.05 17.2 3 17.32 3 17.46ZM20.71 7.04C20.8027 6.94749 20.8762 6.8376 20.9264 6.71663C20.9766 6.59565 21.0024 6.46597 21.0024 6.335C21.0024 6.20403 20.9766 6.07435 20.9264 5.95338C20.8762 5.83241 20.8027 5.72252 20.71 5.63L18.37 3.29C18.2775 3.1973 18.1676 3.12375 18.0466 3.07357C17.9257 3.02339 17.796 2.99756 17.665 2.99756C17.534 2.99756 17.4043 3.02339 17.2834 3.07357C17.1624 3.12375 17.0525 3.1973 16.96 3.29L15.13 5.12L18.88 8.87L20.71 7.04Z"
fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 694 B

View File

@ -15,6 +15,21 @@ data class DownloadCredentials(
fun empty()=DownloadCredentials(
link = ""
)
fun from(credentials: IDownloadCredentials): DownloadCredentials {
credentials.run {
return when (this) {
is DownloadCredentials -> this
else -> DownloadCredentials(
link = link,
headers = headers,
username = username,
password = password,
downloadPage = downloadPage,
userAgent = userAgent,
)
}
}
}
}
}
@ -26,16 +41,3 @@ interface IDownloadCredentials {
val downloadPage: String?
val userAgent: String?
}
fun IDownloadCredentials.copy(): DownloadCredentials {
return when (this) {
is DownloadCredentials -> this
else -> DownloadCredentials(
link = link,
headers = headers,
username = username,
password = password,
downloadPage = downloadPage,
userAgent = userAgent,
)
}
}

View File

@ -16,7 +16,7 @@ data class DownloadItem(
var name: String,
var contentLength: Long = LENGTH_UNKNOWN,
var serverETag:String? = null,
var serverETag: String? = null,
var dateAdded: Long = 0,
var startTime: Long? = null,
@ -30,6 +30,29 @@ data class DownloadItem(
}
}
fun DownloadItem.applyFrom(other: DownloadItem) {
link = other.link
headers = other.headers
username = other.username
password = other.password
downloadPage = other.downloadPage
userAgent = other.userAgent
id = other.id
folder = other.folder
name = other.name
contentLength = other.contentLength
serverETag = other.serverETag
dateAdded = other.dateAdded
startTime = other.startTime
completeTime = other.completeTime
status = other.status
preferredConnectionCount = other.preferredConnectionCount
speedLimit = other.speedLimit
}
fun DownloadItem.withCredentials(credentials: IDownloadCredentials) = apply {
link = credentials.link
headers = credentials.headers

View File

@ -52,15 +52,18 @@ class DownloadJob(
private val _isDownloadActive = MutableStateFlow(false)
val isDownloadActive = _isDownloadActive.asStateFlow()
private fun initializeDestination() {
val outFile = downloadManager.calculateOutputFile(downloadItem)
destination = SimpleDownloadDestination(
file = outFile,
diskStat = downloadManager.diskStat,
emptyFileCreator = downloadManager.emptyFileCreator
)
}
suspend fun boot() {
if (!booted) {
val outFile = downloadManager.calculateOutputFile(downloadItem)
destination = SimpleDownloadDestination(
file = outFile,
diskStat = downloadManager.diskStat,
emptyFileCreator = downloadManager.emptyFileCreator
)
initializeDestination()
loadPartState()
supportsConcurrent = when (getParts().size) {
in 2..Int.MAX_VALUE -> true
@ -226,6 +229,11 @@ class DownloadJob(
suspend fun changeConfig(updater: (DownloadItem) -> Unit): DownloadItem {
val last = downloadItem.copy()
downloadItem.apply(updater)
if (downloadManager.calculateOutputFile(last) != downloadManager.calculateOutputFile(downloadItem)) {
pause()
// destination should be closed for now!
initializeDestination()
}
if (last.preferredConnectionCount != downloadItem.preferredConnectionCount) {
onPreferredConnectionCountChanged()
}

View File

@ -1,5 +1,7 @@
package ir.amirab.downloader.monitor
import ir.amirab.downloader.downloaditem.DownloadJobStatus
import ir.amirab.util.flow.mapStateFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -14,4 +16,16 @@ interface IDownloadMonitor {
suspend fun waitForDownloadToFinishOrCancel(
id: Long
): Boolean
}
fun IDownloadMonitor.isDownloadActiveFlow(
downloadId: Long,
): StateFlow<Boolean> {
return activeDownloadListFlow.mapStateFlow { activeDownloadList ->
activeDownloadList.find {
downloadId == it.id
}?.statusOrFinished()?.let {
it is DownloadJobStatus.IsActive
} ?: false
}
}

View File

@ -23,6 +23,7 @@ interface IMyIcons {
val windowMaximize: IconSource
val windowClose: IconSource
val exit: IconSource
val edit: IconSource
val undo: IconSource
// val menu: IconSource
// val menuClose: IconSource

View File

@ -20,6 +20,8 @@ close=Close
ok=Ok
add=Add
change=Change
edit=Edit
change_anyway=Change Anyway
download=Download
refresh=Refresh
settings=Settings
@ -99,7 +101,7 @@ group=Group
add_download=Add Download
add_multi_download_page_header=Select Items you want to pick up for download
save_to=Save To
where_should_each_item_saved=Where should each item saved?
where_should_each_item_saved=Where should each item be saved?
there_are_multiple_items_please_select_a_way_you_want_to_save_them=There are multiple items! please select a way you want to save them
each_item_on_its_own_category=Each item on its own category
each_item_on_its_own_category_description=Each item will be placed in a category that have that file type
@ -211,6 +213,8 @@ download_item_settings_thread_count_description=How much thread used to download
download_item_settings_thread_count_describe={{count}} threads for this download
download_item_settings_username_description=Provide a username if the link is a protected resource
download_item_settings_password_description=Provide a password if the link is a protected resource
download_item_settings_download_page=Download Page
download_item_settings_download_page_description=The webpage where this download was initiated
username=Username
password=Password
average_speed=Average Speed
@ -282,3 +286,7 @@ address=Address
port=Port
address_and_port=Address & Port
use_authentication=Use Authentication
warning_you_may_have_to_restart_the_download_later=You may have to restart the download later!
edit_download_title=Edit Download
edit_download_update_from_download_page=Update from Download Page
edit_download_update_from_download_page_description=When this window is open, you can go to the Download Page and click the download button. The app will capture and update the new download credentials so you can save them.