Merge pull request #116 from amir1376/feature/custom-categories-add-url-pattern

add url patterns to category
This commit is contained in:
AmirHossein Abdolmotallebi 2024-10-17 13:27:09 +03:30 committed by GitHub
commit 981debc02e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 240 additions and 47 deletions

View File

@ -134,12 +134,15 @@ val downloadSystemModule = module {
}
)
}
single {
DownloadManagerCategoryItemProvider(get())
}.bind<ICategoryItemProvider>()
single {
CategoryManager(
categoryStorage = get(),
scope = get(),
defaultCategoriesFactory = get(),
downloadManager = get(),
categoryItemProvider = get(),
)
}

View File

@ -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
),

View File

@ -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 {

View File

@ -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!
)
)

View File

@ -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) {

View File

@ -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)
}
}

View File

@ -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,
)
}
)
}

View File

@ -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()

View File

@ -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()

View File

@ -18,11 +18,15 @@ import kotlinx.serialization.Serializable
data class Category(
val id: Long,
val name: String,
val path: String,
val acceptedFileTypes: List<String>,
val icon: String,
val items: List<Long>,
val path: String,
val acceptedFileTypes: List<String> = emptyList(),
// this is optional if nothing provided it means that every url is acceptable
val acceptedUrlPatterns: List<String> = emptyList(),
val items: List<Long> = 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? {

View File

@ -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

View File

@ -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<List<Category>>(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<Pair<Long, String>>,
unCategorizedItems: List<CategoryItemWithId>,
) {
val newItemsMap = mutableMapOf<Long, MutableList<Long>>()
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) {

View File

@ -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,

View File

@ -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<CategoryItemWithId> {
return dowManager.getDownloadList().map {
CategoryItemWithId(
id = it.id,
fileName = it.name,
url = it.link
)
}
}
}

View File

@ -0,0 +1,5 @@
package com.abdownloadmanager.utils.category
interface ICategoryItemProvider {
suspend fun getAll(): List<CategoryItemWithId>
}