mirror of
https://github.com/amir1376/ab-download-manager.git
synced 2025-02-20 11:43:24 +08:00
Merge pull request #116 from amir1376/feature/custom-categories-add-url-pattern
add url patterns to category
This commit is contained in:
commit
981debc02e
@ -134,12 +134,15 @@ val downloadSystemModule = module {
|
||||
}
|
||||
)
|
||||
}
|
||||
single {
|
||||
DownloadManagerCategoryItemProvider(get())
|
||||
}.bind<ICategoryItemProvider>()
|
||||
single {
|
||||
CategoryManager(
|
||||
categoryStorage = get(),
|
||||
scope = get(),
|
||||
defaultCategoriesFactory = get(),
|
||||
downloadManager = get(),
|
||||
categoryItemProvider = get(),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
),
|
||||
|
@ -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 {
|
||||
|
@ -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!
|
||||
)
|
||||
)
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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? {
|
||||
|
@ -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
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package com.abdownloadmanager.utils.category
|
||||
|
||||
interface ICategoryItemProvider {
|
||||
suspend fun getAll(): List<CategoryItemWithId>
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user