mirror of
https://github.com/amir1376/ab-download-manager.git
synced 2025-02-20 11:43:24 +08:00
Merge pull request #84 from amir1376/feature/batch-download
add batch download
This commit is contained in:
commit
8ec33e1fa1
@ -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<BatchDownloadConfig>()
|
||||
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<DownloadCredentials>
|
||||
links: List<DownloadCredentials>,
|
||||
) {
|
||||
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
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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<String>) -> Unit,
|
||||
) : BaseComponent(ctx), ContainsEffects<BatchDownloadEffects> 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>(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<String> = batch
|
||||
.mapStateFlow { it?.first() ?: "" }
|
||||
val endLinkResult: StateFlow<String> = 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<String> {
|
||||
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<String> {
|
||||
return range
|
||||
.asSequence()
|
||||
.map(::get)
|
||||
.iterator()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 <T> Multiselect(
|
||||
selections: List<T>,
|
||||
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))
|
||||
}
|
@ -322,6 +322,7 @@ class HomeComponent(
|
||||
subMenu("File") {
|
||||
+newDownloadAction
|
||||
+newDownloadFromClipboardAction
|
||||
+batchDownloadAction
|
||||
separator()
|
||||
+exitAction
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user