diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/di/Di.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/di/Di.kt index bc012a2..762f9e2 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/di/Di.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/di/Di.kt @@ -134,12 +134,15 @@ val downloadSystemModule = module { } ) } + single { + DownloadManagerCategoryItemProvider(get()) + }.bind() single { CategoryManager( categoryStorage = get(), scope = get(), defaultCategoriesFactory = get(), - downloadManager = get(), + categoryItemProvider = get(), ) } diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/multiple/AddMultiDownloadComponent.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/multiple/AddMultiDownloadComponent.kt index ca0f0e6..5684348 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/multiple/AddMultiDownloadComponent.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/multiple/AddMultiDownloadComponent.kt @@ -13,6 +13,7 @@ import com.abdownloadmanager.desktop.pages.addDownload.multiple.AddMultiItemSave import com.abdownloadmanager.desktop.utils.asState import com.abdownloadmanager.utils.FileIconProvider import com.abdownloadmanager.utils.category.Category +import com.abdownloadmanager.utils.category.CategoryItem import com.abdownloadmanager.utils.category.CategoryManager import com.abdownloadmanager.utils.category.CategorySelectionMode import com.arkivanov.decompose.ComponentContext @@ -179,13 +180,19 @@ class AddMultiDownloadComponent( private fun getFolderForItem( categorySelectionMode: CategorySelectionMode?, + url: String, fleName: String, defaultFolder: String, ): String { return when (categorySelectionMode) { CategorySelectionMode.Auto -> { downloadSystem.categoryManager - .getCategoryOfFileName(fleName)?.path + .getCategoryOf( + CategoryItem( + url = url, + fileName = fleName, + ) + )?.path ?: defaultFolder } @@ -224,6 +231,7 @@ class AddMultiDownloadComponent( id = -1, folder = getFolderForItem( categorySelectionMode = categorySelectionMode, + url = it.credentials.value.link, fleName = it.name.value, defaultFolder = it.folder.value ), diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/single/AddSingleDownloadComponent.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/single/AddSingleDownloadComponent.kt index c7b87ff..3368e5e 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/single/AddSingleDownloadComponent.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/addDownload/single/AddSingleDownloadComponent.kt @@ -29,6 +29,7 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import com.abdownloadmanager.utils.FileIconProvider import com.abdownloadmanager.utils.category.Category +import com.abdownloadmanager.utils.category.CategoryItem import com.abdownloadmanager.utils.category.CategoryManager sealed interface AddSingleDownloadPageEffects { @@ -128,9 +129,15 @@ class AddSingleDownloadComponent( ) .onEachLatest { onDuplicateStrategy.update { null } } .launchIn(scope) - - name.onEach { - val category = categoryManager.getCategoryOfFileName(it) + combine( + name, credentials.map { it.link } + ) { name, link -> + val category = categoryManager.getCategoryOf( + CategoryItem( + fileName = name, + url = link, + ) + ) if (category == null) { setUseCategory(false) } else { diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/category/CategoryComponent.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/category/CategoryComponent.kt index 9d99294..1d9a386 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/category/CategoryComponent.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/category/CategoryComponent.kt @@ -1,6 +1,5 @@ package com.abdownloadmanager.desktop.pages.category -import arrow.core.split import com.abdownloadmanager.desktop.repository.AppRepository import com.abdownloadmanager.desktop.utils.BaseComponent import com.abdownloadmanager.utils.category.Category @@ -40,6 +39,8 @@ class CategoryComponent( setIcon(category.iconSource()) setName(category.name) setTypes(category.acceptedFileTypes.joinToString(" ")) + setUrlPatternsEnabled(category.acceptedUrlPatterns.isNotEmpty()) + setUrlPatterns(category.acceptedUrlPatterns.joinToString(" ")) setPath(category.path) } } @@ -62,6 +63,18 @@ class CategoryComponent( _types.value = types } + private val _urlPatternsEnabled = MutableStateFlow(false) + val urlPatternsEnabled = _urlPatternsEnabled.asStateFlow() + fun setUrlPatternsEnabled(urlPatterns: Boolean) { + _urlPatternsEnabled.value = urlPatterns + } + + private val _urlPatterns = MutableStateFlow("") + val urlPatterns = _urlPatterns.asStateFlow() + fun setUrlPatterns(urlPatterns: String) { + _urlPatterns.value = urlPatterns + } + private val _path = MutableStateFlow("") val path = _path.asStateFlow() fun setPath(path: String) { @@ -101,6 +114,10 @@ class CategoryComponent( .value!! .uriOrNull()!!, path = path, + acceptedUrlPatterns = urlPatterns.value + .split(" ") + .filterNot { it.isBlank() } + .distinct(), items = emptyList() // ignored! ) ) diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/category/NewCategoryPage.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/category/NewCategoryPage.kt index da71a66..53d9873 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/category/NewCategoryPage.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/category/NewCategoryPage.kt @@ -64,6 +64,13 @@ fun NewCategory( onTypesChanged = categoryComponent::setTypes ) Spacer(Modifier.height(12.dp)) + CategoryAutoUrls( + urlPatterns = categoryComponent.urlPatterns.collectAsState().value, + onUrlPatternChanged = categoryComponent::setUrlPatterns, + enabled = categoryComponent.urlPatternsEnabled.collectAsState().value, + setEnabled = categoryComponent::setUrlPatternsEnabled + ) + Spacer(Modifier.height(12.dp)) CategoryDefaultPath( path = categoryComponent.path.collectAsState().value, onPathChanged = categoryComponent::setPath, @@ -152,8 +159,30 @@ fun CategoryAutoTypes( modifier = Modifier.fillMaxWidth(), placeholder = "ext1 ext2 ext3 (separate with space)", singleLine = false, - minLines = 2, - maxLines = 2, + ) + } +} + +@Composable +fun CategoryAutoUrls( + enabled: Boolean, + setEnabled: (Boolean) -> Unit, + urlPatterns: String, + onUrlPatternChanged: (String) -> Unit, +) { + OptionalWithLabel( + label = "URL patterns", + helpText = "Automatically put download from these URLs to this category. (when you add new download)\nSeparate URLs with space, you can also use * for wildcard", + enabled = enabled, + setEnabled = setEnabled + ) { + CategoryPageTextField( + text = urlPatterns, + onTextChange = onUrlPatternChanged, + modifier = Modifier.fillMaxWidth(), + placeholder = "dl.example.com/pics example.com/*/path", + enabled = enabled, + singleLine = false, ) } } @@ -197,6 +226,37 @@ private fun WithLabel( } } +@Composable +private fun OptionalWithLabel( + label: String, + modifier: Modifier = Modifier, + enabled: Boolean, + setEnabled: (Boolean) -> Unit, + helpText: String? = null, + content: @Composable () -> Unit, +) { + Column(modifier) { + Row(verticalAlignment = Alignment.CenterVertically) { + Row( + modifier = Modifier.onClick { + setEnabled(!enabled) + }, + verticalAlignment = Alignment.CenterVertically, + ) { + CheckBox(enabled, setEnabled, size = 16.dp) + Spacer(Modifier.width(8.dp)) + Text(label) + } + helpText?.let { + Spacer(Modifier.width(8.dp)) + Help(helpText) + } + } + Spacer(Modifier.height(8.dp)) + content() + } +} + @Composable private fun CategoryIcon( iconSource: IconSource?, @@ -328,6 +388,7 @@ private fun CategoryPageTextField( singleLine: Boolean = true, maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, minLines: Int = 1, + enabled: Boolean = true, start: @Composable (() -> Unit)? = null, end: @Composable (() -> Unit)? = null, ) { @@ -351,6 +412,7 @@ private fun CategoryPageTextField( background = myColors.surface / 50, interactionSource = interactionSource, shape = RoundedCornerShape(6.dp), + enabled = enabled, start = start?.let { { WithContentAlpha(0.5f) { diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/category/ShowCategoryDialogs.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/category/ShowCategoryDialogs.kt index 88bf477..71e9aa4 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/category/ShowCategoryDialogs.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/category/ShowCategoryDialogs.kt @@ -12,17 +12,7 @@ import com.abdownloadmanager.desktop.ui.customwindow.CustomWindow fun ShowCategoryDialogs(dialogManager: CategoryDialogManager) { val dialogs by dialogManager.openedCategoryDialogs.collectAsState() for (d in dialogs) { - CustomWindow( - onCloseRequest = { - d.close() - }, - alwaysOnTop = true, - state = rememberWindowState( - size = DpSize(350.dp, 350.dp) - ) - ) { - CategoryDialog(d) - } + CategoryDialog(d) } } @@ -30,5 +20,15 @@ fun ShowCategoryDialogs(dialogManager: CategoryDialogManager) { private fun CategoryDialog( component: CategoryComponent, ) { - NewCategory(component) + CustomWindow( + onCloseRequest = { + component.close() + }, + alwaysOnTop = true, + state = rememberWindowState( + size = DpSize(350.dp, 400.dp) + ) + ) { + NewCategory(component) + } } \ 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 fe70027..3b5966b 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 @@ -24,6 +24,7 @@ import com.abdownloadmanager.desktop.pages.category.CategoryDialogManager import com.abdownloadmanager.desktop.storage.AppSettingsStorage import com.abdownloadmanager.utils.FileIconProvider import com.abdownloadmanager.utils.category.Category +import com.abdownloadmanager.utils.category.CategoryItemWithId import com.abdownloadmanager.utils.category.CategoryManager import com.abdownloadmanager.utils.category.DefaultCategories import com.arkivanov.decompose.ComponentContext @@ -465,7 +466,13 @@ class HomeComponent( } categoryManager .autoAddItemsToCategoriesBasedOnFileNames( - unCategorizedItems.map { it.id to it.name } + unCategorizedItems.map { + CategoryItemWithId( + id = it.id, + fileName = it.name, + url = it.downloadLink, + ) + } ) } diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/widget/MyTextField.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/widget/MyTextField.kt index 1b72e28..bee603c 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/widget/MyTextField.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/ui/widget/MyTextField.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -47,7 +48,7 @@ fun MyTextField( modifier: Modifier, background: Color = myColors.surface, contentColor: Color = myColors.getContentColorFor(background).takeIf { it.isSpecified } - ?: LocalContentColor.current, + ?: LocalContentColor.current, focusedBorderColor: Color = myColors.primary, borderColor: Color = myColors.onBackground / 0.1f, shape: Shape = RoundedCornerShape(12.dp), @@ -70,10 +71,16 @@ fun MyTextField( val textSize = fontSize.takeOrElse { LocalTextStyle.current.fontSize } Row( modifier + .ifThen(!enabled) { + alpha(0.5f) + } .clip(shape) .height(IntrinsicSize.Max) // .height(32.dp) - .pointerHoverIcon(PointerIcon.Text) + .pointerHoverIcon( + if (enabled) PointerIcon.Text + else PointerIcon.Default + ) .onKeyEvent { if (it.key == Key.Escape) { fm.clearFocus() diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/DownloadSystem.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/DownloadSystem.kt index dd1a3f0..9937686 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/DownloadSystem.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/DownloadSystem.kt @@ -1,5 +1,6 @@ package com.abdownloadmanager.desktop.utils +import com.abdownloadmanager.utils.category.CategoryItemWithId import com.abdownloadmanager.utils.category.CategoryManager import com.abdownloadmanager.utils.category.CategorySelectionMode import ir.amirab.downloader.DownloadManager @@ -68,7 +69,12 @@ class DownloadSystem( CategorySelectionMode.Auto -> { categoryManager.autoAddItemsToCategoriesBasedOnFileNames( createdIds.mapIndexed { index: Int, id: Long -> - id to newItemsToAdd.get(index).name + val downloadItem = newItemsToAdd[index] + CategoryItemWithId( + id = id, + fileName = downloadItem.name, + url = downloadItem.link, + ) } ) } @@ -105,7 +111,7 @@ class DownloadSystem( } suspend fun removeDownload(id: Long, alsoRemoveFile: Boolean) { - downloadManager.deleteDownload(id, alsoRemoveFile,RemovedBy(User)) + downloadManager.deleteDownload(id, alsoRemoveFile, RemovedBy(User)) categoryManager.removeItemInCategories(listOf(id)) } @@ -113,7 +119,7 @@ class DownloadSystem( // if (mainDownloadQueue.isQueueActive) { // return false // } - downloadManager.resume(id,ResumedBy(User)) + downloadManager.resume(id, ResumedBy(User)) return true } @@ -131,7 +137,7 @@ class DownloadSystem( } suspend fun startQueue( - queueId: Long + queueId: Long, ) { val queue = queueManager.getQueue(queueId) if (queue.isQueueActive) { @@ -149,7 +155,7 @@ class DownloadSystem( } suspend fun stopQueue( - queueId: Long + queueId: Long, ) { queueManager.getQueue(queueId) .stop() diff --git a/shared/app-utils/src/main/kotlin/com/abdownloadmanager/utils/category/Category.kt b/shared/app-utils/src/main/kotlin/com/abdownloadmanager/utils/category/Category.kt index c679161..0b11625 100644 --- a/shared/app-utils/src/main/kotlin/com/abdownloadmanager/utils/category/Category.kt +++ b/shared/app-utils/src/main/kotlin/com/abdownloadmanager/utils/category/Category.kt @@ -18,11 +18,15 @@ import kotlinx.serialization.Serializable data class Category( val id: Long, val name: String, - val path: String, - val acceptedFileTypes: List, val icon: String, - val items: List, + val path: String, + val acceptedFileTypes: List = emptyList(), + // this is optional if nothing provided it means that every url is acceptable + val acceptedUrlPatterns: List = emptyList(), + val items: List = emptyList(), ) { + val hasUrlPattern = acceptedUrlPatterns.isNotEmpty() + fun acceptFileName(fileName: String): Boolean { return acceptedFileTypes.any { ext -> fileName.endsWith( @@ -37,6 +41,29 @@ data class Category( items = items.plus(newItems).distinct() ) } + + fun acceptUrl(url: String): Boolean { + if (!hasUrlPattern) { + return true + } + return acceptedUrlPatterns.any { + test( + patten = it, + input = url + ) + } + } +} + +private fun test( + patten: String, + input: String, +): Boolean { + return patten + .split("*") + .joinToString(".*") { Regex.escape(it) } + .toRegex() + .containsMatchIn(input) } fun Category.iconSource(): IconSource? { diff --git a/shared/app-utils/src/main/kotlin/com/abdownloadmanager/utils/category/CategoryItem.kt b/shared/app-utils/src/main/kotlin/com/abdownloadmanager/utils/category/CategoryItem.kt new file mode 100644 index 0000000..0d3400f --- /dev/null +++ b/shared/app-utils/src/main/kotlin/com/abdownloadmanager/utils/category/CategoryItem.kt @@ -0,0 +1,21 @@ +package com.abdownloadmanager.utils.category + +import androidx.compose.runtime.Immutable + +interface ICategoryItem { + val fileName: String + val url: String +} + +@Immutable +data class CategoryItem( + override val fileName: String, + override val url: String, +) : ICategoryItem + +@Immutable +data class CategoryItemWithId( + val id: Long, + override val fileName: String, + override val url: String, +) : ICategoryItem \ No newline at end of file diff --git a/shared/app-utils/src/main/kotlin/com/abdownloadmanager/utils/category/CategoryManager.kt b/shared/app-utils/src/main/kotlin/com/abdownloadmanager/utils/category/CategoryManager.kt index 3fedea5..a30316b 100644 --- a/shared/app-utils/src/main/kotlin/com/abdownloadmanager/utils/category/CategoryManager.kt +++ b/shared/app-utils/src/main/kotlin/com/abdownloadmanager/utils/category/CategoryManager.kt @@ -1,6 +1,5 @@ package com.abdownloadmanager.utils.category -import ir.amirab.downloader.DownloadManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview @@ -12,7 +11,7 @@ class CategoryManager( private val categoryStorage: CategoryStorage, private val scope: CoroutineScope, private val defaultCategoriesFactory: DefaultCategories, - private val downloadManager: DownloadManager, + private val categoryItemProvider: ICategoryItemProvider, ) { private val _categories = MutableStateFlow>(emptyList()) val categoriesFlow = _categories.asStateFlow() @@ -45,9 +44,8 @@ class CategoryManager( prepareCategory(it) } autoAddItemsToCategoriesBasedOnFileNames( - downloadManager - .getDownloadList() - .map { it.id to it.name } + categoryItemProvider + .getAll() ) } } @@ -80,6 +78,20 @@ class CategoryManager( } } + fun getCategoryOf(categoryItem: ICategoryItem): Category? { + val url = categoryItem.url + val fileName = categoryItem.fileName + return getCategories() + .filter { + it.acceptFileName(fileName) + }.sortedByDescending { + it.hasUrlPattern + }.firstOrNull { + it.acceptUrl(url) + } + + } + fun getCategoryOfItem(id: Long): Category? { return getCategories() .firstOrNull { @@ -119,7 +131,7 @@ class CategoryManager( prepareCategory(newCategory) } - fun createDirectoryIfNecessary(category: Category) { + private fun createDirectoryIfNecessary(category: Category) { kotlin.runCatching { val folder = File(category.path) if (folder.exists()) { @@ -169,15 +181,15 @@ class CategoryManager( } fun autoAddItemsToCategoriesBasedOnFileNames( - unCategorizedItems: List>, + unCategorizedItems: List, ) { val newItemsMap = mutableMapOf>() var count = 0 - for ((id, name) in unCategorizedItems) { - val categoryToUpdate = getCategoryOfFileName(name) ?: continue + for (item in unCategorizedItems) { + val categoryToUpdate = getCategoryOf(item) ?: continue newItemsMap .getOrPut(categoryToUpdate.id) { mutableListOf() } - .add(id) + .add(item.id) count++ } for ((categoryId, itemsToAdd) in newItemsMap) { diff --git a/shared/app-utils/src/main/kotlin/com/abdownloadmanager/utils/category/DefaultCategories.kt b/shared/app-utils/src/main/kotlin/com/abdownloadmanager/utils/category/DefaultCategories.kt index d894dad..4cee588 100644 --- a/shared/app-utils/src/main/kotlin/com/abdownloadmanager/utils/category/DefaultCategories.kt +++ b/shared/app-utils/src/main/kotlin/com/abdownloadmanager/utils/category/DefaultCategories.kt @@ -43,7 +43,6 @@ class DefaultCategories( "dmg", "tgz", ), - items = emptyList(), ) val programs = Category( @@ -63,7 +62,6 @@ class DefaultCategories( "rpm", "bin", ), - items = emptyList(), ) val videos = Category( id = 2, @@ -82,7 +80,6 @@ class DefaultCategories( "3gp", "mpeg", ), - items = emptyList(), ) val music = Category( @@ -100,7 +97,6 @@ class DefaultCategories( "wma", "m4a", ), - items = emptyList(), ) val pictures = Category( @@ -123,7 +119,6 @@ class DefaultCategories( "raw", "psd", ), - items = emptyList(), ) val documents = Category( id = 5, @@ -145,7 +140,6 @@ class DefaultCategories( "epub", "pages", ), - items = emptyList(), ) return listOf( compressed, diff --git a/shared/app-utils/src/main/kotlin/com/abdownloadmanager/utils/category/DownloadManagerCategoryItemProvider.kt b/shared/app-utils/src/main/kotlin/com/abdownloadmanager/utils/category/DownloadManagerCategoryItemProvider.kt new file mode 100644 index 0000000..5c4f663 --- /dev/null +++ b/shared/app-utils/src/main/kotlin/com/abdownloadmanager/utils/category/DownloadManagerCategoryItemProvider.kt @@ -0,0 +1,17 @@ +package com.abdownloadmanager.utils.category + +import ir.amirab.downloader.DownloadManager + +class DownloadManagerCategoryItemProvider( + private val dowManager: DownloadManager, +) : ICategoryItemProvider { + override suspend fun getAll(): List { + return dowManager.getDownloadList().map { + CategoryItemWithId( + id = it.id, + fileName = it.name, + url = it.link + ) + } + } +} \ No newline at end of file diff --git a/shared/app-utils/src/main/kotlin/com/abdownloadmanager/utils/category/ICategoryItemProvider.kt b/shared/app-utils/src/main/kotlin/com/abdownloadmanager/utils/category/ICategoryItemProvider.kt new file mode 100644 index 0000000..39c62de --- /dev/null +++ b/shared/app-utils/src/main/kotlin/com/abdownloadmanager/utils/category/ICategoryItemProvider.kt @@ -0,0 +1,5 @@ +package com.abdownloadmanager.utils.category + +interface ICategoryItemProvider { + suspend fun getAll(): List +} \ No newline at end of file