From bf4449c91097a375716416ba0ff7ed81afda58d3 Mon Sep 17 00:00:00 2001 From: AmirHossein Abdolmotallebi Date: Sun, 22 Sep 2024 03:47:14 +0330 Subject: [PATCH] add batch download --- .../abdownloadmanager/desktop/AppComponent.kt | 91 +++-- .../abdownloadmanager/desktop/actions/main.kt | 7 +- .../batchdownload/BatchDownloadComponent.kt | 192 +++++++++ .../batchdownload/BatchDownloadWindow.kt | 28 ++ .../pages/batchdownload/BatchDownnload.kt | 385 ++++++++++++++++++ .../desktop/pages/home/HomeComponent.kt | 1 + .../com/abdownloadmanager/desktop/ui/Ui.kt | 5 + 7 files changed, 685 insertions(+), 24 deletions(-) create mode 100644 desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/batchdownload/BatchDownloadComponent.kt create mode 100644 desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/batchdownload/BatchDownloadWindow.kt create mode 100644 desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/batchdownload/BatchDownnload.kt diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/AppComponent.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/AppComponent.kt index da33701..c969fcd 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/AppComponent.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/AppComponent.kt @@ -4,6 +4,7 @@ import com.abdownloadmanager.desktop.pages.addDownload.AddDownloadComponent import com.abdownloadmanager.desktop.pages.addDownload.AddDownloadConfig import com.abdownloadmanager.desktop.pages.addDownload.multiple.AddMultiDownloadComponent import com.abdownloadmanager.desktop.pages.addDownload.single.AddSingleDownloadComponent +import com.abdownloadmanager.desktop.pages.batchdownload.BatchDownloadComponent import com.abdownloadmanager.desktop.pages.home.HomeComponent import com.abdownloadmanager.desktop.pages.queue.QueuesComponent import com.abdownloadmanager.desktop.pages.settings.SettingsComponent @@ -46,13 +47,13 @@ import kotlin.system.exitProcess sealed interface AppEffects { data class SimpleNotificationNotification( - val notificationModel: NotificationModel + val notificationModel: NotificationModel, ) : AppEffects } -interface NotificationSender{ - fun sendDialogNotification(title: String,description: String,type: MessageDialogType) - fun sendNotification(tag: Any,title:String,description: String,type: NotificationType) +interface NotificationSender { + fun sendDialogNotification(title: String, description: String, type: MessageDialogType) + fun sendNotification(tag: Any, title: String, description: String, type: NotificationType) } class AppComponent( @@ -117,6 +118,28 @@ class AppComponent( } ).subscribeAsStateFlow() + class BatchDownloadConfig + + private val batchDownload = SlotNavigation() + val batchDownloadSlot = childSlot( + batchDownload, + serializer = null, + key = "batchDownload", + childFactory = { _: BatchDownloadConfig, componentContext: ComponentContext -> + BatchDownloadComponent( + ctx = componentContext, + onClose = this::closeBatchDownload, + importLinks = { + openAddDownloadDialog(it.map { + DownloadCredentials( + link = it + ) + }) + } + ) + } + ).subscribeAsStateFlow() + fun openSettings() { scope.launch { @@ -259,11 +282,11 @@ class AppComponent( type: MessageDialogType, ) { beep() - newDialogMessage(MessageDialogModel(title = title, description = description, type = type,)) + newDialogMessage(MessageDialogModel(title = title, description = description, type = type)) } private fun beep() { - if (appSettings.notificationSound.value){ + if (appSettings.notificationSound.value) { Toolkit.getDefaultToolkit().beep() } } @@ -285,6 +308,7 @@ class AppComponent( ) ) } + init { downloadSystem .downloadEvents @@ -309,6 +333,7 @@ class AppComponent( IntegrationResult.Inactive -> { IntegrationPortBroadcaster.setIntegrationPortInFile(null) } + is IntegrationResult.Success -> { IntegrationPortBroadcaster.setIntegrationPortInFile(it.port) } @@ -317,7 +342,7 @@ class AppComponent( } private fun onNewDownloadEvent(it: DownloadManagerEvents) { - if (it.context[ResumedBy]?.by !is User){ + if (it.context[ResumedBy]?.by !is User) { //only notify events that is started by user return } @@ -337,19 +362,19 @@ class AppComponent( return } var isMaxTryReachedError = false - val actualCause = if (exception is TooManyErrorException){ - isMaxTryReachedError=true + val actualCause = if (exception is TooManyErrorException) { + isMaxTryReachedError = true exception.findActualDownloadErrorCause() - }else exception + } else exception if (ExceptionUtils.isNormalCancellation(actualCause)) { return } val prefix = if (isMaxTryReachedError) { "Too Many Error: " - }else{ + } else { "Error: " } - val reason = actualCause.message?:"Unknown" + val reason = actualCause.message ?: "Unknown" sendNotification( "downloadId=${it.downloadItem.id}", title = it.downloadItem.name, @@ -369,7 +394,7 @@ class AppComponent( override suspend fun openDownloadItem(id: Long) { val item = downloadSystem.getDownloadItemById(id) - if (item==null){ + if (item == null) { sendNotification( "Open File", "Can't open file", @@ -380,6 +405,7 @@ class AppComponent( } openDownloadItem(item) } + override fun openDownloadItem(downloadItem: DownloadItem) { runCatching { FileUtils.openFile(downloadSystem.getDownloadFile(downloadItem)) @@ -396,7 +422,7 @@ class AppComponent( override suspend fun openDownloadItemFolder(id: Long) { val item = downloadSystem.getDownloadItemById(id) - if (item==null){ + if (item == null) { sendNotification( "Open Folder", "Can't open folder", @@ -423,7 +449,7 @@ class AppComponent( } override fun openAddDownloadDialog( - links: List + links: List, ) { scope.launch { //remove duplicates @@ -548,22 +574,23 @@ class AppComponent( } fun closeAbout() { - showAboutPage .update { false } + showAboutPage.update { false } } fun openOpenSourceLibraries() { - showOpenSourceLibraries .update { true } + showOpenSourceLibraries.update { true } } + fun closeOpenSourceLibraries() { - showOpenSourceLibraries .update { false } + showOpenSourceLibraries.update { false } } fun openQueues() { scope.launch { showQueuesSlot.value.child?.instance.let { - if (it!=null){ + if (it != null) { it.bringToFront() - }else{ + } else { showQueues.activate(QueuePageConfig()) } } @@ -574,6 +601,23 @@ class AppComponent( showQueues.dismiss() } + fun openBatchDownload() { + scope.launch { + + batchDownloadSlot.value.child?.instance.let { + if (it != null) { + it.bringToFront() + } else { + batchDownload.activate(BatchDownloadConfig()) + } + } + } + } + + fun closeBatchDownload() { + batchDownload.dismiss() + } + var showCreateQueueDialog = MutableStateFlow(false) private set @@ -613,10 +657,11 @@ class AppComponent( IntegrationPortBroadcaster.isInitialized(), ).all { it } } -// TODO enable updater + + // TODO enable updater // val updater = UpdateComponent(childContext("updater")) - val showAboutPage=MutableStateFlow(false) - val showOpenSourceLibraries=MutableStateFlow(false) + val showAboutPage = MutableStateFlow(false) + val showOpenSourceLibraries = MutableStateFlow(false) val theme = appRepository.theme // val uiScale = appRepository.uiScale } diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/actions/main.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/actions/main.kt index e662698..35829d0 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/actions/main.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/actions/main.kt @@ -50,7 +50,12 @@ val newDownloadFromClipboardAction = simpleAction( } appComponent.openAddDownloadDialog(items) } - +val batchDownloadAction = simpleAction( + title = "Batch Download", + icon = MyIcons.download +) { + appComponent.openBatchDownload() +} val stopQueueGroupAction = MenuItem.SubMenu( icon = MyIcons.stop, title = "Stop Queue", diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/batchdownload/BatchDownloadComponent.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/batchdownload/BatchDownloadComponent.kt new file mode 100644 index 0000000..5261636 --- /dev/null +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/batchdownload/BatchDownloadComponent.kt @@ -0,0 +1,192 @@ +package com.abdownloadmanager.desktop.pages.batchdownload + +import com.abdownloadmanager.desktop.utils.BaseComponent +import com.abdownloadmanager.desktop.utils.ClipboardUtil +import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects +import com.abdownloadmanager.desktop.utils.mvi.supportEffects +import com.abdownloadmanager.utils.isValidUrl +import com.arkivanov.decompose.ComponentContext +import ir.amirab.util.flow.combineStateFlows +import ir.amirab.util.flow.mapStateFlow +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.math.max + +sealed interface BatchDownloadEffects { + data object BringToFront : BatchDownloadEffects +} +class BatchDownloadComponent( + ctx: ComponentContext, + val onClose: () -> Unit, + val importLinks: (List) -> Unit, +) : BaseComponent(ctx), ContainsEffects by supportEffects() { + + private val _link = MutableStateFlow("") + val link = _link.asStateFlow() + + fun setLink(link: String) { + _link.value = link + } + + private val _start = MutableStateFlow("") + val start = _start.asStateFlow() + + fun setStart(start: String) { + _start.value = start + } + + private val _end = MutableStateFlow("") + val end = _end.asStateFlow() + + fun setEnd(end: String) { + _end.value = end + } + + private val _wildcardLength = MutableStateFlow(WildcardLength.Auto) + val wildcardLength = _wildcardLength + fun setWildCardLength(wildcardLength: WildcardLength) { + _wildcardLength.value = wildcardLength + } + + init { + fillLinkIfUrlIsInClipboard() + } + + private fun fillLinkIfUrlIsInClipboard() { + scope.launch { + withContext(Dispatchers.Default) { + val clipboard = ClipboardUtil.read() ?: return@withContext + if (isValidUrl(clipboard)) { + setLink(clipboard.trim()) + } + } + } + } + + @Suppress("NAME_SHADOWING") + private val batch = combineStateFlows( + link, + start, + end, + wildcardLength, + ) { link, start, end, wildcardLength -> + val minimumSize = max(start.length, end.length) + val start = start.toIntOrNull() ?: return@combineStateFlows null + val end = end.toIntOrNull() ?: return@combineStateFlows null + if (start < 0) return@combineStateFlows null + if (end < 0 || end < start) return@combineStateFlows null + WildcardString( + string = link.trim(), + range = start..end, + wildcardLength = wildcardLength, + minimumAllowed = minimumSize, + ) + } + + + fun bringToFront() { + sendEffect(BatchDownloadEffects.BringToFront) + } + + + val startLinkResult: StateFlow = batch + .mapStateFlow { it?.first() ?: "" } + val endLinkResult: StateFlow = batch + .mapStateFlow { it?.last() ?: "" } + + + val validationResult = batch.mapStateFlow { + when (it) { + null -> BatchDownloadValidationResult.Others + else -> { + val listSize = it.size() + when { + listSize < 1 -> BatchDownloadValidationResult.Others + listSize > MAX_ALLOWED_RANGE -> BatchDownloadValidationResult.MaxRangeExceed(MAX_ALLOWED_RANGE) + !isValidUrl(it.first()) -> BatchDownloadValidationResult.URLInvalid + else -> BatchDownloadValidationResult.Ok + } + } + } + + } + + val canConfirm = validationResult.mapStateFlow { + it is BatchDownloadValidationResult.Ok + } + + fun confirm() { + if (!canConfirm.value) { + println(batch.value?.toList()) + return + } + val items = batch.value?.toList()?.takeIf { it.isNotEmpty() } + if (items != null) { + importLinks(items) + } + onClose() + } + + companion object { + const val MAX_ALLOWED_RANGE = 1000 + } +} + +sealed interface BatchDownloadValidationResult { + data object Ok : BatchDownloadValidationResult + data object Others : BatchDownloadValidationResult + data class MaxRangeExceed(val allowed: Int) : BatchDownloadValidationResult + data object URLInvalid : BatchDownloadValidationResult +} + +sealed class WildcardLength { + data object Auto : WildcardLength() + data object Unspecified : WildcardLength() + data class Custom(val v: Int) : WildcardLength() +} + +data class WildcardString( + val string: String, + val range: IntRange, + val wildcardLength: WildcardLength, + val minimumAllowed: Int = range.last.toString().length, +) : Iterable { + private fun transformIndex(index: Int): String { + var str = index.toString() + if (wildcardLength is WildcardLength.Unspecified) { + return str + } + val length = when (wildcardLength) { + is WildcardLength.Custom -> wildcardLength.v.coerceAtLeast(minimumAllowed) + WildcardLength.Auto -> minimumAllowed + WildcardLength.Unspecified -> null + } + if (length != null) { + str = str.padStart(length, '0') + } + return str + } + + fun get(index: Int): String { + return string.replace("*", transformIndex(index)) + } + + fun first(): String { + return get(range.first) + } + + fun last(): String { + return get(range.last) + } + + fun size() = range.last - range.first + 1 + + override fun iterator(): Iterator { + return range + .asSequence() + .map(::get) + .iterator() + } +} \ No newline at end of file diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/batchdownload/BatchDownloadWindow.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/batchdownload/BatchDownloadWindow.kt new file mode 100644 index 0000000..28cafaa --- /dev/null +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/batchdownload/BatchDownloadWindow.kt @@ -0,0 +1,28 @@ +package com.abdownloadmanager.desktop.pages.batchdownload + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.rememberWindowState +import com.abdownloadmanager.desktop.ui.customwindow.CustomWindow +import com.abdownloadmanager.desktop.utils.mvi.HandleEffects + +@Composable +fun BatchDownloadWindow(batchDownloadComponent: BatchDownloadComponent) { + CustomWindow( + state = rememberWindowState( + size = DpSize(500.dp, 420.dp), + position = WindowPosition(Alignment.Center) + ), + onCloseRequest = batchDownloadComponent.onClose + ) { + HandleEffects(batchDownloadComponent) { + when (it) { + BatchDownloadEffects.BringToFront -> window.toFront() + } + } + BatchDownload(batchDownloadComponent) + } +} \ No newline at end of file diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/batchdownload/BatchDownnload.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/batchdownload/BatchDownnload.kt new file mode 100644 index 0000000..0c6fb49 --- /dev/null +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/batchdownload/BatchDownnload.kt @@ -0,0 +1,385 @@ +package com.abdownloadmanager.desktop.pages.batchdownload + +import androidx.compose.animation.AnimatedVisibility +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.foundation.text.KeyboardOptions +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerIcon +import androidx.compose.ui.input.pointer.pointerHoverIcon +import androidx.compose.ui.unit.dp +import com.abdownloadmanager.desktop.pages.addDownload.single.MyTextFieldIcon +import com.abdownloadmanager.desktop.pages.batchdownload.WildcardSelect.* +import com.abdownloadmanager.desktop.ui.customwindow.WindowTitle +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.util.ifThen +import com.abdownloadmanager.desktop.ui.widget.* +import com.abdownloadmanager.desktop.utils.ClipboardUtil +import com.abdownloadmanager.desktop.utils.div +import com.abdownloadmanager.utils.compose.LocalContentColor +import com.abdownloadmanager.utils.compose.WithContentAlpha +import com.abdownloadmanager.utils.compose.widget.MyIcon +import ir.amirab.util.compose.IconSource + +@Composable +fun BatchDownload( + component: BatchDownloadComponent, +) { + WindowTitle("Batch Download") + val link by component.link.collectAsState() + val setLink = component::setLink + val start by component.start.collectAsState() + val setStart = component::setStart + val end by component.end.collectAsState() + val setEnd = component::setEnd + val scrollState = rememberScrollState() + val scrollAdapter = rememberScrollbarAdapter(scrollState) + val validationResult by component.validationResult.collectAsState() + val linkFocusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { + linkFocusRequester.requestFocus() + } + Column(Modifier.padding(16.dp)) { + Row(Modifier.weight(1f)) { + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(scrollState) + ) { + LabeledContent( + label = { + Text("Enter a link that contains wildcards (use *)") + }, + content = { + BatchDownloadPageTextField( + text = link, + onTextChange = setLink, + placeholder = "Link: https://example.com/photo-*.png", + modifier = Modifier + .focusRequester(linkFocusRequester) + .fillMaxWidth(), + start = { + MyTextFieldIcon(MyIcons.link) + }, + end = { + MyTextFieldIcon(MyIcons.paste, { + val v = ClipboardUtil.read() + if (v != null) { + setLink(v) + } + }) + }, + errorText = when (val v = validationResult) { + BatchDownloadValidationResult.URLInvalid -> { + "Invalid URL" + } + + is BatchDownloadValidationResult.MaxRangeExceed -> "List is too large! maximum ${v.allowed} items allowed" + BatchDownloadValidationResult.Others -> null + BatchDownloadValidationResult.Ok -> null + } + ) + } + ) + Spacer(Modifier.height(8.dp)) + LabeledContent( + label = { + Text("Enter range") + }, + content = { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + BatchDownloadPageTextField( + text = start, + onTextChange = setStart, + placeholder = "Start", + modifier = Modifier.width(80.dp), + start = { + Text("A", Modifier.padding(horizontal = 8.dp)) + } + ) + Spacer(Modifier.width(8.dp)) + Text("To") + Spacer(Modifier.width(8.dp)) + + BatchDownloadPageTextField( + text = end, + onTextChange = setEnd, + placeholder = "End", + modifier = Modifier.width(80.dp), + start = { + Text("B", Modifier.padding(horizontal = 8.dp)) + } + ) + } + } + ) + Spacer(Modifier.height(8.dp)) + LabeledContent( + label = { + Text("Wildcard length") + }, + content = { + WildcardLengthUi( + component.wildcardLength.collectAsState().value, + component::setWildCardLength + ) + } + ) + Spacer(Modifier.height(8.dp)) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + val lineModifier = Modifier + .height(1.dp) + .padding(horizontal = 5.dp) + .background(LocalContentColor.current.copy(0.05f)) + + Spacer(Modifier.padding(vertical = 4.dp).fillMaxWidth().then(lineModifier)) + } + Spacer(Modifier.height(8.dp)) + LabeledContent( + label = { + Text("First Link") + }, + content = { + Text( + component.startLinkResult.collectAsState().value, + Modifier + .fillMaxWidth() + .background(myColors.surface) + .padding(2.dp) + ) + } + ) + Spacer(Modifier.height(8.dp)) + LabeledContent( + label = { + Text("Last Link") + }, + content = { + Text( + component.endLinkResult.collectAsState().value, + Modifier + .fillMaxWidth() + .background(myColors.surface) + .padding(2.dp) + ) + } + ) + } + VerticalScrollbar(scrollAdapter, Modifier.fillMaxHeight()) + } + Spacer(Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End) + ) { + ActionButton( + text = "OK", + enabled = component.canConfirm.collectAsState().value, + onClick = component::confirm + ) + Spacer(Modifier.width(8.dp)) + ActionButton("Cancel", onClick = component.onClose) + } + } +} + +enum class WildcardSelect { + Auto, Unspecified, Custom; + + companion object { + fun fromWildcardLength(wildcardLength: WildcardLength): WildcardSelect { + return when (wildcardLength) { + WildcardLength.Auto -> Auto + is WildcardLength.Custom -> Custom + WildcardLength.Unspecified -> Unspecified + } + } + } +} + +@Composable +private fun WildcardLengthUi( + wildcardLength: WildcardLength, + onChangeWildcardLength: (WildcardLength) -> Unit, +) { + var customLength by remember { + mutableStateOf(2) + } + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Multiselect( + selections = WildcardSelect.entries, + selectedItem = WildcardSelect.fromWildcardLength(wildcardLength), + onSelectionChange = { + onChangeWildcardLength( + when (it) { + Auto -> WildcardLength.Auto + Unspecified -> WildcardLength.Unspecified + Custom -> WildcardLength.Custom(customLength) + } + ) + }, + render = { + Text(it.toString()) + } + ) + AnimatedVisibility(wildcardLength is WildcardLength.Custom) { + Row { + Spacer(Modifier.width(8.dp)) + IntTextField( + value = customLength, + onValueChange = { + customLength = it + onChangeWildcardLength( + WildcardLength.Custom(it) + ) + }, + range = 1..10, + keyboardOptions = KeyboardOptions.Default, + modifier = Modifier.width(72.dp) + ) + } + } + } +} + +@Composable +private fun Multiselect( + selections: List, + selectedItem: T, + onSelectionChange: (T) -> Unit, + render: @Composable (T) -> Unit, +) { + val shape = RoundedCornerShape(6.dp) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(shape) + .background(myColors.surface) + ) { + for (item in selections) { + val isSelected = item == selectedItem + Box( + Modifier + .padding(vertical = 4.dp, horizontal = 4.dp) + .clip(shape) + .ifThen(isSelected) { + background(LocalContentColor.current / 10) + } + .clickable { + onSelectionChange(item) + } + .padding(vertical = 2.dp, horizontal = 4.dp) + ) { + WithContentAlpha( + if (isSelected) { + 1f + } else { + 0.5f + } + ) { + render(item) + } + } + } + } +} + +@Composable +private fun LabeledContent( + label: @Composable () -> Unit, + content: @Composable () -> Unit, +) { + Column { + label() + Spacer(Modifier.height(8.dp)) + content() + } +} + + +@Composable +private fun BatchDownloadPageTextField( + text: String, + onTextChange: (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, + onTextChange, + 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, + ) + } + } + } +} + +@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)) +} \ No newline at end of file diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/HomeComponent.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/HomeComponent.kt index f0ef4ea..b88a37c 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/HomeComponent.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/home/HomeComponent.kt @@ -322,6 +322,7 @@ class HomeComponent( subMenu("File") { +newDownloadAction +newDownloadFromClipboardAction + +batchDownloadAction separator() +exitAction diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/Ui.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/Ui.kt index f335357..5d6a1c8 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/Ui.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/Ui.kt @@ -25,6 +25,7 @@ import com.abdownloadmanager.desktop.utils.isInDebugMode import com.abdownloadmanager.desktop.utils.mvi.HandleEffects import androidx.compose.runtime.* import androidx.compose.ui.window.* +import com.abdownloadmanager.desktop.pages.batchdownload.BatchDownloadWindow import com.abdownloadmanager.desktop.pages.home.HomeWindow import com.abdownloadmanager.desktop.pages.settings.ThemeManager import com.abdownloadmanager.utils.compose.ProvideDebugInfo @@ -71,6 +72,10 @@ object Ui : KoinComponent { showQueuesSlot.child?.instance?.let { QueuesWindow(it) } + val batchDownloadSlot = appComponent.batchDownloadSlot.collectAsState().value + batchDownloadSlot.child?.instance?.let { + BatchDownloadWindow(it) + } ShowAddDownloadDialogs(appComponent) ShowDownloadDialogs(appComponent) //TODO Enable Updater