mirror of
https://github.com/amir1376/ab-download-manager.git
synced 2025-02-20 11:43:24 +08:00
Merge pull request #107 from amir1376/feature/custom-categories
add custom categories
This commit is contained in:
commit
5613f6c2c0
@ -5,6 +5,8 @@ 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.category.CategoryComponent
|
||||
import com.abdownloadmanager.desktop.pages.category.CategoryDialogManager
|
||||
import com.abdownloadmanager.desktop.pages.home.HomeComponent
|
||||
import com.abdownloadmanager.desktop.pages.queue.QueuesComponent
|
||||
import com.abdownloadmanager.desktop.pages.settings.SettingsComponent
|
||||
@ -35,6 +37,8 @@ import ir.amirab.downloader.utils.ExceptionUtils
|
||||
import ir.amirab.downloader.utils.OnDuplicateStrategy
|
||||
import com.abdownloadmanager.integration.Integration
|
||||
import com.abdownloadmanager.integration.IntegrationResult
|
||||
import com.abdownloadmanager.utils.category.CategoryManager
|
||||
import com.abdownloadmanager.utils.category.CategorySelectionMode
|
||||
import ir.amirab.downloader.exception.TooManyErrorException
|
||||
import ir.amirab.util.osfileutil.FileUtils
|
||||
import kotlinx.coroutines.flow.*
|
||||
@ -61,6 +65,7 @@ class AppComponent(
|
||||
) : BaseComponent(ctx),
|
||||
DownloadDialogManager,
|
||||
AddDownloadDialogManager,
|
||||
CategoryDialogManager,
|
||||
NotificationSender,
|
||||
DownloadItemOpener,
|
||||
ContainsEffects<AppEffects> by supportEffects(),
|
||||
@ -101,7 +106,8 @@ class AppComponent(
|
||||
downloadItemOpener = this,
|
||||
downloadDialogManager = this,
|
||||
addDownloadDialogManager = this,
|
||||
notificationSender = this
|
||||
categoryDialogManager = this,
|
||||
notificationSender = this,
|
||||
)
|
||||
}
|
||||
).subscribeAsStateFlow()
|
||||
@ -192,12 +198,25 @@ class AppComponent(
|
||||
onRequestClose = {
|
||||
closeAddDownloadDialog(config.id)
|
||||
},
|
||||
onRequestAddToQueue = { item, queueId, onDuplicate ->
|
||||
addDownload(item = item, queueId = queueId, onDuplicateStrategy = onDuplicate)
|
||||
onRequestAddToQueue = { item, queueId, onDuplicate, categoryId ->
|
||||
addDownload(
|
||||
item = item,
|
||||
queueId = queueId,
|
||||
categoryId = categoryId,
|
||||
onDuplicateStrategy = onDuplicate,
|
||||
)
|
||||
closeAddDownloadDialog(dialogId = config.id)
|
||||
},
|
||||
onRequestDownload = { item, onDuplicate ->
|
||||
startNewDownload(item, onDuplicate, true)
|
||||
onRequestAddCategory = {
|
||||
openCategoryDialog(-1)
|
||||
},
|
||||
onRequestDownload = { item, onDuplicate, categoryId ->
|
||||
startNewDownload(
|
||||
item = item,
|
||||
onDuplicateStrategy = onDuplicate,
|
||||
openDownloadDialog = true,
|
||||
categoryId = categoryId,
|
||||
)
|
||||
closeAddDownloadDialog(config.id)
|
||||
},
|
||||
openExistingDownload = {
|
||||
@ -213,16 +232,20 @@ class AppComponent(
|
||||
|
||||
is AddDownloadConfig.MultipleAddConfig -> {
|
||||
AddMultiDownloadComponent(
|
||||
ctx,
|
||||
config.id,
|
||||
{ closeAddDownloadDialog(config.id) },
|
||||
{ items, strategy, queueId ->
|
||||
ctx = ctx,
|
||||
id = config.id,
|
||||
onRequestClose = { closeAddDownloadDialog(config.id) },
|
||||
onRequestAdd = { items, strategy, queueId, categorySelectionMode ->
|
||||
addDownload(
|
||||
items = items,
|
||||
onDuplicateStrategy = strategy,
|
||||
queueId = queueId,
|
||||
categorySelectionMode = categorySelectionMode
|
||||
)
|
||||
closeAddDownloadDialog(config.id)
|
||||
},
|
||||
onRequestAddCategory = {
|
||||
openCategoryDialog(-1)
|
||||
}
|
||||
).apply { addItems(config.links) }
|
||||
}
|
||||
@ -263,6 +286,77 @@ class AppComponent(
|
||||
.map { it.items.mapNotNull { it.instance } }
|
||||
.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
private val categoryManager: CategoryManager by inject()
|
||||
|
||||
private val categoryPageControl = PagesNavigation<Long>()
|
||||
private val _openedCategoryDialogs = childPages(
|
||||
key = "openedCategoryDialogs",
|
||||
source = categoryPageControl,
|
||||
serializer = null,
|
||||
initialPages = { Pages() },
|
||||
pageStatus = { _, _ ->
|
||||
ChildNavState.Status.RESUMED
|
||||
},
|
||||
childFactory = { cfg, ctx ->
|
||||
CategoryComponent(
|
||||
ctx = ctx,
|
||||
close = {
|
||||
closeCategoryDialog(cfg)
|
||||
},
|
||||
submit = { submittedCategory ->
|
||||
if (submittedCategory.id < 0) {
|
||||
categoryManager.addCustomCategory(submittedCategory)
|
||||
} else {
|
||||
categoryManager.updateCategory(
|
||||
submittedCategory.id
|
||||
) {
|
||||
submittedCategory.copy(
|
||||
items = it.items
|
||||
)
|
||||
}
|
||||
}
|
||||
closeCategoryDialog(cfg)
|
||||
},
|
||||
id = cfg
|
||||
)
|
||||
}
|
||||
).subscribeAsStateFlow()
|
||||
override val openedCategoryDialogs: StateFlow<List<CategoryComponent>> = _openedCategoryDialogs
|
||||
.map {
|
||||
it.items.mapNotNull { it.instance }
|
||||
}.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
override fun openCategoryDialog(categoryId: Long) {
|
||||
scope.launch {
|
||||
val component = openedCategoryDialogs.value.find {
|
||||
it.id == categoryId
|
||||
}
|
||||
if (component != null) {
|
||||
// component.bringToFront()
|
||||
} else {
|
||||
categoryPageControl.navigate {
|
||||
val newItems = (it.items.toSet() + categoryId).toList()
|
||||
val copy = it.copy(
|
||||
items = newItems,
|
||||
selectedIndex = newItems.lastIndex
|
||||
)
|
||||
copy
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun closeCategoryDialog(categoryId: Long) {
|
||||
scope.launch {
|
||||
categoryPageControl.navigate {
|
||||
val newItems = it.items.filter { config ->
|
||||
config != categoryId
|
||||
}
|
||||
it.copy(items = newItems, selectedIndex = newItems.lastIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
downloadSystem.downloadEvents
|
||||
.filterIsInstance<DownloadManagerEvents.OnJobRemoved>()
|
||||
@ -518,6 +612,7 @@ class AppComponent(
|
||||
fun addDownload(
|
||||
items: List<DownloadItem>,
|
||||
onDuplicateStrategy: (DownloadItem) -> OnDuplicateStrategy,
|
||||
categorySelectionMode: CategorySelectionMode?,
|
||||
queueId: Long?,
|
||||
) {
|
||||
scope.launch {
|
||||
@ -525,6 +620,7 @@ class AppComponent(
|
||||
newItemsToAdd = items,
|
||||
onDuplicateStrategy = onDuplicateStrategy,
|
||||
queueId = queueId,
|
||||
categorySelectionMode = categorySelectionMode,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -532,6 +628,7 @@ class AppComponent(
|
||||
fun addDownload(
|
||||
item: DownloadItem,
|
||||
queueId: Long?,
|
||||
categoryId: Long?,
|
||||
onDuplicateStrategy: OnDuplicateStrategy,
|
||||
) {
|
||||
scope.launch {
|
||||
@ -539,6 +636,7 @@ class AppComponent(
|
||||
downloadItem = item,
|
||||
onDuplicateStrategy = onDuplicateStrategy,
|
||||
queueId = queueId,
|
||||
categoryId = categoryId,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -547,12 +645,14 @@ class AppComponent(
|
||||
item: DownloadItem,
|
||||
onDuplicateStrategy: OnDuplicateStrategy,
|
||||
openDownloadDialog: Boolean,
|
||||
categoryId: Long?,
|
||||
) {
|
||||
scope.launch {
|
||||
val id = downloadSystem.addDownload(
|
||||
item,
|
||||
onDuplicateStrategy,
|
||||
DefaultQueueInfo.ID,
|
||||
downloadItem = item,
|
||||
onDuplicateStrategy = onDuplicateStrategy,
|
||||
queueId = DefaultQueueInfo.ID,
|
||||
categoryId = categoryId,
|
||||
)
|
||||
launch {
|
||||
downloadSystem.manualResume(id)
|
||||
|
@ -12,6 +12,7 @@ import ir.amirab.util.compose.action.buildMenu
|
||||
import ir.amirab.util.compose.action.simpleAction
|
||||
import com.abdownloadmanager.desktop.utils.getIcon
|
||||
import com.abdownloadmanager.desktop.utils.getName
|
||||
import com.abdownloadmanager.utils.category.Category
|
||||
import ir.amirab.downloader.downloaditem.DownloadCredentials
|
||||
import ir.amirab.downloader.queue.DownloadQueue
|
||||
import ir.amirab.downloader.queue.activeQueuesFlow
|
||||
@ -207,6 +208,21 @@ fun moveToQueueAction(
|
||||
}
|
||||
}
|
||||
}
|
||||
fun createMoveToCategoryAction(
|
||||
category: Category,
|
||||
itemIds: List<Long>,
|
||||
): AnAction {
|
||||
return simpleAction(category.name) {
|
||||
scope.launch {
|
||||
downloadSystem
|
||||
.categoryManager
|
||||
.addItemsToCategory(
|
||||
categoryId = category.id,
|
||||
itemIds = itemIds,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopQueueAction(
|
||||
queue: DownloadQueue,
|
||||
|
@ -8,6 +8,7 @@ import com.abdownloadmanager.desktop.pages.settings.ThemeManager
|
||||
import ir.amirab.downloader.queue.QueueManager
|
||||
import com.abdownloadmanager.desktop.repository.AppRepository
|
||||
import com.abdownloadmanager.desktop.storage.*
|
||||
import com.abdownloadmanager.desktop.ui.icon.MyIcons
|
||||
import com.abdownloadmanager.desktop.utils.*
|
||||
import com.abdownloadmanager.desktop.utils.native_messaging.NativeMessaging
|
||||
import com.abdownloadmanager.desktop.utils.native_messaging.NativeMessagingManifestApplier
|
||||
@ -34,6 +35,10 @@ import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
import com.abdownloadmanager.updatechecker.DummyUpdateChecker
|
||||
import com.abdownloadmanager.updatechecker.UpdateChecker
|
||||
import com.abdownloadmanager.utils.FileIconProvider
|
||||
import com.abdownloadmanager.utils.FileIconProviderUsingCategoryIcons
|
||||
import com.abdownloadmanager.utils.category.*
|
||||
import com.abdownloadmanager.utils.compose.IMyIcons
|
||||
import ir.amirab.downloader.monitor.IDownloadMonitor
|
||||
import ir.amirab.downloader.utils.EmptyFileCreator
|
||||
|
||||
@ -92,7 +97,7 @@ val downloaderModule = module {
|
||||
|
||||
}
|
||||
single {
|
||||
val downloadSettings:DownloadSettings = get()
|
||||
val downloadSettings: DownloadSettings = get()
|
||||
EmptyFileCreator(
|
||||
diskStat = get(),
|
||||
useSparseFile = { downloadSettings.useSparseFileAllocation }
|
||||
@ -104,8 +109,42 @@ val downloaderModule = module {
|
||||
single<IDownloadMonitor> {
|
||||
DownloadMonitor(get())
|
||||
}
|
||||
}
|
||||
val downloadSystemModule = module {
|
||||
single {
|
||||
DownloadSystem(get(), get(), get(), get(), get(), get())
|
||||
CategoryFileStorage(
|
||||
file = get<DownloadFoldersRegistry>().registerAndGet(
|
||||
AppInfo.downloadDbDir.resolve("categories")
|
||||
).resolve("categories.json"),
|
||||
fileSaver = get()
|
||||
)
|
||||
}.bind<CategoryStorage>()
|
||||
single {
|
||||
FileIconProviderUsingCategoryIcons(
|
||||
get(),
|
||||
get(),
|
||||
MyIcons,
|
||||
)
|
||||
}.bind<FileIconProvider>()
|
||||
single {
|
||||
DefaultCategories(
|
||||
icons = get(),
|
||||
getDefaultDownloadFolder = {
|
||||
get<AppSettingsStorage>().defaultDownloadFolder.value
|
||||
}
|
||||
)
|
||||
}
|
||||
single {
|
||||
CategoryManager(
|
||||
categoryStorage = get(),
|
||||
scope = get(),
|
||||
defaultCategoriesFactory = get(),
|
||||
downloadManager = get(),
|
||||
)
|
||||
}
|
||||
|
||||
single {
|
||||
DownloadSystem(get(), get(), get(), get(), get(), get(), get())
|
||||
}
|
||||
}
|
||||
val coroutineModule = module {
|
||||
@ -154,6 +193,7 @@ val nativeMessagingModule = module {
|
||||
|
||||
val appModule = module {
|
||||
includes(downloaderModule)
|
||||
includes(downloadSystemModule)
|
||||
includes(coroutineModule)
|
||||
includes(jsonModule)
|
||||
includes(integrationModule)
|
||||
@ -167,8 +207,11 @@ val appModule = module {
|
||||
AppRepository()
|
||||
}
|
||||
single {
|
||||
ThemeManager(get(),get())
|
||||
ThemeManager(get(), get())
|
||||
}
|
||||
single {
|
||||
MyIcons
|
||||
}.bind<IMyIcons>()
|
||||
single {
|
||||
AppSettingsStorage(
|
||||
createMyConfigPreferences(
|
||||
|
@ -31,7 +31,7 @@ fun ShowAddDownloadDialogs(component: AddDownloadDialogManager) {
|
||||
}
|
||||
when (addDownloadComponent) {
|
||||
is AddSingleDownloadComponent -> {
|
||||
val h = 220
|
||||
val h = 265
|
||||
val w = 500
|
||||
val size = remember {
|
||||
DpSize(
|
||||
@ -61,7 +61,7 @@ fun ShowAddDownloadDialogs(component: AddDownloadDialogManager) {
|
||||
|
||||
is AddMultiDownloadComponent -> {
|
||||
val h = 400
|
||||
val w = 600
|
||||
val w = 700
|
||||
val state = rememberWindowState(
|
||||
height = h.dp,
|
||||
width = w.dp,
|
||||
|
@ -9,6 +9,11 @@ import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.abdownloadmanager.desktop.pages.addDownload.multiple.AddMultiItemSaveMode.*
|
||||
import com.abdownloadmanager.desktop.utils.asState
|
||||
import com.abdownloadmanager.utils.category.Category
|
||||
import com.abdownloadmanager.utils.category.CategoryManager
|
||||
import com.abdownloadmanager.utils.category.CategorySelectionMode
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import ir.amirab.downloader.connection.DownloaderClient
|
||||
import ir.amirab.downloader.downloaditem.DownloadCredentials
|
||||
@ -25,10 +30,11 @@ class AddMultiDownloadComponent(
|
||||
id: String,
|
||||
private val onRequestClose: () -> Unit,
|
||||
private val onRequestAdd: OnRequestAdd,
|
||||
private val onRequestAddCategory: () -> Unit,
|
||||
) : AddDownloadComponent(ctx, id),
|
||||
KoinComponent {
|
||||
|
||||
val tableState= TableState(
|
||||
val tableState = TableState(
|
||||
cells = AddMultiItemTableCells.all(),
|
||||
forceVisibleCells = listOf(
|
||||
AddMultiItemTableCells.Check,
|
||||
@ -38,15 +44,38 @@ class AddMultiDownloadComponent(
|
||||
private val appSettings by inject<AppRepository>()
|
||||
private val client by inject<DownloaderClient>()
|
||||
val downloadSystem by inject<DownloadSystem>()
|
||||
private val _folder=MutableStateFlow(appSettings.saveLocation.value)
|
||||
private val _folder = MutableStateFlow(appSettings.saveLocation.value)
|
||||
val folder = _folder.asStateFlow()
|
||||
fun setFolder(folder:String) {
|
||||
fun setFolder(folder: String) {
|
||||
this._folder.update { folder }
|
||||
list.forEach {
|
||||
it.folder.update { folder }
|
||||
}
|
||||
}
|
||||
|
||||
// when we select all files in one location let user option to auto categorize items
|
||||
private val _alsoAutoCategorize = MutableStateFlow(true)
|
||||
val alsoAutoCategorize = _alsoAutoCategorize.asStateFlow()
|
||||
fun setAlsoAutoCategorize(value: Boolean) {
|
||||
_alsoAutoCategorize.update { value }
|
||||
}
|
||||
|
||||
|
||||
private val categoryManager: CategoryManager by inject()
|
||||
val categories = categoryManager.categoriesFlow
|
||||
private val _selectedCategory = MutableStateFlow(categories.value.firstOrNull())
|
||||
val selectedCategory = _selectedCategory.asStateFlow()
|
||||
|
||||
fun setSelectedCategory(category: Category) {
|
||||
_selectedCategory.update {
|
||||
category
|
||||
}
|
||||
}
|
||||
|
||||
fun requestAddCategory() {
|
||||
onRequestAddCategory()
|
||||
}
|
||||
|
||||
private fun newChecker(iDownloadCredentials: DownloadCredentials) = DownloadUiChecker(
|
||||
initialCredentials = iDownloadCredentials,
|
||||
initialName = "",
|
||||
@ -69,6 +98,12 @@ class AddMultiDownloadComponent(
|
||||
}
|
||||
|
||||
var list: List<DownloadUiChecker> by mutableStateOf(emptyList())
|
||||
private val _saveMode = MutableStateFlow(EachFileInTheirOwnCategory)
|
||||
val saveMode = _saveMode.asStateFlow()
|
||||
fun setSaveMode(saveMode: AddMultiItemSaveMode) {
|
||||
_saveMode.update { saveMode }
|
||||
}
|
||||
|
||||
|
||||
private val checkList = MutableSharedFlow<DownloadUiChecker>()
|
||||
private fun enqueueCheck(links: List<DownloadUiChecker>) {
|
||||
@ -121,15 +156,59 @@ class AddMultiDownloadComponent(
|
||||
}
|
||||
}
|
||||
|
||||
val isCategoryModeHasValidState by run {
|
||||
val category by selectedCategory.asState(scope)
|
||||
val saveMode by saveMode.asState(scope)
|
||||
derivedStateOf {
|
||||
when (saveMode) {
|
||||
EachFileInTheirOwnCategory -> true
|
||||
AllInOneCategory -> category != null
|
||||
InSameLocation -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
val canClickAdd by derivedStateOf {
|
||||
selectionList.isNotEmpty()
|
||||
selectionList.isNotEmpty() && isCategoryModeHasValidState
|
||||
}
|
||||
private val queueManager: QueueManager by inject()
|
||||
val queueList = queueManager.queues
|
||||
|
||||
private fun getFolderForItem(
|
||||
categorySelectionMode: CategorySelectionMode?,
|
||||
fleName: String,
|
||||
defaultFolder: String,
|
||||
): String {
|
||||
return when (categorySelectionMode) {
|
||||
CategorySelectionMode.Auto -> {
|
||||
downloadSystem.categoryManager
|
||||
.getCategoryOfFileName(fleName)?.path
|
||||
?: defaultFolder
|
||||
}
|
||||
|
||||
is CategorySelectionMode.Fixed -> {
|
||||
downloadSystem.categoryManager
|
||||
.getCategoryById(categorySelectionMode.categoryId)?.path
|
||||
?: defaultFolder
|
||||
}
|
||||
|
||||
null -> defaultFolder
|
||||
}
|
||||
}
|
||||
|
||||
fun requestAddDownloads(
|
||||
queueId: Long?
|
||||
queueId: Long?,
|
||||
) {
|
||||
val categorySelectionMode = when (saveMode.value) {
|
||||
EachFileInTheirOwnCategory -> CategorySelectionMode.Auto
|
||||
AllInOneCategory -> selectedCategory.value?.let {
|
||||
CategorySelectionMode.Fixed(it.id)
|
||||
}
|
||||
|
||||
InSameLocation -> {
|
||||
if (alsoAutoCategorize.value) CategorySelectionMode.Auto
|
||||
else null
|
||||
}
|
||||
}
|
||||
val itemsToAdd = list
|
||||
.filter { it.credentials.value.link in selectionList }
|
||||
.filter {
|
||||
@ -139,7 +218,11 @@ class AddMultiDownloadComponent(
|
||||
.map {
|
||||
DownloadItem(
|
||||
id = -1,
|
||||
folder = it.folder.value,
|
||||
folder = getFolderForItem(
|
||||
categorySelectionMode = categorySelectionMode,
|
||||
fleName = it.name.value,
|
||||
defaultFolder = it.folder.value
|
||||
),
|
||||
name = it.name.value,
|
||||
link = it.credentials.value.link,
|
||||
contentLength = it.length.value ?: -1,
|
||||
@ -149,9 +232,13 @@ class AddMultiDownloadComponent(
|
||||
onRequestAdd(
|
||||
items = itemsToAdd,
|
||||
onDuplicateStrategy = { OnDuplicateStrategy.AddNumbered },
|
||||
queueId = queueId
|
||||
queueId = queueId,
|
||||
categorySelectionMode = categorySelectionMode
|
||||
)
|
||||
addToLastUsedLocations(folder.value)
|
||||
val folder = folder.value
|
||||
if (saveMode.value == InSameLocation) {
|
||||
addToLastUsedLocations(folder)
|
||||
}
|
||||
requestClose()
|
||||
}
|
||||
}
|
||||
@ -176,6 +263,7 @@ fun interface OnRequestAdd {
|
||||
operator fun invoke(
|
||||
items: List<DownloadItem>,
|
||||
onDuplicateStrategy: (DownloadItem) -> OnDuplicateStrategy,
|
||||
queueId: Long?
|
||||
queueId: Long?,
|
||||
categorySelectionMode: CategorySelectionMode?,
|
||||
)
|
||||
}
|
@ -1,40 +1,72 @@
|
||||
package com.abdownloadmanager.desktop.pages.addDownload.multiple
|
||||
|
||||
import com.abdownloadmanager.desktop.pages.addDownload.shared.ShowAddToQueueDialog
|
||||
import com.abdownloadmanager.desktop.pages.addDownload.shared.LocationTextField
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import com.abdownloadmanager.desktop.ui.theme.myTextSizes
|
||||
import com.abdownloadmanager.desktop.ui.widget.ActionButton
|
||||
import com.abdownloadmanager.desktop.ui.widget.Text
|
||||
import com.abdownloadmanager.utils.compose.WithContentAlpha
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.foundation.onClick
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.window.WindowDraggableArea
|
||||
import androidx.compose.runtime.*
|
||||
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.graphics.Brush
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.rememberDialogState
|
||||
import com.abdownloadmanager.desktop.pages.addDownload.shared.*
|
||||
import com.abdownloadmanager.desktop.ui.customwindow.BaseOptionDialog
|
||||
import com.abdownloadmanager.desktop.ui.icon.MyIcons
|
||||
import com.abdownloadmanager.desktop.ui.theme.myColors
|
||||
import com.abdownloadmanager.desktop.ui.util.ifThen
|
||||
import com.abdownloadmanager.desktop.ui.widget.CheckBox
|
||||
import com.abdownloadmanager.desktop.ui.widget.Help
|
||||
import com.abdownloadmanager.desktop.utils.div
|
||||
import com.abdownloadmanager.desktop.utils.windowUtil.moveSafe
|
||||
import com.abdownloadmanager.utils.category.CategorySelectionMode
|
||||
import com.abdownloadmanager.utils.compose.WithContentColor
|
||||
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
||||
import java.awt.MouseInfo
|
||||
|
||||
@Composable
|
||||
fun AddMultiItemPage(
|
||||
addMultiDownloadComponent: AddMultiDownloadComponent,
|
||||
) {
|
||||
Column(Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 8.dp,bottom = 16.dp)
|
||||
) {
|
||||
WithContentAlpha(1f) {
|
||||
Text(
|
||||
"Select Items you want to pick up for download",
|
||||
fontSize = myTextSizes.base
|
||||
Column(Modifier) {
|
||||
Column(
|
||||
Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 8.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
WithContentAlpha(1f) {
|
||||
Text(
|
||||
"Select Items you want to pick up for download",
|
||||
fontSize = myTextSizes.base
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
AddMultiDownloadTable(
|
||||
Modifier.weight(1f),
|
||||
addMultiDownloadComponent,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
AddMultiDownloadTable(
|
||||
Modifier.weight(1f),
|
||||
Footer(
|
||||
Modifier,
|
||||
addMultiDownloadComponent,
|
||||
)
|
||||
Footer(addMultiDownloadComponent)
|
||||
}
|
||||
if (addMultiDownloadComponent.showAddToQueue){
|
||||
if (addMultiDownloadComponent.showAddToQueue) {
|
||||
ShowAddToQueueDialog(
|
||||
queueList = addMultiDownloadComponent.queueList.collectAsState().value,
|
||||
onQueueSelected = {
|
||||
@ -50,35 +82,376 @@ fun AddMultiItemPage(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Footer(component: AddMultiDownloadComponent) {
|
||||
Row(
|
||||
Modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
fun Footer(
|
||||
modifier: Modifier = Modifier,
|
||||
component: AddMultiDownloadComponent,
|
||||
) {
|
||||
Column(modifier) {
|
||||
Spacer(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(1.dp)
|
||||
.background(myColors.onBackground / 0.15f)
|
||||
)
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(myColors.surface / 0.5f)
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(vertical = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
SaveSettings(
|
||||
modifier = Modifier.width(300.dp),
|
||||
component = component,
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Row(Modifier.align(Alignment.Bottom)) {
|
||||
ActionButton(
|
||||
text = "Add",
|
||||
onClick = {
|
||||
component.openAddToQueueDialog()
|
||||
},
|
||||
enabled = component.canClickAdd,
|
||||
modifier = Modifier,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
ActionButton(
|
||||
text = "Cancel",
|
||||
onClick = {
|
||||
component.requestClose()
|
||||
},
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SaveSettings(
|
||||
modifier: Modifier,
|
||||
component: AddMultiDownloadComponent,
|
||||
) {
|
||||
Column(
|
||||
modifier.animateContentSize()
|
||||
) {
|
||||
ActionButton(
|
||||
text = "Add",
|
||||
onClick = {
|
||||
component.openAddToQueueDialog()
|
||||
var dropdownOpen by remember { mutableStateOf(false) }
|
||||
val saveMode by component.saveMode.collectAsState()
|
||||
Text("Save to:")
|
||||
Spacer(Modifier.height(8.dp))
|
||||
SaveSolution(
|
||||
saveMode = saveMode,
|
||||
setSaveMode = {
|
||||
component.setSaveMode(it)
|
||||
},
|
||||
enabled = component.canClickAdd,
|
||||
modifier = Modifier,
|
||||
isSelectionOpen = dropdownOpen,
|
||||
setSelectionOpen = {
|
||||
dropdownOpen = it
|
||||
}
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
LocationTextField(
|
||||
text = component.folder.collectAsState().value,
|
||||
setText = {
|
||||
component.setFolder(it)
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
lastUsedLocations = component.lastUsedLocations.collectAsState().value
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
ActionButton(
|
||||
text = "Cancel",
|
||||
onClick = {
|
||||
component.requestClose()
|
||||
},
|
||||
modifier = Modifier,
|
||||
when (saveMode) {
|
||||
AddMultiItemSaveMode.EachFileInTheirOwnCategory -> {
|
||||
//
|
||||
}
|
||||
|
||||
AddMultiItemSaveMode.AllInOneCategory -> {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row(
|
||||
Modifier.height(IntrinsicSize.Max),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CategorySelect(
|
||||
categories = component.categories.collectAsState().value,
|
||||
modifier = Modifier.weight(1f),
|
||||
selectedCategory = component.selectedCategory.collectAsState().value,
|
||||
onCategorySelected = {
|
||||
component.setSelectedCategory(it)
|
||||
}
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
CategoryAddButton(
|
||||
Modifier.fillMaxHeight(),
|
||||
enabled = true,
|
||||
onClick = {
|
||||
component.requestAddCategory()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AddMultiItemSaveMode.InSameLocation -> {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
AllFilesInSameDirectory(
|
||||
Modifier,
|
||||
folder = component.folder.collectAsState().value,
|
||||
setFolder = {
|
||||
component.setFolder(it)
|
||||
},
|
||||
alsoCategorize = component.alsoAutoCategorize.collectAsState().value,
|
||||
setAlsoCategorize = component::setAlsoAutoCategorize,
|
||||
knownLocations = component.lastUsedLocations.collectAsState().value,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SaveSolution(
|
||||
saveMode: AddMultiItemSaveMode,
|
||||
setSaveMode: (AddMultiItemSaveMode) -> Unit,
|
||||
isSelectionOpen: Boolean,
|
||||
setSelectionOpen: (Boolean) -> Unit,
|
||||
) {
|
||||
SaveSolutionHeader(
|
||||
saveMode = saveMode,
|
||||
onClick = {
|
||||
setSelectionOpen(!isSelectionOpen)
|
||||
},
|
||||
)
|
||||
if (isSelectionOpen) {
|
||||
SaveSolutionPopup(
|
||||
selectedItem = saveMode,
|
||||
onIteSelected = setSaveMode,
|
||||
onDismiss = {
|
||||
setSelectionOpen(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SaveSolutionPopup(
|
||||
selectedItem: AddMultiItemSaveMode,
|
||||
onIteSelected: (AddMultiItemSaveMode) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val state = rememberDialogState(
|
||||
size = DpSize(
|
||||
height = Dp.Unspecified,
|
||||
width = Dp.Unspecified,
|
||||
),
|
||||
)
|
||||
val close = {
|
||||
onDismiss()
|
||||
}
|
||||
BaseOptionDialog(
|
||||
onCloseRequest = close,
|
||||
state = state,
|
||||
resizeable = false,
|
||||
) {
|
||||
LaunchedEffect(window) {
|
||||
window.moveSafe(
|
||||
MouseInfo.getPointerInfo().location.run {
|
||||
DpOffset(
|
||||
x = x.dp,
|
||||
y = y.dp
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
val shape = RoundedCornerShape(6.dp)
|
||||
Column(
|
||||
Modifier
|
||||
.clip(shape)
|
||||
.border(2.dp, myColors.onBackground / 10, shape)
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
myColors.surface,
|
||||
myColors.background,
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
WithContentColor(myColors.onBackground) {
|
||||
Column(
|
||||
Modifier.widthIn(max = 300.dp)
|
||||
) {
|
||||
WindowDraggableArea(Modifier) {
|
||||
Column(
|
||||
Modifier.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
"Where should each item saved?",
|
||||
Modifier,
|
||||
fontSize = myTextSizes.base
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
WithContentAlpha(0.75f) {
|
||||
Text(
|
||||
"There are multiple items! please select a way you want to save them",
|
||||
Modifier,
|
||||
fontSize = myTextSizes.sm,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Column(
|
||||
Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.padding(bottom = 8.dp)
|
||||
) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Spacer(
|
||||
Modifier.fillMaxWidth()
|
||||
.height(1.dp)
|
||||
.background(myColors.onBackground / 10),
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Column {
|
||||
for (item in AddMultiItemSaveMode.entries) {
|
||||
SaveSolutionItem(
|
||||
title = item.title,
|
||||
description = item.description,
|
||||
isSelected = selectedItem == item,
|
||||
onClick = {
|
||||
onIteSelected(item)
|
||||
close()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SaveSolutionHeader(
|
||||
saveMode: AddMultiItemSaveMode,
|
||||
onClick: () -> Unit,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
val borderColor = myColors.onBackground / 0.1f
|
||||
val background = myColors.surface / 50
|
||||
val shape = RoundedCornerShape(6.dp)
|
||||
Row(
|
||||
Modifier
|
||||
.height(IntrinsicSize.Max)
|
||||
.clip(shape)
|
||||
.ifThen(!enabled) {
|
||||
alpha(0.5f)
|
||||
}
|
||||
.border(1.dp, borderColor, shape)
|
||||
.background(background)
|
||||
.clickable(
|
||||
enabled = enabled
|
||||
) { onClick() }
|
||||
.padding(horizontal = 8.dp)
|
||||
) {
|
||||
val contentModifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.weight(1f)
|
||||
Text(
|
||||
saveMode.title,
|
||||
contentModifier,
|
||||
)
|
||||
Spacer(
|
||||
Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.fillMaxHeight().padding(vertical = 1.dp)
|
||||
.width(1.dp)
|
||||
.background(borderColor)
|
||||
)
|
||||
MyIcon(
|
||||
MyIcons.down,
|
||||
null,
|
||||
Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.size(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SaveSolutionItem(
|
||||
title: String,
|
||||
description: String,
|
||||
isSelected: Boolean?,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(8.dp)
|
||||
) {
|
||||
isSelected?.let {
|
||||
CheckBox(isSelected, { onClick() }, size = 12.dp)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Column {
|
||||
Text(
|
||||
title,
|
||||
fontSize = myTextSizes.base,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
WithContentAlpha(0.7f) {
|
||||
Text(
|
||||
text = description,
|
||||
fontSize = myTextSizes.sm,
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun AllFilesInSameDirectory(
|
||||
modifier: Modifier,
|
||||
folder: String,
|
||||
setFolder: (String) -> Unit,
|
||||
alsoCategorize: Boolean,
|
||||
setAlsoCategorize: (Boolean) -> Unit,
|
||||
knownLocations: List<String>,
|
||||
) {
|
||||
LocationTextField(
|
||||
text = folder,
|
||||
setText = {
|
||||
setFolder(it)
|
||||
},
|
||||
modifier = modifier,
|
||||
lastUsedLocations = knownLocations
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row(
|
||||
Modifier.onClick {
|
||||
setAlsoCategorize(!alsoCategorize)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CheckBox(
|
||||
value = alsoCategorize,
|
||||
onValueChange = setAlsoCategorize
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Auto categorize")
|
||||
}
|
||||
}
|
||||
|
||||
enum class AddMultiItemSaveMode(
|
||||
val title: String,
|
||||
val description: String,
|
||||
) {
|
||||
EachFileInTheirOwnCategory(
|
||||
title = "Each item on its own category",
|
||||
description = "Each item will be placed in a category that have that file type",
|
||||
),
|
||||
AllInOneCategory(
|
||||
title = "All items in one Category",
|
||||
description = "All files will be saved in the selected category location",
|
||||
),
|
||||
InSameLocation(
|
||||
title = "All items in one Location",
|
||||
description = "All items will be saved in the selected directory",
|
||||
);
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
package com.abdownloadmanager.desktop.pages.addDownload.shared
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.*
|
||||
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.unit.*
|
||||
import com.abdownloadmanager.desktop.ui.icon.MyIcons
|
||||
import com.abdownloadmanager.desktop.ui.theme.myColors
|
||||
import com.abdownloadmanager.desktop.ui.util.ifThen
|
||||
import com.abdownloadmanager.desktop.ui.widget.Text
|
||||
import com.abdownloadmanager.desktop.utils.div
|
||||
import com.abdownloadmanager.utils.category.Category
|
||||
import com.abdownloadmanager.utils.category.rememberIconPainter
|
||||
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
||||
|
||||
@Composable
|
||||
fun CategorySelect(
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
categories: List<Category>,
|
||||
selectedCategory: Category?,
|
||||
onCategorySelected: (Category) -> Unit,
|
||||
) {
|
||||
var isSelectionOpen by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val closeDialog = {
|
||||
isSelectionOpen = false
|
||||
}
|
||||
DialogDropDown(
|
||||
selectedItem = selectedCategory,
|
||||
possibleItems = categories,
|
||||
onItemSelected = onCategorySelected,
|
||||
enabled = enabled,
|
||||
renderItem = {
|
||||
RenderCategory(
|
||||
category = it,
|
||||
modifier = Modifier,
|
||||
)
|
||||
},
|
||||
dropdownOpen = isSelectionOpen,
|
||||
onRequestCloseDropDown = {
|
||||
closeDialog()
|
||||
},
|
||||
onRequestOpenDropDown = {
|
||||
isSelectionOpen = true
|
||||
},
|
||||
modifier = modifier,
|
||||
dropDownSize = DpSize(220.dp, 220.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderCategory(
|
||||
modifier: Modifier,
|
||||
category: Category,
|
||||
) {
|
||||
Row(
|
||||
modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val icon = category.rememberIconPainter()
|
||||
val iconModifier = Modifier.size(16.dp)
|
||||
if (icon != null) {
|
||||
MyIcon(
|
||||
icon,
|
||||
null,
|
||||
iconModifier,
|
||||
)
|
||||
} else {
|
||||
Spacer(iconModifier)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
category.name,
|
||||
softWrap = false,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CategoryAddButton(
|
||||
modifier: Modifier,
|
||||
enabled: Boolean = true,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val borderColor = myColors.onBackground / 0.1f
|
||||
val background = myColors.surface / 50
|
||||
val shape = RoundedCornerShape(6.dp)
|
||||
Box(
|
||||
modifier
|
||||
.clip(shape)
|
||||
.ifThen(!enabled) {
|
||||
alpha(0.5f)
|
||||
}
|
||||
.border(1.dp, borderColor, shape)
|
||||
.background(background)
|
||||
.clickable(
|
||||
enabled = enabled
|
||||
) { onClick() }
|
||||
.aspectRatio(1f)
|
||||
// .padding(horizontal = 8.dp)
|
||||
) {
|
||||
MyIcon(
|
||||
MyIcons.add,
|
||||
contentDescription = "Add Category",
|
||||
Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,230 @@
|
||||
package com.abdownloadmanager.desktop.pages.addDownload.shared
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.*
|
||||
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.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.rememberDialogState
|
||||
import com.abdownloadmanager.desktop.ui.customwindow.BaseOptionDialog
|
||||
import com.abdownloadmanager.desktop.ui.icon.MyIcons
|
||||
import com.abdownloadmanager.desktop.ui.theme.myColors
|
||||
import com.abdownloadmanager.desktop.ui.util.ifThen
|
||||
import com.abdownloadmanager.desktop.ui.widget.Text
|
||||
import com.abdownloadmanager.desktop.utils.div
|
||||
import com.abdownloadmanager.desktop.utils.windowUtil.moveSafe
|
||||
import com.abdownloadmanager.utils.compose.WithContentAlpha
|
||||
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
||||
import java.awt.MouseInfo
|
||||
|
||||
@Composable
|
||||
fun <T> DialogDropDown(
|
||||
selectedItem: T?,
|
||||
possibleItems: List<T>,
|
||||
onItemSelected: (T) -> Unit,
|
||||
modifier: Modifier,
|
||||
enabled: Boolean = true,
|
||||
dropdownOpen: Boolean,
|
||||
onRequestOpenDropDown: () -> Unit,
|
||||
onRequestCloseDropDown: () -> Unit,
|
||||
dropDownSize: DpSize = DpSize(220.dp, 250.dp),
|
||||
renderItem: @Composable (T) -> Unit,
|
||||
) {
|
||||
Column(modifier) {
|
||||
DropDownHeader(
|
||||
item = selectedItem,
|
||||
enabled = enabled,
|
||||
onClick = onRequestOpenDropDown,
|
||||
renderItem = renderItem
|
||||
)
|
||||
if (dropdownOpen) {
|
||||
DropDownContent(
|
||||
closeDialog = onRequestCloseDropDown,
|
||||
dropDownSize = dropDownSize,
|
||||
possibleItems = possibleItems,
|
||||
selectedItem = selectedItem,
|
||||
onItemSelected = onItemSelected,
|
||||
renderItem = renderItem,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> DropDownContent(
|
||||
closeDialog: () -> Unit,
|
||||
dropDownSize: DpSize,
|
||||
possibleItems: List<T>,
|
||||
selectedItem: T?,
|
||||
onItemSelected: (T) -> Unit,
|
||||
renderItem: @Composable (T) -> Unit,
|
||||
) {
|
||||
BaseOptionDialog(
|
||||
onCloseRequest = closeDialog,
|
||||
state = rememberDialogState(
|
||||
size = dropDownSize
|
||||
),
|
||||
resizeable = true,
|
||||
content = {
|
||||
LaunchedEffect(window) {
|
||||
window.moveSafe(
|
||||
MouseInfo.getPointerInfo().location.run {
|
||||
DpOffset(
|
||||
x = x.dp,
|
||||
y = y.dp
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
val shape = RoundedCornerShape(6.dp)
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.clip(shape)
|
||||
.border(2.dp, myColors.onBackground / 10, shape)
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
myColors.surface,
|
||||
myColors.background,
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
LazyColumn(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp),
|
||||
state = listState,
|
||||
) {
|
||||
items(possibleItems) {
|
||||
val isSelected = it == selectedItem
|
||||
WithContentAlpha(
|
||||
if (isSelected) 1f else 0.75f
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.clip(shape)
|
||||
.clickable {
|
||||
onItemSelected(it)
|
||||
closeDialog()
|
||||
}
|
||||
.padding(
|
||||
vertical = 8.dp,
|
||||
horizontal = 8.dp
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
Modifier.weight(1f)
|
||||
) {
|
||||
renderItem(it)
|
||||
}
|
||||
val selectedIconModifier = Modifier.size(16.dp)
|
||||
if (isSelected) {
|
||||
MyIcon(
|
||||
MyIcons.check,
|
||||
null,
|
||||
selectedIconModifier,
|
||||
)
|
||||
} else {
|
||||
Spacer(selectedIconModifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = listState.canScrollForward,
|
||||
modifier = Modifier.matchParentSize(),
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Spacer(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colorStops = arrayOf(
|
||||
0f to Color.Transparent,
|
||||
0.8f to Color.Transparent,
|
||||
1f to myColors.background,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T> DropDownHeader(
|
||||
item: T?,
|
||||
enabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
renderItem: @Composable (T) -> Unit,
|
||||
) {
|
||||
val borderColor = myColors.onBackground / 0.1f
|
||||
val background = myColors.surface / 50
|
||||
val shape = RoundedCornerShape(6.dp)
|
||||
Row(
|
||||
Modifier
|
||||
.height(IntrinsicSize.Max)
|
||||
.clip(shape)
|
||||
.ifThen(!enabled) {
|
||||
alpha(0.5f)
|
||||
}
|
||||
.border(1.dp, borderColor, shape)
|
||||
.background(background)
|
||||
.clickable(
|
||||
enabled = enabled
|
||||
) { onClick() }
|
||||
.padding(horizontal = 8.dp)
|
||||
) {
|
||||
val contentModifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.weight(1f)
|
||||
if (item != null) {
|
||||
Box(contentModifier) {
|
||||
renderItem(item)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
"No Category Selected",
|
||||
contentModifier
|
||||
)
|
||||
}
|
||||
Spacer(
|
||||
Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.fillMaxHeight().padding(vertical = 1.dp)
|
||||
.width(1.dp)
|
||||
.background(borderColor)
|
||||
)
|
||||
MyIcon(
|
||||
MyIcons.down,
|
||||
null,
|
||||
Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.size(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
@ -1,9 +1,5 @@
|
||||
package com.abdownloadmanager.desktop.pages.addDownload.single
|
||||
|
||||
import com.abdownloadmanager.desktop.pages.addDownload.shared.ExtraConfig
|
||||
import com.abdownloadmanager.desktop.pages.addDownload.shared.LocationTextField
|
||||
import com.abdownloadmanager.desktop.pages.addDownload.shared.ShowAddToQueueDialog
|
||||
import com.abdownloadmanager.desktop.pages.home.sections.category.DefinedTypeCategories
|
||||
import com.abdownloadmanager.utils.compose.WithContentAlpha
|
||||
import com.abdownloadmanager.utils.compose.WithContentColor
|
||||
import com.abdownloadmanager.desktop.ui.customwindow.BaseOptionDialog
|
||||
@ -34,8 +30,9 @@ import androidx.compose.ui.input.pointer.pointerHoverIcon
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.window.*
|
||||
import com.abdownloadmanager.desktop.pages.addDownload.shared.*
|
||||
import com.abdownloadmanager.desktop.utils.mvi.HandleEffects
|
||||
import ir.amirab.downloader.monitor.ProcessingDownloadItemState
|
||||
import com.abdownloadmanager.utils.category.rememberIconPainter
|
||||
import ir.amirab.downloader.utils.OnDuplicateStrategy
|
||||
import java.awt.MouseInfo
|
||||
|
||||
@ -79,6 +76,47 @@ fun AddDownloadPage(
|
||||
) {
|
||||
val canAddResult by component.canAddResult.collectAsState()
|
||||
Column(Modifier.weight(1f)) {
|
||||
val useCategory by component.useCategory.collectAsState()
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Row(
|
||||
modifier = Modifier.height(IntrinsicSize.Max),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.onClick {
|
||||
component.setUseCategory(!useCategory)
|
||||
}
|
||||
.padding(vertical = 4.dp)
|
||||
) {
|
||||
CheckBox(
|
||||
size = 16.dp,
|
||||
value = useCategory,
|
||||
onValueChange = { component.setUseCategory(it) }
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Use Category")
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
CategorySelect(
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = useCategory,
|
||||
categories = component.categories.collectAsState().value,
|
||||
selectedCategory = component.selectedCategory.collectAsState().value,
|
||||
onCategorySelected = {
|
||||
component.setSelectedCategory(it)
|
||||
},
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
CategoryAddButton(
|
||||
enabled = useCategory,
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
onClick = {
|
||||
component.addNewCategory()
|
||||
},
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.size(8.dp))
|
||||
LocationTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@ -273,7 +311,8 @@ private fun OnDuplicateStrategySolutionItem(
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Column {
|
||||
Text(title,
|
||||
Text(
|
||||
title,
|
||||
fontSize = myTextSizes.base,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
@ -419,7 +458,7 @@ private fun MainActionButtons(component: AddSingleDownloadComponent) {
|
||||
modifier = Modifier,
|
||||
onClick = { component.showSolutionsOnDuplicateDownloadUi = true },
|
||||
)
|
||||
if(component.shouldShowOpenFile.collectAsState().value){
|
||||
if (component.shouldShowOpenFile.collectAsState().value) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
MainConfigActionButton(
|
||||
text = "Open File",
|
||||
@ -475,6 +514,7 @@ fun RenderFileTypeAndSize(
|
||||
) {
|
||||
val isLinkLoading by component.isLinkLoading.collectAsState()
|
||||
val fileInfo by component.linkResponseInfo.collectAsState()
|
||||
val fileIconProvider = component.iconProvider
|
||||
val iconModifier = Modifier.size(16.dp)
|
||||
Box(Modifier.padding(top = 16.dp)) {
|
||||
AnimatedContent(
|
||||
@ -488,11 +528,7 @@ fun RenderFileTypeAndSize(
|
||||
} else {
|
||||
// val extension = getExtension(fileInfo?.fileName ?: usersSetFileName) ?: "unknown"
|
||||
val downloadItem by component.downloadItem.collectAsState()
|
||||
val category = remember(downloadItem) {
|
||||
DefinedTypeCategories.resolveCategoryForDownloadItem(
|
||||
ProcessingDownloadItemState.onlyDownloadItem(downloadItem)
|
||||
)
|
||||
}
|
||||
val icon = fileIconProvider.rememberIcon(downloadItem.name)
|
||||
|
||||
// val bitmap = FileIconProvider.getIconOfFileExtension(extension)
|
||||
|
||||
@ -513,7 +549,7 @@ fun RenderFileTypeAndSize(
|
||||
)
|
||||
}
|
||||
MyIcon(
|
||||
category.icon,
|
||||
icon,
|
||||
null,
|
||||
iconModifier
|
||||
)
|
||||
@ -577,8 +613,9 @@ private fun UrlTextField(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
end = {
|
||||
MyTextFieldIcon(MyIcons.paste) {
|
||||
setText(ClipboardUtil.read()
|
||||
.orEmpty()
|
||||
setText(
|
||||
ClipboardUtil.read()
|
||||
.orEmpty()
|
||||
)
|
||||
}
|
||||
},
|
||||
@ -590,7 +627,7 @@ private fun UrlTextField(
|
||||
private fun NameTextField(
|
||||
text: String,
|
||||
setText: (String) -> Unit,
|
||||
errorText: String? = null
|
||||
errorText: String? = null,
|
||||
) {
|
||||
AddDownloadPageTextField(
|
||||
text,
|
||||
|
@ -27,6 +27,9 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
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.CategoryManager
|
||||
|
||||
sealed interface AddSingleDownloadPageEffects {
|
||||
data class SuggestUrl(val link: String) : AddSingleDownloadPageEffects
|
||||
@ -35,10 +38,11 @@ sealed interface AddSingleDownloadPageEffects {
|
||||
class AddSingleDownloadComponent(
|
||||
ctx: ComponentContext,
|
||||
val onRequestClose: () -> Unit,
|
||||
val onRequestDownload: (DownloadItem, OnDuplicateStrategy) -> Unit,
|
||||
val onRequestAddToQueue: (DownloadItem, queueId: Long?, OnDuplicateStrategy) -> Unit,
|
||||
val onRequestDownload: OnRequestDownloadSingleItem,
|
||||
val onRequestAddToQueue: OnRequestAddSingleItem,
|
||||
val onRequestAddCategory: () -> Unit,
|
||||
val openExistingDownload: (Long) -> Unit,
|
||||
private val downloadItemOpener:DownloadItemOpener,
|
||||
private val downloadItemOpener: DownloadItemOpener,
|
||||
id: String,
|
||||
) : AddDownloadComponent(ctx, id),
|
||||
KoinComponent,
|
||||
@ -47,6 +51,44 @@ class AddSingleDownloadComponent(
|
||||
private val appSettings: AppRepository by inject()
|
||||
private val client: DownloaderClient by inject()
|
||||
val downloadSystem: DownloadSystem by inject()
|
||||
val iconProvider: FileIconProvider by inject()
|
||||
|
||||
private val categoryManager: CategoryManager by inject()
|
||||
|
||||
val categories = categoryManager.categoriesFlow
|
||||
private val _selectedCategory: MutableStateFlow<Category?> = MutableStateFlow(categories.value.firstOrNull())
|
||||
val selectedCategory = _selectedCategory.asStateFlow()
|
||||
|
||||
private val _useCategory = MutableStateFlow(false)
|
||||
val useCategory = _useCategory.asStateFlow()
|
||||
fun setUseCategory(value: Boolean) {
|
||||
_useCategory.update { value }
|
||||
if (value) {
|
||||
useCategoryFolder()
|
||||
} else {
|
||||
useDefaultFolder()
|
||||
}
|
||||
}
|
||||
|
||||
private fun useCategoryFolder() {
|
||||
val category = selectedCategory.value
|
||||
if (useCategory.value && category != null) {
|
||||
setFolder(category.path)
|
||||
}
|
||||
}
|
||||
|
||||
private fun useDefaultFolder() {
|
||||
setFolder(appSettings.saveLocation.value)
|
||||
}
|
||||
|
||||
|
||||
fun setSelectedCategory(category: Category) {
|
||||
_selectedCategory.update { category }
|
||||
if (useCategory.value) {
|
||||
useCategoryFolder()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val downloadChecker = DownloadUiChecker(
|
||||
initialFolder = appSettings.saveLocation.value,
|
||||
@ -86,6 +128,16 @@ class AddSingleDownloadComponent(
|
||||
)
|
||||
.onEachLatest { onDuplicateStrategy.update { null } }
|
||||
.launchIn(scope)
|
||||
|
||||
name.onEach {
|
||||
val category = categoryManager.getCategoryOfFileName(it)
|
||||
if (category == null) {
|
||||
setUseCategory(false)
|
||||
} else {
|
||||
setUseCategory(true)
|
||||
setSelectedCategory(category)
|
||||
}
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
private var wasOpened = false
|
||||
@ -147,12 +199,14 @@ class AddSingleDownloadComponent(
|
||||
this.length,
|
||||
this.speedLimit,
|
||||
this.threadCount
|
||||
) { credentials,
|
||||
folder,
|
||||
name,
|
||||
length,
|
||||
speedLimit,
|
||||
threadCount ->
|
||||
) {
|
||||
credentials,
|
||||
folder,
|
||||
name,
|
||||
length,
|
||||
speedLimit,
|
||||
threadCount,
|
||||
->
|
||||
DownloadItem(
|
||||
id = -1,
|
||||
folder = folder,
|
||||
@ -245,18 +299,44 @@ class AddSingleDownloadComponent(
|
||||
fun onRequestDownload() {
|
||||
val item = downloadItem.value
|
||||
consumeDialog {
|
||||
addToLastUsedLocations(item.folder)
|
||||
onRequestDownload(item, onDuplicateStrategy.value.orDefault())
|
||||
saveLocationIfNecessary(item.folder)
|
||||
onRequestDownload(
|
||||
item,
|
||||
onDuplicateStrategy.value.orDefault(),
|
||||
selectedCategory.value?.id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveLocationIfNecessary(folder: String) {
|
||||
val category = selectedCategory.value?.takeIf {
|
||||
useCategory.value
|
||||
}
|
||||
val shouldAdd = if (category == null) {
|
||||
// always add if user don't use category
|
||||
true
|
||||
} else {
|
||||
// only add if category path is not the same as provided path
|
||||
category.path != folder
|
||||
}
|
||||
if (shouldAdd) {
|
||||
addToLastUsedLocations(folder)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun onRequestAddToQueue(
|
||||
queueId: Long?,
|
||||
) {
|
||||
val downloadItem = downloadItem.value
|
||||
consumeDialog {
|
||||
addToLastUsedLocations(downloadItem.folder)
|
||||
onRequestAddToQueue(downloadItem, queueId, onDuplicateStrategy.value.orDefault())
|
||||
saveLocationIfNecessary(downloadItem.folder)
|
||||
onRequestAddToQueue(
|
||||
downloadItem,
|
||||
queueId,
|
||||
onDuplicateStrategy.value.orDefault(),
|
||||
selectedCategory.value?.id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,19 +353,19 @@ class AddSingleDownloadComponent(
|
||||
var shouldShowAddToQueue by mutableStateOf(false)
|
||||
|
||||
val shouldShowOpenFile = combine(
|
||||
onDuplicateStrategy,canAddResult,
|
||||
){onDuplicateStrategy, result ->
|
||||
if (result is CanAddResult.DownloadAlreadyExists && onDuplicateStrategy==null){
|
||||
onDuplicateStrategy, canAddResult,
|
||||
) { onDuplicateStrategy, result ->
|
||||
if (result is CanAddResult.DownloadAlreadyExists && onDuplicateStrategy == null) {
|
||||
val item = downloadSystem.getDownloadItemById(result.itemId) ?: return@combine false
|
||||
if (item.status!=DownloadStatus.Completed){
|
||||
if (item.status != DownloadStatus.Completed) {
|
||||
return@combine false
|
||||
}
|
||||
downloadSystem.getDownloadFile(item).exists()
|
||||
}else false
|
||||
}.stateIn(scope, SharingStarted.WhileSubscribed(),false)
|
||||
} else false
|
||||
}.stateIn(scope, SharingStarted.WhileSubscribed(), false)
|
||||
|
||||
fun openExistingFile(){
|
||||
val itemId= (canAddResult.value as? CanAddResult.DownloadAlreadyExists)?.itemId?:return
|
||||
fun openExistingFile() {
|
||||
val itemId = (canAddResult.value as? CanAddResult.DownloadAlreadyExists)?.itemId ?: return
|
||||
consumeDialog {
|
||||
scope.launch {
|
||||
downloadItemOpener.openDownloadItem(itemId)
|
||||
@ -293,4 +373,25 @@ class AddSingleDownloadComponent(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addNewCategory() {
|
||||
onRequestAddCategory()
|
||||
}
|
||||
}
|
||||
|
||||
fun interface OnRequestAddSingleItem {
|
||||
operator fun invoke(
|
||||
item: DownloadItem,
|
||||
queueId: Long?,
|
||||
onDuplicateStrategy: OnDuplicateStrategy,
|
||||
categoryId: Long?,
|
||||
)
|
||||
}
|
||||
|
||||
fun interface OnRequestDownloadSingleItem {
|
||||
operator fun invoke(
|
||||
item: DownloadItem,
|
||||
onDuplicateStrategy: OnDuplicateStrategy,
|
||||
categoryId: Long?,
|
||||
)
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
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
|
||||
import com.abdownloadmanager.utils.category.CategoryManager
|
||||
import com.abdownloadmanager.utils.category.iconSource
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import ir.amirab.util.compose.IconSource
|
||||
import ir.amirab.util.compose.uriOrNull
|
||||
import ir.amirab.util.flow.combineStateFlows
|
||||
import ir.amirab.util.osfileutil.FileUtils
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.io.File
|
||||
|
||||
class CategoryComponent(
|
||||
ctx: ComponentContext,
|
||||
val id: Long,
|
||||
val close: () -> Unit,
|
||||
private val submit: (Category) -> Unit,
|
||||
) : BaseComponent(ctx), KoinComponent {
|
||||
private val appRepository: AppRepository by inject()
|
||||
val defaultDownloadLocation = appRepository.saveLocation
|
||||
private val categoryManager: CategoryManager by inject()
|
||||
|
||||
init {
|
||||
if (id >= 0) {
|
||||
loadCategoryData()
|
||||
}
|
||||
}
|
||||
|
||||
fun loadCategoryData() {
|
||||
scope.launch {
|
||||
val category = categoryManager.getCategoryById(id) ?: return@launch
|
||||
setIcon(category.iconSource())
|
||||
setName(category.name)
|
||||
setTypes(category.acceptedFileTypes.joinToString(" "))
|
||||
setPath(category.path)
|
||||
}
|
||||
}
|
||||
|
||||
private val _icon = MutableStateFlow(null as IconSource?)
|
||||
val icon = _icon.asStateFlow()
|
||||
fun setIcon(iconSource: IconSource?) {
|
||||
_icon.value = iconSource
|
||||
}
|
||||
|
||||
private val _name = MutableStateFlow("")
|
||||
val name = _name.asStateFlow()
|
||||
fun setName(name: String) {
|
||||
_name.value = name
|
||||
}
|
||||
|
||||
private val _types = MutableStateFlow("")
|
||||
val types = _types.asStateFlow()
|
||||
fun setTypes(types: String) {
|
||||
_types.value = types
|
||||
}
|
||||
|
||||
private val _path = MutableStateFlow("")
|
||||
val path = _path.asStateFlow()
|
||||
fun setPath(path: String) {
|
||||
_path.value = path
|
||||
}
|
||||
|
||||
val canSubmit = combineStateFlows(
|
||||
icon,
|
||||
name,
|
||||
types,
|
||||
path
|
||||
) { icon, name, types, path ->
|
||||
val iconOk = icon != null
|
||||
val nameOk = name.isNotBlank()
|
||||
val pathOk = FileUtils.canWriteInThisFolder(path)
|
||||
iconOk && nameOk && pathOk
|
||||
}
|
||||
val isEditMode = id >= 0
|
||||
|
||||
fun submit() {
|
||||
if (!canSubmit.value) {
|
||||
return
|
||||
}
|
||||
val path = path.value
|
||||
kotlin.runCatching {
|
||||
File(path).mkdirs()
|
||||
}
|
||||
submit(
|
||||
Category(
|
||||
id = id,
|
||||
name = name.value,
|
||||
acceptedFileTypes = types.value
|
||||
.split(" ")
|
||||
.filterNot { it.isBlank() }
|
||||
.distinct(),
|
||||
icon = icon
|
||||
.value!!
|
||||
.uriOrNull()!!,
|
||||
path = path,
|
||||
items = emptyList() // ignored!
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.abdownloadmanager.desktop.pages.category
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface CategoryDialogManager {
|
||||
val openedCategoryDialogs: StateFlow<List<CategoryComponent>>
|
||||
fun openCategoryDialog(categoryId: Long)
|
||||
fun closeCategoryDialog(categoryId: Long)
|
||||
}
|
@ -0,0 +1,381 @@
|
||||
package com.abdownloadmanager.desktop.pages.category
|
||||
|
||||
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.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.abdownloadmanager.desktop.pages.addDownload.single.MyTextFieldIcon
|
||||
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.div
|
||||
import com.abdownloadmanager.utils.compose.WithContentAlpha
|
||||
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
||||
import io.github.vinceglb.filekit.compose.rememberDirectoryPickerLauncher
|
||||
import io.github.vinceglb.filekit.core.FileKitPlatformSettings
|
||||
import ir.amirab.util.compose.IconSource
|
||||
import ir.amirab.util.desktop.LocalWindow
|
||||
import java.io.File
|
||||
|
||||
@Composable
|
||||
fun NewCategory(
|
||||
categoryComponent: CategoryComponent,
|
||||
) {
|
||||
WindowTitle(
|
||||
if (categoryComponent.isEditMode) "Edit Category"
|
||||
else "Add Category"
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 32.dp)
|
||||
.padding(vertical = 16.dp)
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Row {
|
||||
CategoryIcon(
|
||||
iconSource = categoryComponent.icon.collectAsState().value,
|
||||
onChange = categoryComponent::setIcon
|
||||
)
|
||||
Spacer(Modifier.width(16.dp))
|
||||
CategoryName(
|
||||
modifier = Modifier.weight(1f),
|
||||
name = categoryComponent.name.collectAsState().value,
|
||||
onNameChanged = categoryComponent::setName
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(12.dp))
|
||||
CategoryAutoTypes(
|
||||
types = categoryComponent.types.collectAsState().value,
|
||||
onTypesChanged = categoryComponent::setTypes
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
CategoryDefaultPath(
|
||||
path = categoryComponent.path.collectAsState().value,
|
||||
onPathChanged = categoryComponent::setPath,
|
||||
defaultDownloadLocation = categoryComponent.defaultDownloadLocation.collectAsState().value
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Row(Modifier.fillMaxWidth().wrapContentWidth(Alignment.End)) {
|
||||
ActionButton(
|
||||
when (categoryComponent.isEditMode) {
|
||||
true -> "Change"
|
||||
false -> "Add"
|
||||
},
|
||||
enabled = categoryComponent.canSubmit.collectAsState().value,
|
||||
onClick = {
|
||||
categoryComponent.submit()
|
||||
}
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
ActionButton(
|
||||
"Cancel",
|
||||
onClick = {
|
||||
categoryComponent.close()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CategoryDefaultPath(
|
||||
defaultDownloadLocation: String,
|
||||
path: String,
|
||||
onPathChanged: (String) -> Unit,
|
||||
) {
|
||||
val initialDirectory = remember(path, defaultDownloadLocation) {
|
||||
path
|
||||
.takeIf { it.isNotBlank() }
|
||||
?.let {
|
||||
runCatching {
|
||||
File(path).canonicalPath
|
||||
}.getOrNull()
|
||||
} ?: defaultDownloadLocation
|
||||
}
|
||||
val downloadFolderPickerLauncher = rememberDirectoryPickerLauncher(
|
||||
title = "Category Download Location",
|
||||
initialDirectory = initialDirectory,
|
||||
platformSettings = FileKitPlatformSettings(
|
||||
parentWindow = LocalWindow.current
|
||||
)
|
||||
) { directory ->
|
||||
directory?.path?.let(onPathChanged)
|
||||
}
|
||||
|
||||
WithLabel(
|
||||
"Category Download Location",
|
||||
helpText = """When this category chosen in "Add Download Page" use this directory as "Download Location"""
|
||||
) {
|
||||
CategoryPageTextField(
|
||||
text = path,
|
||||
onTextChange = onPathChanged,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = "",
|
||||
errorText = null,
|
||||
end = {
|
||||
MyTextFieldIcon(MyIcons.folder) {
|
||||
downloadFolderPickerLauncher.launch()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CategoryAutoTypes(
|
||||
types: String,
|
||||
onTypesChanged: (String) -> Unit,
|
||||
) {
|
||||
WithLabel(
|
||||
label = "Category file types",
|
||||
helpText = "Automatically put download to these file types to this category. (when you add new download)\n Separate file extensions with space (ext1 ext2 ...) "
|
||||
) {
|
||||
CategoryPageTextField(
|
||||
text = types,
|
||||
onTextChange = onTypesChanged,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = "ext1 ext2 ext3 (separate with space)",
|
||||
singleLine = false,
|
||||
minLines = 2,
|
||||
maxLines = 2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CategoryName(
|
||||
name: String,
|
||||
onNameChanged: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
WithLabel(
|
||||
"Category Name",
|
||||
modifier,
|
||||
) {
|
||||
CategoryPageTextField(
|
||||
text = name,
|
||||
onTextChange = onNameChanged,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = "Something...",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WithLabel(
|
||||
label: String,
|
||||
modifier: Modifier = Modifier,
|
||||
helpText: String? = null,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Column(modifier) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(label)
|
||||
helpText?.let {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Help(helpText)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CategoryIcon(
|
||||
iconSource: IconSource?,
|
||||
onChange: (IconSource) -> Unit,
|
||||
) {
|
||||
var showIconPicker by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
WithLabel(
|
||||
"Icon"
|
||||
) {
|
||||
RenderIcon(
|
||||
icon = iconSource,
|
||||
requiresAttention = iconSource == null,
|
||||
onClick = {
|
||||
showIconPicker = !showIconPicker
|
||||
}
|
||||
)
|
||||
if (showIconPicker) {
|
||||
IconPick(
|
||||
selectedIcon = iconSource,
|
||||
icons = listOf(
|
||||
MyIcons.pictureFile,
|
||||
MyIcons.musicFile,
|
||||
MyIcons.zipFile,
|
||||
MyIcons.videoFile,
|
||||
MyIcons.applicationFile,
|
||||
MyIcons.documentFile,
|
||||
MyIcons.otherFile,
|
||||
|
||||
MyIcons.file,
|
||||
MyIcons.folder,
|
||||
|
||||
MyIcons.browserIntegration,
|
||||
MyIcons.appearance,
|
||||
|
||||
MyIcons.settings,
|
||||
MyIcons.search,
|
||||
MyIcons.info,
|
||||
MyIcons.check,
|
||||
MyIcons.link,
|
||||
MyIcons.download,
|
||||
MyIcons.speaker,
|
||||
MyIcons.group,
|
||||
MyIcons.activeCount,
|
||||
MyIcons.speed,
|
||||
MyIcons.resume,
|
||||
MyIcons.pause,
|
||||
MyIcons.stop,
|
||||
MyIcons.queue,
|
||||
MyIcons.remove,
|
||||
MyIcons.clear,
|
||||
MyIcons.add,
|
||||
MyIcons.paste,
|
||||
MyIcons.copy,
|
||||
MyIcons.refresh,
|
||||
MyIcons.share,
|
||||
MyIcons.lock,
|
||||
MyIcons.question,
|
||||
MyIcons.verticalDirection,
|
||||
MyIcons.downloadEngine,
|
||||
MyIcons.network,
|
||||
MyIcons.externalLink,
|
||||
),
|
||||
onSelected = {
|
||||
onChange(it)
|
||||
showIconPicker = false
|
||||
},
|
||||
onCancel = {
|
||||
showIconPicker = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun RenderIcon(
|
||||
icon: IconSource?,
|
||||
indicateActive: Boolean = false,
|
||||
requiresAttention: Boolean = false,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val shape = RoundedCornerShape(10.dp)
|
||||
Box(
|
||||
Modifier
|
||||
.border(
|
||||
1.dp,
|
||||
myColors.onBackground / 10,
|
||||
shape
|
||||
)
|
||||
.ifThen(indicateActive || requiresAttention) {
|
||||
border(
|
||||
1.dp,
|
||||
myColors.primary / if (indicateActive) 1f else alphaFlicker(),
|
||||
shape
|
||||
)
|
||||
}
|
||||
.clip(shape)
|
||||
.background(myColors.surface)
|
||||
.clickable {
|
||||
onClick()
|
||||
}
|
||||
.padding(6.dp)
|
||||
) {
|
||||
val modifier = Modifier
|
||||
.size(20.dp)
|
||||
if (icon != null) {
|
||||
MyIcon(
|
||||
icon,
|
||||
null,
|
||||
modifier,
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun CategoryPageTextField(
|
||||
text: String,
|
||||
onTextChange: (String) -> Unit,
|
||||
placeholder: String,
|
||||
modifier: Modifier,
|
||||
errorText: String? = null,
|
||||
singleLine: Boolean = true,
|
||||
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||
minLines: Int = 1,
|
||||
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 = text,
|
||||
onTextChange = onTextChange,
|
||||
placeholder = placeholder,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
maxLines = maxLines,
|
||||
minLines = minLines,
|
||||
singleLine = singleLine,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,34 @@
|
||||
package com.abdownloadmanager.desktop.pages.category
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.rememberWindowState
|
||||
import com.abdownloadmanager.desktop.ui.customwindow.CustomWindow
|
||||
|
||||
@Composable
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CategoryDialog(
|
||||
component: CategoryComponent,
|
||||
) {
|
||||
NewCategory(component)
|
||||
}
|
@ -5,7 +5,6 @@ import com.abdownloadmanager.desktop.actions.*
|
||||
import com.abdownloadmanager.desktop.pages.home.sections.DownloadListCells
|
||||
import com.abdownloadmanager.desktop.pages.home.sections.category.DefinedStatusCategories
|
||||
import com.abdownloadmanager.desktop.pages.home.sections.category.DownloadStatusCategoryFilter
|
||||
import com.abdownloadmanager.desktop.pages.home.sections.category.DownloadTypeCategoryFilter
|
||||
import com.abdownloadmanager.desktop.storage.PageStatesStorage
|
||||
import com.abdownloadmanager.desktop.ui.icon.MyIcons
|
||||
import com.abdownloadmanager.desktop.ui.widget.NotificationType
|
||||
@ -21,7 +20,12 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.CategoryManager
|
||||
import com.abdownloadmanager.utils.category.DefaultCategories
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import ir.amirab.downloader.downloaditem.DownloadCredentials
|
||||
import ir.amirab.downloader.downloaditem.DownloadJobStatus
|
||||
@ -32,6 +36,7 @@ import ir.amirab.util.flow.combineStateFlows
|
||||
import ir.amirab.util.flow.mapStateFlow
|
||||
import ir.amirab.util.flow.mapTwoWayStateFlow
|
||||
import com.abdownloadmanager.utils.extractors.linkextractor.DownloadCredentialFromStringExtractor
|
||||
import ir.amirab.util.osfileutil.FileUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
@ -43,7 +48,7 @@ import java.net.URI
|
||||
@Stable
|
||||
class FilterState {
|
||||
var textToSearch by mutableStateOf("")
|
||||
var typeCategoryFilter by mutableStateOf(null as DownloadTypeCategoryFilter?)
|
||||
var typeCategoryFilter by mutableStateOf(null as Category?)
|
||||
var statusFilter by mutableStateOf<DownloadStatusCategoryFilter>(DefinedStatusCategories.All)
|
||||
}
|
||||
|
||||
@ -53,6 +58,13 @@ sealed interface HomeEffects {
|
||||
data class DeleteItems(
|
||||
val list: List<Long>,
|
||||
) : HomeEffects
|
||||
|
||||
data class DeleteCategory(
|
||||
val category: Category,
|
||||
) : HomeEffects
|
||||
|
||||
data object ResetCategoriesToDefault : HomeEffects
|
||||
data object AutoCategorize : HomeEffects
|
||||
}
|
||||
|
||||
|
||||
@ -63,6 +75,7 @@ class DownloadActions(
|
||||
val selections: StateFlow<List<IDownloadItemState>>,
|
||||
private val mainItem: StateFlow<Long?>,
|
||||
private val queueManager: QueueManager,
|
||||
private val categoryManager: CategoryManager,
|
||||
private val openFile: (Long) -> Unit,
|
||||
private val openFolder: (Long) -> Unit,
|
||||
private val requestDelete: (List<Long>) -> Unit,
|
||||
@ -214,6 +227,28 @@ class DownloadActions(
|
||||
setItems(list)
|
||||
}.launchIn(scope)
|
||||
}
|
||||
private val moveToCategoryAction = MenuItem.SubMenu(
|
||||
title = "Move To Category",
|
||||
items = emptyList()
|
||||
).apply {
|
||||
merge(
|
||||
categoryManager.categoriesFlow.mapStateFlow {
|
||||
it.map(Category::id)
|
||||
},
|
||||
selections
|
||||
).onEach {
|
||||
val categories = categoryManager.categoriesFlow.value
|
||||
val list = categories.map { category ->
|
||||
createMoveToCategoryAction(
|
||||
category = category,
|
||||
itemIds = selections.value.map { iDownloadItemState ->
|
||||
iDownloadItemState.id
|
||||
}
|
||||
)
|
||||
}
|
||||
setItems(list)
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
|
||||
val menu: List<MenuItem> = buildMenu {
|
||||
@ -226,17 +261,123 @@ class DownloadActions(
|
||||
+(reDownloadAction)
|
||||
separator()
|
||||
+moveToQueueItems
|
||||
+moveToCategoryAction
|
||||
separator()
|
||||
+(copyDownloadLinkAction)
|
||||
+(openDownloadDialogAction)
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class CategoryActions(
|
||||
private val scope: CoroutineScope,
|
||||
private val categoryManager: CategoryManager,
|
||||
private val defaultCategories: DefaultCategories,
|
||||
|
||||
val categoryItem: Category?,
|
||||
|
||||
private val openFolder: (Category) -> Unit,
|
||||
private val requestDelete: (Category) -> Unit,
|
||||
private val requestEdit: (Category) -> Unit,
|
||||
|
||||
private val onRequestResetToDefaults: () -> Unit,
|
||||
private val onRequestCategorizeItems: () -> Unit,
|
||||
private val onRequestAddCategory: () -> Unit,
|
||||
) {
|
||||
private val mainItemExists = MutableStateFlow(categoryItem != null)
|
||||
private inline fun useItem(
|
||||
block: (Category) -> Unit,
|
||||
) {
|
||||
categoryItem?.let(block)
|
||||
}
|
||||
|
||||
val openCategoryFolderAction = simpleAction(
|
||||
title = "Open Folder",
|
||||
icon = MyIcons.folderOpen,
|
||||
checkEnable = mainItemExists,
|
||||
onActionPerformed = {
|
||||
scope.launch {
|
||||
useItem {
|
||||
openFolder(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val deleteAction = simpleAction(
|
||||
title = "Delete Category",
|
||||
icon = MyIcons.remove,
|
||||
checkEnable = mainItemExists,
|
||||
onActionPerformed = {
|
||||
scope.launch {
|
||||
useItem {
|
||||
requestDelete(it)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
val editAction = simpleAction(
|
||||
title = "Edit Category",
|
||||
icon = MyIcons.settings,
|
||||
checkEnable = mainItemExists,
|
||||
onActionPerformed = {
|
||||
scope.launch {
|
||||
useItem {
|
||||
requestEdit(it)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
val addCategoryAction = simpleAction(
|
||||
title = "Add Category",
|
||||
icon = MyIcons.add,
|
||||
onActionPerformed = {
|
||||
scope.launch {
|
||||
onRequestAddCategory()
|
||||
}
|
||||
},
|
||||
)
|
||||
val categorizeItemsAction = simpleAction(
|
||||
title = "Auto Categorise Items",
|
||||
icon = MyIcons.refresh,
|
||||
onActionPerformed = {
|
||||
scope.launch {
|
||||
onRequestCategorizeItems()
|
||||
}
|
||||
},
|
||||
)
|
||||
val resetToDefaultAction = simpleAction(
|
||||
title = "Restore Defaults",
|
||||
icon = MyIcons.undo,
|
||||
checkEnable = categoryManager
|
||||
.categoriesFlow
|
||||
.mapStateFlow { !defaultCategories.isDefault(it) },
|
||||
onActionPerformed = {
|
||||
scope.launch {
|
||||
onRequestResetToDefaults()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
val menu: List<MenuItem> = buildMenu {
|
||||
+editAction
|
||||
+openCategoryFolderAction
|
||||
+deleteAction
|
||||
separator()
|
||||
+addCategoryAction
|
||||
separator()
|
||||
+categorizeItemsAction
|
||||
+resetToDefaultAction
|
||||
}
|
||||
}
|
||||
|
||||
class HomeComponent(
|
||||
ctx: ComponentContext,
|
||||
private val downloadItemOpener: DownloadItemOpener,
|
||||
private val downloadDialogManager: DownloadDialogManager,
|
||||
private val addDownloadDialogManager: AddDownloadDialogManager,
|
||||
private val categoryDialogManager: CategoryDialogManager,
|
||||
private val notificationSender: NotificationSender,
|
||||
) : BaseComponent(ctx),
|
||||
ContainsShortcuts,
|
||||
@ -251,6 +392,10 @@ class HomeComponent(
|
||||
|
||||
private val homePageStateToPersist = MutableStateFlow(pageStorage.homePageStorage.value)
|
||||
|
||||
val categoryManager: CategoryManager by inject()
|
||||
private val defaultCategories: DefaultCategories by inject()
|
||||
val fileIconProvider: FileIconProvider by inject()
|
||||
|
||||
init {
|
||||
homePageStateToPersist
|
||||
.debounce(500)
|
||||
@ -296,6 +441,12 @@ class HomeComponent(
|
||||
sendEffect(HomeEffects.DeleteItems(downloadList))
|
||||
}
|
||||
|
||||
fun onConfirmDeleteCategory(promptState: CategoryDeletePromptState) {
|
||||
scope.launch {
|
||||
categoryManager.deleteCategory(promptState.category)
|
||||
}
|
||||
}
|
||||
|
||||
fun confirmDelete(promptState: DeletePromptState) {
|
||||
scope.launch {
|
||||
val selectionList = promptState.downloadList
|
||||
@ -305,6 +456,25 @@ class HomeComponent(
|
||||
}
|
||||
}
|
||||
|
||||
fun onConfirmAutoCategorize() {
|
||||
val categorizedItems = categoryManager.getCategories()
|
||||
.flatMap { it.items }
|
||||
val allDownloads = activeDownloadList.value + completedList.value
|
||||
val unCategorizedItems = allDownloads.filterNot {
|
||||
it.id in categorizedItems
|
||||
}
|
||||
categoryManager
|
||||
.autoAddItemsToCategoriesBasedOnFileNames(
|
||||
unCategorizedItems.map { it.id to it.name }
|
||||
)
|
||||
}
|
||||
|
||||
fun onConfirmResetCategories() {
|
||||
scope.launch {
|
||||
categoryManager.reset()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun requestAddNewDownload(
|
||||
link: List<DownloadCredentials> = listOf(DownloadCredentials.empty()),
|
||||
@ -370,7 +540,6 @@ class HomeComponent(
|
||||
}.filterIsInstance<MenuItem.SubMenu>()
|
||||
|
||||
|
||||
|
||||
private val shouldShowOptions = MutableStateFlow(false)
|
||||
val downloadOptions = combineStateFlows(
|
||||
shouldShowOptions,
|
||||
@ -465,7 +634,7 @@ class HomeComponent(
|
||||
|
||||
fun onFilterChange(
|
||||
statusCategoryFilter: DownloadStatusCategoryFilter,
|
||||
typeCategoryFilter: DownloadTypeCategoryFilter?,
|
||||
typeCategoryFilter: Category?,
|
||||
) {
|
||||
this.filterState.statusFilter = statusCategoryFilter
|
||||
this.filterState.typeCategoryFilter = typeCategoryFilter
|
||||
@ -546,6 +715,14 @@ class HomeComponent(
|
||||
emptyList()
|
||||
)
|
||||
|
||||
init {
|
||||
categoryManager.categoriesFlow.onEach { categories ->
|
||||
val currentCategory = filterState.typeCategoryFilter ?: return@onEach
|
||||
filterState.typeCategoryFilter = categories.find {
|
||||
it.id == currentCategory.id
|
||||
}
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
val downloadList = merge(
|
||||
snapshotFlow { filterState.textToSearch },
|
||||
@ -558,7 +735,8 @@ class HomeComponent(
|
||||
(activeDownloadList.value + completedList.value)
|
||||
.filter {
|
||||
val statusAccepted = filterState.statusFilter.accept(it)
|
||||
val typeAccepted = filterState.typeCategoryFilter?.accept(it) ?: true
|
||||
// val typeAccepted = filterState.typeCategoryFilter?.accept(it.name) ?: true
|
||||
val typeAccepted = filterState.typeCategoryFilter?.items?.contains(it.id) ?: true
|
||||
val searchAccepted = it.name.contains(filterState.textToSearch, ignoreCase = true)
|
||||
typeAccepted && statusAccepted && searchAccepted
|
||||
}
|
||||
@ -630,16 +808,54 @@ class HomeComponent(
|
||||
|
||||
|
||||
private val downloadActions = DownloadActions(
|
||||
scope,
|
||||
downloadSystem,
|
||||
downloadDialogManager,
|
||||
selectionListItems,
|
||||
mainItem,
|
||||
queueManager,
|
||||
this::openFile,
|
||||
this::openFolder,
|
||||
this::requestDelete,
|
||||
scope = scope,
|
||||
downloadSystem = downloadSystem,
|
||||
downloadDialogManager = downloadDialogManager,
|
||||
selections = selectionListItems,
|
||||
mainItem = mainItem,
|
||||
queueManager = queueManager,
|
||||
categoryManager = categoryManager,
|
||||
openFile = this::openFile,
|
||||
openFolder = this::openFolder,
|
||||
requestDelete = this::requestDelete,
|
||||
)
|
||||
val categoryActions = MutableStateFlow(null as CategoryActions?)
|
||||
|
||||
fun showCategoryOptions(categoryItem: Category?) {
|
||||
categoryActions.value = CategoryActions(
|
||||
scope = scope,
|
||||
categoryManager = categoryManager,
|
||||
defaultCategories = defaultCategories,
|
||||
categoryItem = categoryItem,
|
||||
openFolder = {
|
||||
runCatching {
|
||||
FileUtils.openFolder(File(it.path))
|
||||
}
|
||||
},
|
||||
onRequestAddCategory = {
|
||||
categoryDialogManager.openCategoryDialog(-1)
|
||||
},
|
||||
requestDelete = {
|
||||
sendEffect(
|
||||
HomeEffects.DeleteCategory(it)
|
||||
)
|
||||
},
|
||||
requestEdit = {
|
||||
categoryDialogManager.openCategoryDialog(it.id)
|
||||
},
|
||||
onRequestCategorizeItems = {
|
||||
sendEffect(HomeEffects.AutoCategorize)
|
||||
},
|
||||
onRequestResetToDefaults = {
|
||||
sendEffect(HomeEffects.ResetCategoriesToDefault)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun closeCategoryOptions() {
|
||||
categoryActions.value = null
|
||||
}
|
||||
|
||||
override val shortcutManager = ShortcutManager().apply {
|
||||
"ctrl N" to newDownloadAction
|
||||
"ctrl V" to newDownloadFromClipboardAction
|
||||
|
@ -40,7 +40,11 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.abdownloadmanager.desktop.ui.customwindow.*
|
||||
import com.abdownloadmanager.desktop.ui.widget.menu.ShowOptionsInDropDown
|
||||
import com.abdownloadmanager.desktop.utils.externaldraggable.DragData
|
||||
import com.abdownloadmanager.utils.category.Category
|
||||
import com.abdownloadmanager.utils.category.rememberIconPainter
|
||||
import ir.amirab.util.compose.action.MenuItem
|
||||
|
||||
|
||||
@Composable
|
||||
@ -53,6 +57,14 @@ fun HomePage(component: HomeComponent) {
|
||||
mutableStateOf(null as DeletePromptState?)
|
||||
}
|
||||
|
||||
var showDeleteCategoryPromptState by remember {
|
||||
mutableStateOf(null as CategoryDeletePromptState?)
|
||||
}
|
||||
|
||||
var showConfirmPrompt by remember {
|
||||
mutableStateOf(null as ConfirmPromptState?)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
component.effects.onEach {
|
||||
when (it) {
|
||||
@ -62,6 +74,26 @@ fun HomePage(component: HomeComponent) {
|
||||
}
|
||||
}
|
||||
|
||||
is HomeEffects.DeleteCategory -> {
|
||||
showDeleteCategoryPromptState = CategoryDeletePromptState(it.category)
|
||||
}
|
||||
|
||||
is HomeEffects.AutoCategorize -> {
|
||||
showConfirmPrompt = ConfirmPromptState(
|
||||
title = "Auto categorize downloads",
|
||||
description = "Any uncategorized item will be automatically added to it's related category.",
|
||||
onConfirm = component::onConfirmAutoCategorize
|
||||
)
|
||||
}
|
||||
|
||||
is HomeEffects.ResetCategoriesToDefault -> {
|
||||
showConfirmPrompt = ConfirmPromptState(
|
||||
title = "Reset to Default Categories",
|
||||
description = "this will REMOVE all categories and brings backs default categories",
|
||||
onConfirm = component::onConfirmResetCategories
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
@ -78,6 +110,29 @@ fun HomePage(component: HomeComponent) {
|
||||
component.confirmDelete(it)
|
||||
})
|
||||
}
|
||||
showDeleteCategoryPromptState?.let {
|
||||
ShowDeleteCategoryPrompt(
|
||||
deletePromptState = it,
|
||||
onCancel = {
|
||||
showDeleteCategoryPromptState = null
|
||||
},
|
||||
onConfirm = {
|
||||
showDeleteCategoryPromptState = null
|
||||
component.onConfirmDeleteCategory(it)
|
||||
})
|
||||
}
|
||||
showConfirmPrompt?.let {
|
||||
ShowConfirmPrompt(
|
||||
promptState = it,
|
||||
onCancel = {
|
||||
showConfirmPrompt = null
|
||||
},
|
||||
onConfirm = {
|
||||
showConfirmPrompt?.onConfirm?.invoke()
|
||||
showConfirmPrompt = null
|
||||
}
|
||||
)
|
||||
}
|
||||
val mergeTopBar = shouldMergeTopBarWithTitleBar(component)
|
||||
if (mergeTopBar) {
|
||||
WindowTitlePosition(
|
||||
@ -147,8 +202,9 @@ fun HomePage(component: HomeComponent) {
|
||||
Row() {
|
||||
val categoriesWidth by component.categoriesWidth.collectAsState()
|
||||
Categories(
|
||||
Modifier.padding(top = 8.dp)
|
||||
.width(categoriesWidth), component
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
.width(categoriesWidth),
|
||||
component = component
|
||||
)
|
||||
Spacer(Modifier.size(8.dp))
|
||||
//split pane
|
||||
@ -195,6 +251,8 @@ fun HomePage(component: HomeComponent) {
|
||||
},
|
||||
lastSelectedId = lastSelected,
|
||||
tableState = component.tableState,
|
||||
fileIconProvider = component.fileIconProvider,
|
||||
categoryManager = component.categoryManager,
|
||||
)
|
||||
Spacer(
|
||||
Modifier
|
||||
@ -315,6 +373,122 @@ private fun ShowDeletePrompts(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowConfirmPrompt(
|
||||
promptState: ConfirmPromptState,
|
||||
onConfirm: () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
) {
|
||||
val shape = RoundedCornerShape(6.dp)
|
||||
Dialog(onDismissRequest = onCancel) {
|
||||
Column(
|
||||
Modifier
|
||||
.clip(shape)
|
||||
.border(2.dp, myColors.onBackground / 10, shape)
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
myColors.surface,
|
||||
myColors.background,
|
||||
)
|
||||
)
|
||||
)
|
||||
.padding(16.dp)
|
||||
.width(IntrinsicSize.Max)
|
||||
.widthIn(max = 260.dp)
|
||||
) {
|
||||
Text(
|
||||
text = promptState.title,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = myTextSizes.xl,
|
||||
color = myColors.onBackground,
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(
|
||||
text = promptState.description,
|
||||
fontSize = myTextSizes.base,
|
||||
color = myColors.onBackground,
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Spacer(Modifier.weight(1f))
|
||||
ActionButton(
|
||||
text = "Delete",
|
||||
onClick = onConfirm,
|
||||
borderColor = SolidColor(myColors.error),
|
||||
contentColor = myColors.error,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
ActionButton(text = "Cancel", onClick = onCancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowDeleteCategoryPrompt(
|
||||
deletePromptState: CategoryDeletePromptState,
|
||||
onConfirm: () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
) {
|
||||
val shape = RoundedCornerShape(6.dp)
|
||||
Dialog(onDismissRequest = onCancel) {
|
||||
Column(
|
||||
Modifier
|
||||
.clip(shape)
|
||||
.border(2.dp, myColors.onBackground / 10, shape)
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
myColors.surface,
|
||||
myColors.background,
|
||||
)
|
||||
)
|
||||
)
|
||||
.padding(16.dp)
|
||||
.width(IntrinsicSize.Max)
|
||||
.widthIn(max = 260.dp)
|
||||
) {
|
||||
Text(
|
||||
"""Removing "${deletePromptState.category.name}" Category""",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = myTextSizes.xl,
|
||||
color = myColors.onBackground,
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(
|
||||
"""Are you sure you want to delete "${deletePromptState.category.name}" Category ?""",
|
||||
fontSize = myTextSizes.base,
|
||||
color = myColors.onBackground,
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(
|
||||
"Your downloads won't be deleted",
|
||||
fontSize = myTextSizes.base,
|
||||
color = myColors.onBackground,
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Spacer(Modifier.weight(1f))
|
||||
ActionButton(
|
||||
text = "Delete",
|
||||
onClick = onConfirm,
|
||||
borderColor = SolidColor(myColors.error),
|
||||
contentColor = myColors.error,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
ActionButton(text = "Cancel", onClick = onCancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class DeletePromptState(
|
||||
val downloadList: List<Long>,
|
||||
@ -322,6 +496,18 @@ class DeletePromptState(
|
||||
var alsoDeleteFile by mutableStateOf(false)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class CategoryDeletePromptState(
|
||||
val category: Category,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class ConfirmPromptState(
|
||||
val title: String,
|
||||
val description: String,
|
||||
val onConfirm: () -> Unit,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun DragWidget(
|
||||
modifier: Modifier,
|
||||
@ -375,15 +561,26 @@ fun DragWidget(
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun Categories(
|
||||
modifier: Modifier,
|
||||
component: HomeComponent,
|
||||
) {
|
||||
|
||||
val currentTypeFilter = component.filterState.typeCategoryFilter
|
||||
val currentStatusFilter = component.filterState.statusFilter
|
||||
|
||||
val categories by component.categoryManager.categoriesFlow.collectAsState()
|
||||
val clipShape = RoundedCornerShape(12.dp)
|
||||
val showCategoryOption by component.categoryActions.collectAsState()
|
||||
|
||||
fun showCategoryOption(item: Category?) {
|
||||
component.showCategoryOptions(item)
|
||||
}
|
||||
|
||||
fun closeCategoryOptions() {
|
||||
component.closeCategoryOptions()
|
||||
}
|
||||
Column(
|
||||
modifier
|
||||
.padding(start = 16.dp)
|
||||
@ -398,16 +595,42 @@ private fun Categories(
|
||||
currentTypeCategoryFilter = currentTypeFilter,
|
||||
currentStatusCategoryFilter = currentStatusFilter,
|
||||
statusFilter = statusCategoryFilter,
|
||||
typeFilter = DefinedTypeCategories.values(),
|
||||
categories = categories,
|
||||
onFilterChange = {
|
||||
component.onFilterChange(statusCategoryFilter, it)
|
||||
},
|
||||
onRequestExpand = { expand ->
|
||||
expendedItem = statusCategoryFilter.takeIf { expand }
|
||||
},
|
||||
onRequestOpenOptionMenu = {
|
||||
showCategoryOption(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
showCategoryOption?.let {
|
||||
CategoryOption(
|
||||
categoryOptionMenuState = it,
|
||||
onDismiss = {
|
||||
closeCategoryOptions()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CategoryOption(
|
||||
categoryOptionMenuState: CategoryActions,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
ShowOptionsInDropDown(
|
||||
MenuItem.SubMenu(
|
||||
icon = categoryOptionMenuState.categoryItem?.rememberIconPainter(),
|
||||
title = categoryOptionMenuState.categoryItem?.name.orEmpty(),
|
||||
categoryOptionMenuState.menu,
|
||||
),
|
||||
onDismiss
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
@ -25,6 +25,9 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.key.*
|
||||
import androidx.compose.ui.input.pointer.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.abdownloadmanager.utils.FileIconProvider
|
||||
import com.abdownloadmanager.utils.category.CategoryManager
|
||||
import com.abdownloadmanager.utils.category.rememberCategoryOf
|
||||
import ir.amirab.downloader.monitor.IDownloadItemState
|
||||
import ir.amirab.downloader.monitor.remainingOrNull
|
||||
import ir.amirab.downloader.monitor.speedOrNull
|
||||
@ -63,6 +66,8 @@ fun DownloadList(
|
||||
onRequestOpenDownload: (Long) -> Unit,
|
||||
onNewSelection: (List<Long>) -> Unit,
|
||||
lastSelectedId: Long?,
|
||||
fileIconProvider: FileIconProvider,
|
||||
categoryManager: CategoryManager,
|
||||
) {
|
||||
val state = rememberLazyListState()
|
||||
ShowDownloadOptions(
|
||||
@ -249,7 +254,11 @@ fun DownloadList(
|
||||
}
|
||||
|
||||
DownloadListCells.Name -> {
|
||||
NameCell(item)
|
||||
NameCell(
|
||||
itemState = item,
|
||||
category = categoryManager.rememberCategoryOf(item.id),
|
||||
fileIconProvider = fileIconProvider,
|
||||
)
|
||||
}
|
||||
|
||||
DownloadListCells.DateAdded -> {
|
||||
|
@ -1,7 +1,6 @@
|
||||
package com.abdownloadmanager.desktop.pages.home.sections
|
||||
|
||||
import com.abdownloadmanager.desktop.pages.home.sections.SortIndicatorMode.*
|
||||
import com.abdownloadmanager.desktop.pages.home.sections.category.DefinedTypeCategories
|
||||
import com.abdownloadmanager.utils.compose.LocalContentColor
|
||||
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
||||
import com.abdownloadmanager.desktop.ui.theme.myColors
|
||||
@ -20,6 +19,8 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.abdownloadmanager.utils.FileIconProvider
|
||||
import com.abdownloadmanager.utils.category.Category
|
||||
import ir.amirab.downloader.downloaditem.DownloadJobStatus
|
||||
import ir.amirab.downloader.monitor.CompletedDownloadItemState
|
||||
import ir.amirab.downloader.monitor.IDownloadItemState
|
||||
@ -30,7 +31,7 @@ import kotlinx.coroutines.isActive
|
||||
import kotlinx.datetime.*
|
||||
|
||||
val LocalDownloadItemProperties =
|
||||
compositionLocalOf<DownloadItemProperties> { error("not provided download properties") }
|
||||
compositionLocalOf<DownloadItemProperties> { error("not provided download properties") }
|
||||
|
||||
|
||||
data class DownloadItemProperties(
|
||||
@ -91,16 +92,16 @@ fun CheckCell(
|
||||
|
||||
@Composable
|
||||
fun NameCell(
|
||||
itemState: IDownloadItemState
|
||||
itemState: IDownloadItemState,
|
||||
category: Category?,
|
||||
fileIconProvider: FileIconProvider,
|
||||
) {
|
||||
val typeCategoryFilter = remember(itemState.id) {
|
||||
DefinedTypeCategories.resolveCategoryForDownloadItem(itemState)
|
||||
}
|
||||
val fileIcon = fileIconProvider.rememberIcon(itemState.name)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
MyIcon(
|
||||
icon = typeCategoryFilter.icon,
|
||||
icon = fileIcon,
|
||||
modifier = Modifier.size(16.dp),
|
||||
contentDescription = null,
|
||||
// tint = LocalContentColor.current / 75
|
||||
@ -113,7 +114,8 @@ fun NameCell(
|
||||
fontSize = myTextSizes.base,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(typeCategoryFilter.name, maxLines = 1, fontSize = myTextSizes.xs,
|
||||
Text(
|
||||
category?.name ?: "General", maxLines = 1, fontSize = myTextSizes.xs,
|
||||
color = LocalContentColor.current / 50
|
||||
)
|
||||
}
|
||||
@ -203,9 +205,9 @@ fun StatusCell(
|
||||
ProgressAndPercent(
|
||||
itemState.percent,
|
||||
if (ExceptionUtils.isNormalCancellation(status.e)) {
|
||||
if (!itemState.gotAnyProgress){
|
||||
if (!itemState.gotAnyProgress) {
|
||||
DownloadProgressStatus.Added
|
||||
}else{
|
||||
} else {
|
||||
DownloadProgressStatus.Paused
|
||||
}
|
||||
} else {
|
||||
@ -218,9 +220,9 @@ fun StatusCell(
|
||||
DownloadJobStatus.IDLE -> {
|
||||
ProgressAndPercent(
|
||||
itemState.percent,
|
||||
if (!itemState.gotAnyProgress){
|
||||
if (!itemState.gotAnyProgress) {
|
||||
DownloadProgressStatus.Added
|
||||
}else{
|
||||
} else {
|
||||
DownloadProgressStatus.Paused
|
||||
},
|
||||
itemState.gotAnyProgress
|
||||
@ -245,7 +247,7 @@ fun StatusCell(
|
||||
|
||||
DownloadJobStatus.Finished,
|
||||
DownloadJobStatus.Resuming,
|
||||
-> SimpleStatus(itemState.status.toString())
|
||||
-> SimpleStatus(itemState.status.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@ -275,22 +277,22 @@ private enum class DownloadProgressStatus {
|
||||
private fun ProgressAndPercent(
|
||||
percent: Int?,
|
||||
status: DownloadProgressStatus,
|
||||
gotAnyProgress:Boolean,
|
||||
gotAnyProgress: Boolean,
|
||||
) {
|
||||
val background = when (status) {
|
||||
DownloadProgressStatus.Error -> myColors.errorGradient
|
||||
DownloadProgressStatus.Paused,DownloadProgressStatus.Added -> myColors.warningGradient
|
||||
DownloadProgressStatus.Paused, DownloadProgressStatus.Added -> myColors.warningGradient
|
||||
DownloadProgressStatus.CreatingFile -> myColors.infoGradient
|
||||
DownloadProgressStatus.Downloading -> myColors.primaryGradient
|
||||
}
|
||||
Column {
|
||||
val statusText = if (gotAnyProgress){
|
||||
val statusText = if (gotAnyProgress) {
|
||||
"${percent ?: "."}% $status"
|
||||
}else{
|
||||
} else {
|
||||
"$status"
|
||||
}
|
||||
SimpleStatus(statusText)
|
||||
if (status != DownloadProgressStatus.Added){
|
||||
if (status != DownloadProgressStatus.Added) {
|
||||
Spacer(Modifier.height(2.5.dp))
|
||||
ProgressStatus(
|
||||
percent, background
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.abdownloadmanager.desktop.pages.home.sections.category
|
||||
|
||||
import androidx.compose.foundation.PointerMatcher
|
||||
import ir.amirab.util.compose.IconSource
|
||||
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
||||
import com.abdownloadmanager.desktop.ui.icon.MyIcons
|
||||
@ -8,43 +9,25 @@ import com.abdownloadmanager.desktop.ui.widget.ExpandableItem
|
||||
import com.abdownloadmanager.utils.compose.WithContentAlpha
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.onClick
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import com.abdownloadmanager.desktop.ui.widget.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.input.pointer.PointerButton
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.abdownloadmanager.utils.category.Category
|
||||
import com.abdownloadmanager.utils.category.rememberIconPainter
|
||||
import ir.amirab.downloader.downloaditem.DownloadStatus
|
||||
import ir.amirab.downloader.monitor.IDownloadItemState
|
||||
import ir.amirab.downloader.monitor.statusOrFinished
|
||||
|
||||
abstract class DownloadTypeCategoryFilter(
|
||||
val name: String,
|
||||
val icon: IconSource,
|
||||
) {
|
||||
abstract fun accept(iDownloadStatus: IDownloadItemState): Boolean
|
||||
}
|
||||
|
||||
class DownloadTypeCategoryFilterByList(
|
||||
name: String,
|
||||
icon: IconSource,
|
||||
acceptedTypes: List<String>,
|
||||
) : DownloadTypeCategoryFilter(name,icon) {
|
||||
val acceptedTypes = acceptedTypes.map { it.lowercase() }
|
||||
override fun accept(iDownloadStatus: IDownloadItemState): Boolean {
|
||||
val extension = iDownloadStatus.name
|
||||
.split(".")
|
||||
.lastOrNull()
|
||||
?.lowercase() ?: return false
|
||||
return extension in acceptedTypes
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class DownloadStatusCategoryFilterByList(
|
||||
name: String,
|
||||
icon: IconSource,
|
||||
@ -91,61 +74,11 @@ object DefinedStatusCategories {
|
||||
)
|
||||
}
|
||||
|
||||
object DefinedTypeCategories {
|
||||
fun values() = listOf(
|
||||
Image, Music, Video, App, Document, Compressed, Other
|
||||
)
|
||||
|
||||
fun resolveCategoryForDownloadItem(item: IDownloadItemState): DownloadTypeCategoryFilter {
|
||||
return values().first {
|
||||
it.accept(item)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val Image = DownloadTypeCategoryFilterByList(
|
||||
"Image",
|
||||
MyIcons.pictureFile,
|
||||
listOf("png", "jpg", "jpeg", "gif", "svg")
|
||||
)
|
||||
val Music = DownloadTypeCategoryFilterByList(
|
||||
"Music",
|
||||
MyIcons.musicFile,
|
||||
listOf("mp3")
|
||||
)
|
||||
val Video = DownloadTypeCategoryFilterByList(
|
||||
"Video",
|
||||
MyIcons.videoFile,
|
||||
listOf("mp4", "mkv", "3gp", "avi")
|
||||
)
|
||||
val App = DownloadTypeCategoryFilterByList(
|
||||
"Apps",
|
||||
MyIcons.applicationFile,
|
||||
listOf("apk", "deb", "exe", "msi", "jar")
|
||||
)
|
||||
val Document = DownloadTypeCategoryFilterByList(
|
||||
"Document",
|
||||
MyIcons.documentFile,
|
||||
listOf("txt", "docx", "pdf")
|
||||
)
|
||||
val Compressed = DownloadTypeCategoryFilterByList(
|
||||
"Compressed",
|
||||
MyIcons.zipFile,
|
||||
listOf("zip", "rar", "tz")
|
||||
)
|
||||
val Other = object : DownloadTypeCategoryFilter(
|
||||
"Other",
|
||||
MyIcons.otherFile,
|
||||
) {
|
||||
override fun accept(iDownloadStatus: IDownloadItemState): Boolean =true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun CategoryFilterItem(
|
||||
modifier: Modifier,
|
||||
category: DownloadTypeCategoryFilter,
|
||||
category: Category,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
@ -156,13 +89,13 @@ private fun CategoryFilterItem(
|
||||
onClick = onClick
|
||||
)
|
||||
.padding(start = 24.dp)
|
||||
.padding(horizontal = 4.dp,vertical = 6.dp)
|
||||
,
|
||||
.padding(horizontal = 4.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
WithContentAlpha(if (isSelected)1f else 0.75f){
|
||||
WithContentAlpha(if (isSelected) 1f else 0.75f) {
|
||||
val iconPainter = category.rememberIconPainter()
|
||||
MyIcon(
|
||||
category.icon,
|
||||
iconPainter ?: MyIcons.folder,
|
||||
null,
|
||||
Modifier.size(16.dp),
|
||||
)
|
||||
@ -182,17 +115,24 @@ private fun CategoryFilterItem(
|
||||
fun StatusFilterItem(
|
||||
isExpanded: Boolean,
|
||||
onRequestExpand: (Boolean) -> Unit,
|
||||
currentTypeCategoryFilter: DownloadTypeCategoryFilter?,
|
||||
currentTypeCategoryFilter: Category?,
|
||||
currentStatusCategoryFilter: DownloadStatusCategoryFilter?,
|
||||
statusFilter: DownloadStatusCategoryFilter,
|
||||
typeFilter: List<DownloadTypeCategoryFilter>,
|
||||
categories: List<Category>,
|
||||
onFilterChange: (
|
||||
typeFilter: DownloadTypeCategoryFilter?,
|
||||
typeFilter: Category?,
|
||||
) -> Unit,
|
||||
onRequestOpenOptionMenu: (Category?) -> Unit,
|
||||
) {
|
||||
val isStatusSelected = currentStatusCategoryFilter == statusFilter
|
||||
val isSelected = isStatusSelected && currentTypeCategoryFilter == null
|
||||
ExpandableItem(
|
||||
modifier = Modifier
|
||||
.onClick(
|
||||
matcher = PointerMatcher.mouse(PointerButton.Secondary),
|
||||
) {
|
||||
onRequestOpenOptionMenu(null)
|
||||
},
|
||||
isExpanded = isExpanded,
|
||||
header = {
|
||||
Row(
|
||||
@ -242,9 +182,13 @@ fun StatusFilterItem(
|
||||
},
|
||||
body = {
|
||||
Column(Modifier) {
|
||||
typeFilter.forEach {
|
||||
categories.forEach {
|
||||
CategoryFilterItem(
|
||||
modifier = Modifier,
|
||||
modifier = Modifier.onClick(
|
||||
matcher = PointerMatcher.mouse(PointerButton.Secondary),
|
||||
) {
|
||||
onRequestOpenOptionMenu(it)
|
||||
},
|
||||
category = it,
|
||||
isSelected = isStatusSelected && currentTypeCategoryFilter == it,
|
||||
onClick = {
|
||||
|
@ -26,6 +26,7 @@ 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.category.ShowCategoryDialogs
|
||||
import com.abdownloadmanager.desktop.pages.home.HomeWindow
|
||||
import com.abdownloadmanager.desktop.pages.settings.ThemeManager
|
||||
import com.abdownloadmanager.utils.compose.ProvideDebugInfo
|
||||
@ -78,6 +79,7 @@ object Ui : KoinComponent {
|
||||
}
|
||||
ShowAddDownloadDialogs(appComponent)
|
||||
ShowDownloadDialogs(appComponent)
|
||||
ShowCategoryDialogs(appComponent)
|
||||
//TODO Enable Updater
|
||||
//ShowUpdaterDialog(appComponent.updater)
|
||||
ShowAboutDialog(appComponent)
|
||||
|
@ -5,89 +5,89 @@ import ir.amirab.util.compose.IconSource
|
||||
import com.abdownloadmanager.utils.compose.asIconSource
|
||||
|
||||
object MyIcons : IMyIcons {
|
||||
override val appIcon: IconSource get() = "icons/app_icon.svg".asIconSource(false)
|
||||
override val appIcon: IconSource get() = "/icons/app_icon.svg".asIconSource(false)
|
||||
|
||||
override val settings get() = "icons/settings.svg".asIconSource()
|
||||
override val search get() = "icons/search.svg".asIconSource()
|
||||
override val info get() = "icons/info.svg".asIconSource()
|
||||
override val check get() = "icons/check.svg".asIconSource()
|
||||
override val link get() = "icons/add_link.svg".asIconSource()
|
||||
override val download get() = "icons/down_speed.svg".asIconSource()
|
||||
override val settings get() = "/icons/settings.svg".asIconSource()
|
||||
override val search get() = "/icons/search.svg".asIconSource()
|
||||
override val info get() = "/icons/info.svg".asIconSource()
|
||||
override val check get() = "/icons/check.svg".asIconSource()
|
||||
override val link get() = "/icons/add_link.svg".asIconSource()
|
||||
override val download get() = "/icons/down_speed.svg".asIconSource()
|
||||
|
||||
override val windowMinimize get() = "icons/window_minimize.svg".asIconSource()
|
||||
override val windowFloating get() = "icons/window_floating.svg".asIconSource()
|
||||
override val windowMaximize get() = "icons/window_maximize.svg".asIconSource()
|
||||
override val windowClose get() = "icons/window_close.svg".asIconSource()
|
||||
override val windowMinimize get() = "/icons/window_minimize.svg".asIconSource()
|
||||
override val windowFloating get() = "/icons/window_floating.svg".asIconSource()
|
||||
override val windowMaximize get() = "/icons/window_maximize.svg".asIconSource()
|
||||
override val windowClose get() = "/icons/window_close.svg".asIconSource()
|
||||
|
||||
override val exit get() = "icons/exit.svg".asIconSource()
|
||||
override val undo get() = "icons/undo.svg".asIconSource()
|
||||
override val exit get() = "/icons/exit.svg".asIconSource()
|
||||
override val undo get() = "/icons/undo.svg".asIconSource()
|
||||
|
||||
override val openSource: IconSource get() = "icons/open_source.svg".asIconSource()
|
||||
override val telegram: IconSource get() = "icons/telegram.svg".asIconSource(false)
|
||||
override val speaker: IconSource get() = "icons/speaker.svg".asIconSource()
|
||||
override val group: IconSource get() = "icons/group.svg".asIconSource()
|
||||
override val openSource: IconSource get() = "/icons/open_source.svg".asIconSource()
|
||||
override val telegram: IconSource get() = "/icons/telegram.svg".asIconSource(false)
|
||||
override val speaker: IconSource get() = "/icons/speaker.svg".asIconSource()
|
||||
override val group: IconSource get() = "/icons/group.svg".asIconSource()
|
||||
|
||||
|
||||
override val browserMozillaFirefox: IconSource get() = "icons/browser_mozilla_firefox.svg".asIconSource(false)
|
||||
override val browserGoogleChrome: IconSource get() = "icons/browser_google_chrome.svg".asIconSource(false)
|
||||
override val browserMicrosoftEdge: IconSource get() = "icons/browser_microsoft_edge.svg".asIconSource(false)
|
||||
override val browserOpera: IconSource get() = "icons/browser_opera.svg".asIconSource(false)
|
||||
override val browserMozillaFirefox: IconSource get() = "/icons/browser_mozilla_firefox.svg".asIconSource(false)
|
||||
override val browserGoogleChrome: IconSource get() = "/icons/browser_google_chrome.svg".asIconSource(false)
|
||||
override val browserMicrosoftEdge: IconSource get() = "/icons/browser_microsoft_edge.svg".asIconSource(false)
|
||||
override val browserOpera: IconSource get() = "/icons/browser_opera.svg".asIconSource(false)
|
||||
|
||||
// override val menu get() = TablerIcons.Menu.asIconSource()
|
||||
// override val menuClose get() = TablerIcons.X.asIconSource()
|
||||
|
||||
|
||||
override val next get() = "icons/next.svg".asIconSource()
|
||||
override val next get() = "/icons/next.svg".asIconSource()
|
||||
// override val back get() = TablerIcons.ChevronLeft.asIconSource()
|
||||
override val back get() = "icons/back.svg".asIconSource()
|
||||
override val up get() = "icons/up.svg".asIconSource()
|
||||
override val down get() = "icons/down.svg".asIconSource()
|
||||
override val back get() = "/icons/back.svg".asIconSource()
|
||||
override val up get() = "/icons/up.svg".asIconSource()
|
||||
override val down get() = "/icons/down.svg".asIconSource()
|
||||
|
||||
override val activeCount get() = "icons/list.svg".asIconSource()
|
||||
override val speed get() = "icons/down_speed.svg".asIconSource()
|
||||
override val activeCount get() = "/icons/list.svg".asIconSource()
|
||||
override val speed get() = "/icons/down_speed.svg".asIconSource()
|
||||
|
||||
|
||||
override val resume get() = "icons/resume.svg".asIconSource()
|
||||
override val pause get() = "icons/pause.svg".asIconSource()
|
||||
override val stop get() = "icons/stop.svg".asIconSource()
|
||||
override val resume get() = "/icons/resume.svg".asIconSource()
|
||||
override val pause get() = "/icons/pause.svg".asIconSource()
|
||||
override val stop get() = "/icons/stop.svg".asIconSource()
|
||||
|
||||
override val queue get() = "icons/queue.svg".asIconSource()
|
||||
override val queue get() = "/icons/queue.svg".asIconSource()
|
||||
|
||||
override val remove get() = "icons/delete.svg".asIconSource()
|
||||
override val clear get() = "icons/clear.svg".asIconSource()
|
||||
override val add get() = "icons/plus.svg".asIconSource()
|
||||
override val paste get() = "icons/clipboard.svg".asIconSource()
|
||||
override val remove get() = "/icons/delete.svg".asIconSource()
|
||||
override val clear get() = "/icons/clear.svg".asIconSource()
|
||||
override val add get() = "/icons/plus.svg".asIconSource()
|
||||
override val paste get() = "/icons/clipboard.svg".asIconSource()
|
||||
|
||||
override val copy get() = "icons/copy.svg".asIconSource()
|
||||
override val refresh get() = "icons/refresh.svg".asIconSource()
|
||||
override val editFolder get() = "icons/folder.svg".asIconSource()
|
||||
override val copy get() = "/icons/copy.svg".asIconSource()
|
||||
override val refresh get() = "/icons/refresh.svg".asIconSource()
|
||||
override val editFolder get() = "/icons/folder.svg".asIconSource()
|
||||
|
||||
override val share get() = "icons/share.svg".asIconSource()
|
||||
override val file get() = "icons/file.svg".asIconSource()
|
||||
override val folder get() = "icons/folder.svg".asIconSource()
|
||||
override val share get() = "/icons/share.svg".asIconSource()
|
||||
override val file get() = "/icons/file.svg".asIconSource()
|
||||
override val folder get() = "/icons/folder.svg".asIconSource()
|
||||
|
||||
override val fileOpen get() = file
|
||||
override val folderOpen get() = folder
|
||||
override val pictureFile get() = "icons/file_picture.svg".asIconSource()
|
||||
override val musicFile get() = "icons/file_music.svg".asIconSource()
|
||||
override val zipFile get() = "icons/file_zip.svg".asIconSource()
|
||||
override val videoFile get() = "icons/file_video.svg".asIconSource()
|
||||
override val applicationFile get() = "icons/file_application.svg".asIconSource()
|
||||
override val documentFile get() = "icons/file_document.svg".asIconSource()
|
||||
override val otherFile get() = "icons/file_unknown.svg".asIconSource()
|
||||
override val pictureFile get() = "/icons/file_picture.svg".asIconSource()
|
||||
override val musicFile get() = "/icons/file_music.svg".asIconSource()
|
||||
override val zipFile get() = "/icons/file_zip.svg".asIconSource()
|
||||
override val videoFile get() = "/icons/file_video.svg".asIconSource()
|
||||
override val applicationFile get() = "/icons/file_application.svg".asIconSource()
|
||||
override val documentFile get() = "/icons/file_document.svg".asIconSource()
|
||||
override val otherFile get() = "/icons/file_unknown.svg".asIconSource()
|
||||
|
||||
override val lock get() = "icons/lock.svg".asIconSource()
|
||||
override val lock get() = "/icons/lock.svg".asIconSource()
|
||||
|
||||
override val question get() = "icons/question_mark.svg".asIconSource()
|
||||
override val question get() = "/icons/question_mark.svg".asIconSource()
|
||||
|
||||
override val sortUp get() = "icons/sort_321.svg".asIconSource()
|
||||
override val sortDown get() = "icons/sort_123.svg".asIconSource()
|
||||
override val verticalDirection get() = "icons/vertical_direction.svg".asIconSource()
|
||||
override val sortUp get() = "/icons/sort_321.svg".asIconSource()
|
||||
override val sortDown get() = "/icons/sort_123.svg".asIconSource()
|
||||
override val verticalDirection get() = "/icons/vertical_direction.svg".asIconSource()
|
||||
|
||||
override val browserIntegration: IconSource get() = "icons/earth.svg".asIconSource()
|
||||
override val appearance: IconSource get() = "icons/color.svg".asIconSource()
|
||||
override val downloadEngine: IconSource get() = "icons/down_speed.svg".asIconSource()
|
||||
override val network: IconSource get() = "icons/network.svg".asIconSource()
|
||||
override val browserIntegration: IconSource get() = "/icons/earth.svg".asIconSource()
|
||||
override val appearance: IconSource get() = "/icons/color.svg".asIconSource()
|
||||
override val downloadEngine: IconSource get() = "/icons/down_speed.svg".asIconSource()
|
||||
override val network: IconSource get() = "/icons/network.svg".asIconSource()
|
||||
|
||||
override val externalLink: IconSource get() = "icons/external_link.svg".asIconSource()
|
||||
override val externalLink: IconSource get() = "/icons/external_link.svg".asIconSource()
|
||||
}
|
||||
|
@ -3,14 +3,16 @@ package com.abdownloadmanager.desktop.ui.widget
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun ExpandableItem(
|
||||
isExpanded:Boolean,
|
||||
header:@Composable ()->Unit,
|
||||
body:@Composable ()->Unit
|
||||
body: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
){
|
||||
Column {
|
||||
Column(modifier) {
|
||||
header()
|
||||
AnimatedVisibility(isExpanded){
|
||||
body()
|
||||
|
@ -0,0 +1,82 @@
|
||||
package com.abdownloadmanager.desktop.ui.widget
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.rememberComponentRectPositionProvider
|
||||
import com.abdownloadmanager.desktop.pages.settings.configurable.Configurable
|
||||
import com.abdownloadmanager.desktop.ui.icon.MyIcons
|
||||
import com.abdownloadmanager.desktop.ui.theme.myColors
|
||||
import com.abdownloadmanager.desktop.ui.theme.myTextSizes
|
||||
import com.abdownloadmanager.utils.compose.WithContentColor
|
||||
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
||||
|
||||
@Composable
|
||||
fun Help(
|
||||
content: String,
|
||||
) {
|
||||
var showHelpContent by remember { mutableStateOf(false) }
|
||||
val onRequestCloseShowHelpContent = {
|
||||
showHelpContent = false
|
||||
}
|
||||
Column {
|
||||
MyIcon(
|
||||
MyIcons.question,
|
||||
"Hint",
|
||||
Modifier
|
||||
.clip(CircleShape)
|
||||
.clickable {
|
||||
showHelpContent = !showHelpContent
|
||||
}
|
||||
.border(
|
||||
1.dp,
|
||||
if (showHelpContent) myColors.primary
|
||||
else Color.Transparent,
|
||||
CircleShape
|
||||
)
|
||||
.background(myColors.surface)
|
||||
.padding(4.dp)
|
||||
.size(12.dp),
|
||||
tint = myColors.onSurface,
|
||||
)
|
||||
if (showHelpContent) {
|
||||
Popup(
|
||||
popupPositionProvider = rememberComponentRectPositionProvider(
|
||||
anchor = Alignment.TopCenter,
|
||||
alignment = Alignment.TopCenter,
|
||||
),
|
||||
onDismissRequest = onRequestCloseShowHelpContent
|
||||
) {
|
||||
val shape = RoundedCornerShape(6.dp)
|
||||
Box(
|
||||
Modifier
|
||||
.padding(vertical = 4.dp)
|
||||
.widthIn(max = 240.dp)
|
||||
.shadow(24.dp)
|
||||
.clip(shape)
|
||||
.border(1.dp, myColors.surface, shape)
|
||||
.background(myColors.menuGradientBackground)
|
||||
.padding(8.dp)
|
||||
) {
|
||||
WithContentColor(myColors.onSurface) {
|
||||
Text(
|
||||
content,
|
||||
fontSize = myTextSizes.base,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
package com.abdownloadmanager.desktop.ui.widget
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.abdownloadmanager.desktop.ui.theme.myColors
|
||||
import com.abdownloadmanager.desktop.ui.util.ifThen
|
||||
import com.abdownloadmanager.desktop.ui.widget.menu.MyDropDown
|
||||
import com.abdownloadmanager.desktop.utils.div
|
||||
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
||||
import ir.amirab.util.compose.IconSource
|
||||
|
||||
@Composable
|
||||
fun IconPick(
|
||||
selectedIcon: IconSource?,
|
||||
icons: List<IconSource>,
|
||||
onSelected: (IconSource) -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
) {
|
||||
MyDropDown(
|
||||
onDismissRequest = onCancel,
|
||||
offset = DpOffset(y = 2.dp, x = 0.dp),
|
||||
content = {
|
||||
val shape = RoundedCornerShape(6.dp)
|
||||
Box(
|
||||
Modifier
|
||||
.shadow(24.dp)
|
||||
// .verticalScroll(rememberScrollState())
|
||||
.clip(shape)
|
||||
// .width(IntrinsicSize.Max)
|
||||
.widthIn(120.dp)
|
||||
.height(220.dp)
|
||||
.border(1.dp, myColors.surface, shape)
|
||||
.background(myColors.menuGradientBackground)
|
||||
|
||||
) {
|
||||
Content(
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 0.dp),
|
||||
selectedIcon = selectedIcon,
|
||||
icons = icons,
|
||||
onSelected = onSelected,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(
|
||||
modifier: Modifier,
|
||||
selectedIcon: IconSource?,
|
||||
icons: List<IconSource>,
|
||||
onSelected: (IconSource) -> Unit,
|
||||
) {
|
||||
val state = rememberLazyListState()
|
||||
Box {
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
content = {
|
||||
val shape = RoundedCornerShape(6.dp)
|
||||
items(icons.chunked(6)) { rowItems ->
|
||||
Row {
|
||||
for (iconSource in rowItems) {
|
||||
val isSelected = selectedIcon == iconSource
|
||||
MyIcon(
|
||||
iconSource,
|
||||
null,
|
||||
Modifier
|
||||
.clip(shape)
|
||||
.ifThen(isSelected) {
|
||||
background(myColors.primary / 0.25f)
|
||||
}
|
||||
.border(
|
||||
1.dp,
|
||||
if (isSelected) myColors.primary / 0.25f
|
||||
else Color.Transparent,
|
||||
shape
|
||||
)
|
||||
.clickable {
|
||||
onSelected(iconSource)
|
||||
}
|
||||
.padding(8.dp)
|
||||
.size(24.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// LazyVerticalGrid(
|
||||
// columns = GridCells.Fixed(6),
|
||||
// content = {
|
||||
// val shape = RoundedCornerShape(6.dp)
|
||||
// items(icons) {
|
||||
// MyIcon(
|
||||
// it,
|
||||
// null,
|
||||
// Modifier
|
||||
// .clip(shape)
|
||||
// .ifThen(selectedIcon == it) {
|
||||
// background(myColors.primary / 0.25f)
|
||||
// }
|
||||
// .clickable {
|
||||
// onSelected(it)
|
||||
// }
|
||||
// .padding(8.dp)
|
||||
// .size(24.dp),
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
}
|
||||
)
|
||||
AnimatedVisibility(
|
||||
state.canScrollForward,
|
||||
modifier = Modifier.matchParentSize(),
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Spacer(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colorStops = arrayOf(
|
||||
0f to Color.Transparent,
|
||||
0.8f to Color.Transparent,
|
||||
1f to myColors.background,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -57,7 +57,9 @@ fun MyTextField(
|
||||
enabled: Boolean = true,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
|
||||
singleLine: Boolean = true,
|
||||
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||
minLines: Int = 1,
|
||||
start: @Composable (RowScope.() -> Unit)? = null,
|
||||
end: @Composable (RowScope.() -> Unit)? = null,
|
||||
) {
|
||||
@ -98,7 +100,9 @@ fun MyTextField(
|
||||
|
||||
BasicTextField(
|
||||
value = text,
|
||||
singleLine = true,
|
||||
singleLine = singleLine,
|
||||
maxLines = maxLines,
|
||||
minLines = minLines,
|
||||
onValueChange = onTextChange,
|
||||
interactionSource = interactionSource,
|
||||
enabled = enabled,
|
||||
|
@ -1,5 +1,7 @@
|
||||
package com.abdownloadmanager.desktop.utils
|
||||
|
||||
import com.abdownloadmanager.utils.category.CategoryManager
|
||||
import com.abdownloadmanager.utils.category.CategorySelectionMode
|
||||
import ir.amirab.downloader.DownloadManager
|
||||
import ir.amirab.downloader.db.IDownloadListDb
|
||||
import ir.amirab.downloader.downloaditem.DownloadItem
|
||||
@ -26,6 +28,7 @@ import java.io.File
|
||||
class DownloadSystem(
|
||||
val downloadManager: DownloadManager,
|
||||
val queueManager: QueueManager,
|
||||
val categoryManager: CategoryManager,
|
||||
val downloadMonitor: IDownloadMonitor,
|
||||
private val scope: CoroutineScope,
|
||||
private val downloadListDB: IDownloadListDb,
|
||||
@ -40,6 +43,7 @@ class DownloadSystem(
|
||||
foldersRegistry.boot()
|
||||
queueManager.boot()
|
||||
downloadManager.boot()
|
||||
categoryManager.boot()
|
||||
booted.update { true }
|
||||
}
|
||||
|
||||
@ -47,33 +51,62 @@ class DownloadSystem(
|
||||
newItemsToAdd: List<DownloadItem>,
|
||||
onDuplicateStrategy: (DownloadItem) -> OnDuplicateStrategy,
|
||||
queueId: Long? = null,
|
||||
categorySelectionMode: CategorySelectionMode? = null,
|
||||
): List<Long> {
|
||||
return newItemsToAdd.map {
|
||||
val createdIds = newItemsToAdd.map {
|
||||
downloadManager.addDownload(it, onDuplicateStrategy(it))
|
||||
}.also { ids ->
|
||||
}
|
||||
createdIds.also { ids ->
|
||||
queueId?.let {
|
||||
queueManager.addToQueue(
|
||||
it, ids
|
||||
)
|
||||
}
|
||||
}
|
||||
categorySelectionMode?.let {
|
||||
when (it) {
|
||||
CategorySelectionMode.Auto -> {
|
||||
categoryManager.autoAddItemsToCategoriesBasedOnFileNames(
|
||||
createdIds.mapIndexed { index: Int, id: Long ->
|
||||
id to newItemsToAdd.get(index).name
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
is CategorySelectionMode.Fixed -> {
|
||||
categoryManager.addItemsToCategory(
|
||||
it.categoryId,
|
||||
createdIds,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return createdIds
|
||||
}
|
||||
|
||||
suspend fun addDownload(
|
||||
downloadItem: DownloadItem,
|
||||
onDuplicateStrategy: OnDuplicateStrategy,
|
||||
queueId: Long?,
|
||||
categoryId: Long?,
|
||||
context: DownloadItemContext = EmptyContext,
|
||||
): Long {
|
||||
val downloadId = downloadManager.addDownload(downloadItem, onDuplicateStrategy, context)
|
||||
queueId?.let {
|
||||
queueManager.addToQueue(queueId, downloadId)
|
||||
}
|
||||
categoryId?.let {
|
||||
categoryManager.addItemsToCategory(
|
||||
categoryId = categoryId,
|
||||
itemIds = listOf(downloadId)
|
||||
)
|
||||
}
|
||||
return downloadId
|
||||
}
|
||||
|
||||
suspend fun removeDownload(id: Long, alsoRemoveFile: Boolean) {
|
||||
downloadManager.deleteDownload(id, alsoRemoveFile,RemovedBy(User))
|
||||
categoryManager.removeItemInCategories(listOf(id))
|
||||
}
|
||||
|
||||
suspend fun manualResume(id: Long): Boolean {
|
||||
@ -144,7 +177,12 @@ class DownloadSystem(
|
||||
val id = items.sortedByDescending { it.dateAdded }.first().id
|
||||
return id
|
||||
}
|
||||
val id = addDownload(downloadItem, OnDuplicateStrategy.AddNumbered, null)
|
||||
val id = addDownload(
|
||||
downloadItem = downloadItem,
|
||||
onDuplicateStrategy = OnDuplicateStrategy.AddNumbered,
|
||||
queueId = null,
|
||||
categoryId = null,
|
||||
)
|
||||
return id
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
plugins {
|
||||
id(MyPlugins.kotlin)
|
||||
id(MyPlugins.composeBase)
|
||||
id(Plugins.Kotlin.serialization)
|
||||
}
|
||||
dependencies {
|
||||
implementation(project(":downloader:core"))
|
||||
implementation(project(":downloader:monitor"))
|
||||
api(project(":shared:config"))
|
||||
api(project(":shared:utils"))
|
||||
api(project(":shared:compose-utils"))
|
||||
|
@ -0,0 +1,62 @@
|
||||
package com.abdownloadmanager.utils
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import com.abdownloadmanager.utils.category.CategoryManager
|
||||
import com.abdownloadmanager.utils.category.DefaultCategories
|
||||
import com.abdownloadmanager.utils.category.iconSource
|
||||
import com.abdownloadmanager.utils.compose.IMyIcons
|
||||
import ir.amirab.util.compose.IconSource
|
||||
|
||||
|
||||
interface FileIconProvider {
|
||||
fun getIcon(fileName: String): IconSource
|
||||
|
||||
/**
|
||||
* Automatically update icon if other dependencies changed
|
||||
*/
|
||||
@Composable
|
||||
fun rememberIcon(fileName: String): IconSource
|
||||
}
|
||||
|
||||
class FileIconProviderUsingCategoryIcons(
|
||||
private val defaultCategories: DefaultCategories,
|
||||
private val categoryManager: CategoryManager,
|
||||
private val icons: IMyIcons,
|
||||
) : FileIconProvider {
|
||||
override fun getIcon(fileName: String): IconSource {
|
||||
return fromDefaultCategories(fileName)
|
||||
?: fromUserDefinedCategories(fileName)
|
||||
?: icons.file
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun rememberIcon(fileName: String): IconSource {
|
||||
val fromDefault = remember(fileName) {
|
||||
fromDefaultCategories(fileName)
|
||||
}
|
||||
if (fromDefault != null) {
|
||||
return fromDefault
|
||||
}
|
||||
val categories by categoryManager.categoriesFlow.collectAsState()
|
||||
val fromCategories = remember(fileName, categories) {
|
||||
fromUserDefinedCategories(fileName)
|
||||
}
|
||||
if (fromCategories != null) {
|
||||
return fromCategories
|
||||
}
|
||||
return icons.file
|
||||
}
|
||||
|
||||
private fun fromDefaultCategories(fileName: String): IconSource? {
|
||||
return defaultCategories
|
||||
.getCategoryOfFileName(fileName)?.iconSource()
|
||||
}
|
||||
|
||||
private fun fromUserDefinedCategories(fileName: String): IconSource? {
|
||||
return categoryManager
|
||||
.getCategoryOfFileName(fileName)?.iconSource()
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package com.abdownloadmanager.utils.category
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.remember
|
||||
import ir.amirab.util.compose.IconSource
|
||||
import ir.amirab.util.compose.fromUri
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* @param path
|
||||
* this is a default download path for this category
|
||||
* @param icon
|
||||
* can be used by [IconSource]
|
||||
*/
|
||||
@Immutable
|
||||
@Serializable
|
||||
data class Category(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val path: String,
|
||||
val acceptedFileTypes: List<String>,
|
||||
val icon: String,
|
||||
val items: List<Long>,
|
||||
) {
|
||||
fun acceptFileName(fileName: String): Boolean {
|
||||
return acceptedFileTypes.any { ext ->
|
||||
fileName.endsWith(
|
||||
suffix = ".$ext",
|
||||
ignoreCase = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun withExtraItems(newItems: List<Long>): Category {
|
||||
return copy(
|
||||
items = items.plus(newItems).distinct()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun Category.iconSource(): IconSource? {
|
||||
return IconSource.fromUri(icon)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Category.rememberIconPainter(): IconSource? {
|
||||
return remember(icon) {
|
||||
iconSource()
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package com.abdownloadmanager.utils.category
|
||||
|
||||
import ir.amirab.downloader.db.TransactionalFileSaver
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.io.File
|
||||
|
||||
class CategoryFileStorage(
|
||||
val file: File,
|
||||
val fileSaver: TransactionalFileSaver,
|
||||
) : CategoryStorage {
|
||||
val lock = Mutex()
|
||||
override suspend fun setCategories(categories: List<Category>) {
|
||||
lock.withLock {
|
||||
fileSaver.writeObject(file, categories)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getCategories(): List<Category> {
|
||||
return fileSaver.readObject(file) ?: emptyList()
|
||||
}
|
||||
|
||||
override suspend fun isCategoriesSet(): Boolean {
|
||||
return file.exists()
|
||||
}
|
||||
}
|
@ -0,0 +1,221 @@
|
||||
package com.abdownloadmanager.utils.category
|
||||
|
||||
import ir.amirab.downloader.DownloadManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
class CategoryManager(
|
||||
private val categoryStorage: CategoryStorage,
|
||||
private val scope: CoroutineScope,
|
||||
private val defaultCategoriesFactory: DefaultCategories,
|
||||
private val downloadManager: DownloadManager,
|
||||
) {
|
||||
private val _categories = MutableStateFlow<List<Category>>(emptyList())
|
||||
val categoriesFlow = _categories.asStateFlow()
|
||||
|
||||
private var booted = false
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
suspend fun boot() {
|
||||
synchronized(this) {
|
||||
if (booted) return
|
||||
}
|
||||
if (categoryStorage.isCategoriesSet()) {
|
||||
_categories.value = categoryStorage
|
||||
.getCategories()
|
||||
} else {
|
||||
reset()
|
||||
}
|
||||
_categories
|
||||
.sample(500)
|
||||
.onEach { categoryStorage.setCategories(it) }
|
||||
.launchIn(scope)
|
||||
booted = true
|
||||
}
|
||||
|
||||
suspend fun reset() {
|
||||
val newCategories = defaultCategoriesFactory.getDefaultCategories()
|
||||
setCategories(newCategories)
|
||||
withContext(Dispatchers.IO) {
|
||||
newCategories.forEach {
|
||||
prepareCategory(it)
|
||||
}
|
||||
autoAddItemsToCategoriesBasedOnFileNames(
|
||||
downloadManager
|
||||
.getDownloadList()
|
||||
.map { it.id to it.name }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getCategories(): List<Category> {
|
||||
return _categories.value
|
||||
}
|
||||
|
||||
fun setCategories(categories: List<Category>) {
|
||||
_categories.update { categories }
|
||||
}
|
||||
|
||||
fun getCategoryById(id: Long): Category? {
|
||||
return getCategories()
|
||||
.firstOrNull { it.id == id }
|
||||
}
|
||||
|
||||
fun getCategoryOfType(extension: String): Category? {
|
||||
return getCategories().firstOrNull { c ->
|
||||
c.acceptedFileTypes.any {
|
||||
it.equals(extension, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getCategoryOfFileName(fileName: String): Category? {
|
||||
return getCategories()
|
||||
.firstOrNull {
|
||||
it.acceptFileName(fileName)
|
||||
}
|
||||
}
|
||||
|
||||
fun getCategoryOfItem(id: Long): Category? {
|
||||
return getCategories()
|
||||
.firstOrNull {
|
||||
it.items.contains(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteCategory(category: Category) {
|
||||
deleteCategory(category.id)
|
||||
}
|
||||
|
||||
fun deleteCategory(categoryId: Long) {
|
||||
_categories.update {
|
||||
it.filter {
|
||||
it.id != categoryId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addCustomCategory(category: Category) {
|
||||
require(category.id == -1L)
|
||||
val categories = getCategories()
|
||||
val newId = (
|
||||
categories
|
||||
.maxOfOrNull { it.id }
|
||||
?.coerceAtLeast(DEFAULT_CATEGORY_END_ID)
|
||||
?: DEFAULT_CATEGORY_END_ID
|
||||
) + 1
|
||||
val newCategory = category.copy(
|
||||
id = newId
|
||||
)
|
||||
setCategories(
|
||||
categories.plus(
|
||||
newCategory
|
||||
)
|
||||
)
|
||||
prepareCategory(newCategory)
|
||||
}
|
||||
|
||||
fun createDirectoryIfNecessary(category: Category) {
|
||||
kotlin.runCatching {
|
||||
val folder = File(category.path)
|
||||
if (folder.exists()) {
|
||||
folder.mkdirs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareCategory(newCategory: Category) {
|
||||
createDirectoryIfNecessary(newCategory)
|
||||
}
|
||||
|
||||
fun updateCategory(categoryToUpdate: Category) {
|
||||
_categories.update {
|
||||
it.updatedItem(
|
||||
categoryId = categoryToUpdate.id,
|
||||
update = { categoryToUpdate }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCategory(id: Long, categoryToUpdate: (Category) -> Category) {
|
||||
_categories.update {
|
||||
it.updatedItem(id, categoryToUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun addItemsToCategory(categoryId: Long, itemIds: List<Long>) {
|
||||
_categories.update { previousCategories ->
|
||||
previousCategories
|
||||
.removedItemIds(itemIds)
|
||||
.updatedItem(categoryId) {
|
||||
it.withExtraItems(itemIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeItemInCategories(idsToRemove: List<Long>) {
|
||||
_categories.update {
|
||||
it.removedItemIds(idsToRemove)
|
||||
}
|
||||
}
|
||||
|
||||
fun isDefaultCategory(category: Category): Boolean {
|
||||
return category.id in 0..DEFAULT_CATEGORY_END_ID
|
||||
}
|
||||
|
||||
fun autoAddItemsToCategoriesBasedOnFileNames(
|
||||
unCategorizedItems: List<Pair<Long, String>>,
|
||||
) {
|
||||
val newItemsMap = mutableMapOf<Long, MutableList<Long>>()
|
||||
var count = 0
|
||||
for ((id, name) in unCategorizedItems) {
|
||||
val categoryToUpdate = getCategoryOfFileName(name) ?: continue
|
||||
newItemsMap
|
||||
.getOrPut(categoryToUpdate.id) { mutableListOf() }
|
||||
.add(id)
|
||||
count++
|
||||
}
|
||||
for ((categoryId, itemsToAdd) in newItemsMap) {
|
||||
updateCategory(categoryId) {
|
||||
it.withExtraItems(itemsToAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isThisPathBelongsToACategory(folder: String): Boolean {
|
||||
return getCategories()
|
||||
.map { it.path }.contains(folder)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Reserved ids for default categories
|
||||
* this is too big BTW as we only use 5 for now
|
||||
* maybe we need more or extra hidden categories that users can enable (maybe ?)
|
||||
*/
|
||||
const val DEFAULT_CATEGORY_END_ID = 100L
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Category>.removedItemIds(itemIds: List<Long>): List<Category> {
|
||||
return map {
|
||||
it.copy(
|
||||
items = it.items.filter { itemId ->
|
||||
itemId !in itemIds
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun List<Category>.updatedItem(categoryId: Long, update: (Category) -> Category): List<Category> {
|
||||
return map {
|
||||
if (it.id == categoryId) {
|
||||
update(it)
|
||||
} else it
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.abdownloadmanager.utils.category
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
|
||||
@Composable
|
||||
fun CategoryManager.rememberCategoryOf(
|
||||
itemId: Long,
|
||||
): Category? {
|
||||
val categories by categoriesFlow.collectAsState()
|
||||
return remember(itemId, categories) {
|
||||
categories.firstOrNull {
|
||||
it.items.contains(itemId)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.abdownloadmanager.utils.category
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
sealed interface CategorySelectionMode {
|
||||
data class Fixed(val categoryId: Long) : CategorySelectionMode
|
||||
data object Auto : CategorySelectionMode
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.abdownloadmanager.utils.category
|
||||
|
||||
interface CategoryStorage {
|
||||
suspend fun setCategories(categories: List<Category>)
|
||||
suspend fun getCategories(): List<Category>
|
||||
suspend fun isCategoriesSet(): Boolean
|
||||
}
|
@ -0,0 +1,165 @@
|
||||
package com.abdownloadmanager.utils.category
|
||||
|
||||
import com.abdownloadmanager.utils.compose.IMyIcons
|
||||
import ir.amirab.util.compose.IconSource
|
||||
import ir.amirab.util.compose.uriOrNull
|
||||
import java.io.File
|
||||
|
||||
class DefaultCategories(
|
||||
private val icons: IMyIcons,
|
||||
private val getDefaultDownloadFolder: () -> String,
|
||||
) {
|
||||
|
||||
fun getCategoryOfFileName(name: String): Category? {
|
||||
return getDefaultCategories()
|
||||
.firstOrNull { it.acceptFileName(name) }
|
||||
}
|
||||
|
||||
fun getDefaultCategories(): List<Category> {
|
||||
fun IconSource.toUri(): String {
|
||||
return requireNotNull(uriOrNull()) {
|
||||
"It seems that we use an icon that does not have uri"
|
||||
}
|
||||
}
|
||||
|
||||
fun relative(path: String): String {
|
||||
return File(getDefaultDownloadFolder(), path).path
|
||||
}
|
||||
|
||||
val compressed = Category(
|
||||
id = 0,
|
||||
name = "Compressed",
|
||||
path = relative("Compressed"),
|
||||
icon = icons.zipFile.toUri(),
|
||||
acceptedFileTypes = listOf(
|
||||
"zip",
|
||||
"rar",
|
||||
"7z",
|
||||
"tar",
|
||||
"gz",
|
||||
"bz2",
|
||||
"xz",
|
||||
"iso",
|
||||
"dmg",
|
||||
"tgz",
|
||||
),
|
||||
items = emptyList(),
|
||||
)
|
||||
|
||||
val programs = Category(
|
||||
id = 1,
|
||||
name = "Programs",
|
||||
path = relative("Programs"),
|
||||
icon = icons.applicationFile.toUri(),
|
||||
acceptedFileTypes = listOf(
|
||||
"apk",
|
||||
"exe",
|
||||
"msi",
|
||||
"bat",
|
||||
"sh",
|
||||
"jar",
|
||||
"app",
|
||||
"deb",
|
||||
"rpm",
|
||||
"bin",
|
||||
),
|
||||
items = emptyList(),
|
||||
)
|
||||
val videos = Category(
|
||||
id = 2,
|
||||
name = "Videos",
|
||||
path = relative("Videos"),
|
||||
icon = icons.videoFile.toUri(),
|
||||
acceptedFileTypes = listOf(
|
||||
"mp4",
|
||||
"avi",
|
||||
"mkv",
|
||||
"mov",
|
||||
"wmv",
|
||||
"flv",
|
||||
"webm",
|
||||
"m4v",
|
||||
"3gp",
|
||||
"mpeg",
|
||||
),
|
||||
items = emptyList(),
|
||||
)
|
||||
|
||||
val music = Category(
|
||||
id = 3,
|
||||
name = "Music",
|
||||
path = relative("Music"),
|
||||
icon = icons.musicFile.toUri(),
|
||||
acceptedFileTypes = listOf(
|
||||
"mp3",
|
||||
"wav",
|
||||
"aac",
|
||||
"flac",
|
||||
"ogg",
|
||||
"aiff",
|
||||
"wma",
|
||||
"m4a",
|
||||
),
|
||||
items = emptyList(),
|
||||
)
|
||||
|
||||
val pictures = Category(
|
||||
id = 4,
|
||||
name = "Pictures",
|
||||
path = relative("Pictures"),
|
||||
icon = icons.pictureFile.toUri(),
|
||||
acceptedFileTypes = listOf(
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"png",
|
||||
"gif",
|
||||
"bmp",
|
||||
"tiff",
|
||||
"tif",
|
||||
"svg",
|
||||
"webp",
|
||||
"heic",
|
||||
"ico",
|
||||
"raw",
|
||||
"psd",
|
||||
),
|
||||
items = emptyList(),
|
||||
)
|
||||
val documents = Category(
|
||||
id = 5,
|
||||
name = "Documents",
|
||||
path = relative("Documents"),
|
||||
icon = icons.documentFile.toUri(),
|
||||
acceptedFileTypes = listOf(
|
||||
"doc",
|
||||
"docx",
|
||||
"pdf",
|
||||
"txt",
|
||||
"rtf",
|
||||
"odt",
|
||||
"xls",
|
||||
"xlsx",
|
||||
"ppt",
|
||||
"pptx",
|
||||
"csv",
|
||||
"epub",
|
||||
"pages",
|
||||
),
|
||||
items = emptyList(),
|
||||
)
|
||||
return listOf(
|
||||
compressed,
|
||||
programs,
|
||||
videos,
|
||||
music,
|
||||
pictures,
|
||||
documents,
|
||||
)
|
||||
}
|
||||
|
||||
fun isDefault(categories: List<Category>): Boolean {
|
||||
return getDefaultCategories() == categories.map {
|
||||
it.copy(items = emptyList())
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package com.abdownloadmanager.utils.category
|
||||
|
||||
class InMemoryCategoryStorage : CategoryStorage {
|
||||
private var categories = emptyList<Category>()
|
||||
|
||||
override suspend fun setCategories(categories: List<Category>) {
|
||||
this.categories = categories
|
||||
}
|
||||
|
||||
override suspend fun getCategories(): List<Category> {
|
||||
return categories
|
||||
}
|
||||
|
||||
override suspend fun isCategoriesSet(): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@ context (IMyIcons)
|
||||
fun ImageVector.asIconSource(requiredTint: Boolean = true) = IconSource.VectorIconSource(this, requiredTint)
|
||||
|
||||
context (IMyIcons)
|
||||
fun String.asIconSource(requiredTint: Boolean = true) = IconSource.StorageIconSource(this, requiredTint)
|
||||
fun String.asIconSource(requiredTint: Boolean = true) = IconSource.ResourceIconSource(this, requiredTint)
|
||||
|
||||
interface IMyIcons {
|
||||
val appIcon: IconSource
|
||||
@ -71,7 +71,7 @@ interface IMyIcons {
|
||||
val question: IconSource
|
||||
val sortUp: IconSource
|
||||
val sortDown: IconSource
|
||||
val verticalDirection: IconSource.StorageIconSource
|
||||
val verticalDirection: IconSource
|
||||
val appearance: IconSource
|
||||
val downloadEngine: IconSource
|
||||
val browserIntegration: IconSource
|
||||
|
@ -5,7 +5,13 @@ import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.res.ClassLoaderResourceLoader
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import okio.FileSystem
|
||||
import okio.Path.Companion.toPath
|
||||
import java.net.URI
|
||||
|
||||
private const val RESOURCE_PROTOCOL = "app-resource"
|
||||
|
||||
@Immutable
|
||||
sealed interface IconSource {
|
||||
@ -16,12 +22,16 @@ sealed interface IconSource {
|
||||
fun rememberPainter(): Painter
|
||||
|
||||
@Immutable
|
||||
data class StorageIconSource(
|
||||
data class ResourceIconSource(
|
||||
override val value: String,
|
||||
override val requiredTint: Boolean,
|
||||
) : IconSource {
|
||||
) : IconSourceWithURI {
|
||||
@Composable
|
||||
override fun rememberPainter(): Painter = painterResource(value)
|
||||
override fun toUri() = "$RESOURCE_PROTOCOL:$value?tint=${requiredTint}"
|
||||
override fun exists(): Boolean {
|
||||
return FileSystem.RESOURCES.exists(value.toPath())
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@ -32,4 +42,30 @@ sealed interface IconSource {
|
||||
@Composable
|
||||
override fun rememberPainter(): Painter = rememberVectorPainter(value)
|
||||
}
|
||||
|
||||
companion object
|
||||
}
|
||||
|
||||
interface IconSourceWithURI : IconSource {
|
||||
fun toUri(): String
|
||||
fun exists(): Boolean
|
||||
}
|
||||
|
||||
fun IconSource.uriOrNull() = (this as? IconSourceWithURI)?.toUri()
|
||||
|
||||
@Suppress("NAME_SHADOWING")
|
||||
fun IconSource.Companion.fromUri(uri: String): IconSourceWithURI? {
|
||||
val uri = URI(uri)
|
||||
return when (uri.scheme) {
|
||||
RESOURCE_PROTOCOL -> IconSource.ResourceIconSource(
|
||||
value = uri.path,
|
||||
// requiredTint = uri.query["tint"]?.toBooleanOrNull()?:true,
|
||||
requiredTint = true,
|
||||
)
|
||||
|
||||
else -> null
|
||||
// else -> kotlin.runCatching { uri.toURL() }
|
||||
// .getOrNull()
|
||||
// ?.openStream()
|
||||
}?.takeIf { it.exists() }
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user