mirror of
https://github.com/amir1376/ab-download-manager.git
synced 2025-02-20 11:43:24 +08:00
add localization
This commit is contained in:
parent
62a8bb73c4
commit
b14c2bb5ad
@ -3,6 +3,7 @@ import ir.amirab.util.platform.Platform
|
|||||||
object MyPlugins {
|
object MyPlugins {
|
||||||
private const val namespace = "myPlugins"
|
private const val namespace = "myPlugins"
|
||||||
const val kotlin = "$namespace.kotlin"
|
const val kotlin = "$namespace.kotlin"
|
||||||
|
const val kotlinMultiplatform = "$namespace.kotlinMultiplatform"
|
||||||
const val composeDesktop = "$namespace.composeDesktop"
|
const val composeDesktop = "$namespace.composeDesktop"
|
||||||
const val composeBase = "$namespace.composeBase"
|
const val composeBase = "$namespace.composeBase"
|
||||||
const val proguardDesktop = "$namespace.proguardDesktop"
|
const val proguardDesktop = "$namespace.proguardDesktop"
|
||||||
@ -22,4 +23,6 @@ object Plugins {
|
|||||||
const val changeLog = "org.jetbrains.changelog"
|
const val changeLog = "org.jetbrains.changelog"
|
||||||
const val buildConfig = "com.github.gmazzo.buildconfig"
|
const val buildConfig = "com.github.gmazzo.buildconfig"
|
||||||
const val aboutLibraries = "com.mikepenz.aboutlibraries.plugin"
|
const val aboutLibraries = "com.mikepenz.aboutlibraries.plugin"
|
||||||
|
|
||||||
|
const val multiplatformResources = "dev.icerock.mobile.multiplatform-resources"
|
||||||
}
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package myPlugins
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("multiplatform")
|
||||||
|
}
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
google()
|
||||||
|
}
|
@ -71,6 +71,7 @@ dependencies {
|
|||||||
implementation(project(":shared:updater"))
|
implementation(project(":shared:updater"))
|
||||||
implementation(project(":shared:auto-start"))
|
implementation(project(":shared:auto-start"))
|
||||||
implementation(project(":shared:nanohttp4k"))
|
implementation(project(":shared:nanohttp4k"))
|
||||||
|
implementation(project(":shared:resources"))
|
||||||
}
|
}
|
||||||
|
|
||||||
aboutLibraries {
|
aboutLibraries {
|
||||||
|
@ -37,9 +37,13 @@ import ir.amirab.downloader.utils.ExceptionUtils
|
|||||||
import ir.amirab.downloader.utils.OnDuplicateStrategy
|
import ir.amirab.downloader.utils.OnDuplicateStrategy
|
||||||
import com.abdownloadmanager.integration.Integration
|
import com.abdownloadmanager.integration.Integration
|
||||||
import com.abdownloadmanager.integration.IntegrationResult
|
import com.abdownloadmanager.integration.IntegrationResult
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
import com.abdownloadmanager.utils.category.CategoryManager
|
import com.abdownloadmanager.utils.category.CategoryManager
|
||||||
import com.abdownloadmanager.utils.category.CategorySelectionMode
|
import com.abdownloadmanager.utils.category.CategorySelectionMode
|
||||||
import ir.amirab.downloader.exception.TooManyErrorException
|
import ir.amirab.downloader.exception.TooManyErrorException
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
|
import ir.amirab.util.compose.combineStringSources
|
||||||
import ir.amirab.util.osfileutil.FileUtils
|
import ir.amirab.util.osfileutil.FileUtils
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -56,8 +60,8 @@ sealed interface AppEffects {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface NotificationSender {
|
interface NotificationSender {
|
||||||
fun sendDialogNotification(title: String, description: String, type: MessageDialogType)
|
fun sendDialogNotification(title: StringSource, description: StringSource, type: MessageDialogType)
|
||||||
fun sendNotification(tag: Any, title: String, description: String, type: NotificationType)
|
fun sendNotification(tag: Any, title: StringSource, description: StringSource, type: NotificationType)
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppComponent(
|
class AppComponent(
|
||||||
@ -365,14 +369,14 @@ class AppComponent(
|
|||||||
}.launchIn(scope)
|
}.launchIn(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sendNotification(tag: Any, title: String, description: String, type: NotificationType) {
|
override fun sendNotification(tag: Any, title: StringSource, description: StringSource, type: NotificationType) {
|
||||||
beep()
|
beep()
|
||||||
showNotification(tag = tag, title = title, description = description, type = type)
|
showNotification(tag = tag, title = title, description = description, type = type)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sendDialogNotification(
|
override fun sendDialogNotification(
|
||||||
title: String,
|
title: StringSource,
|
||||||
description: String,
|
description: StringSource,
|
||||||
type: MessageDialogType,
|
type: MessageDialogType,
|
||||||
) {
|
) {
|
||||||
beep()
|
beep()
|
||||||
@ -387,8 +391,8 @@ class AppComponent(
|
|||||||
|
|
||||||
private fun showNotification(
|
private fun showNotification(
|
||||||
tag: Any,
|
tag: Any,
|
||||||
title: String,
|
title: StringSource,
|
||||||
description: String,
|
description: StringSource,
|
||||||
type: NotificationType = NotificationType.Info,
|
type: NotificationType = NotificationType.Info,
|
||||||
) {
|
) {
|
||||||
sendEffect(
|
sendEffect(
|
||||||
@ -418,9 +422,9 @@ class AppComponent(
|
|||||||
is IntegrationResult.Fail -> {
|
is IntegrationResult.Fail -> {
|
||||||
IntegrationPortBroadcaster.setIntegrationPortInFile(null)
|
IntegrationPortBroadcaster.setIntegrationPortInFile(null)
|
||||||
sendDialogNotification(
|
sendDialogNotification(
|
||||||
title = "Can't run browser integration",
|
title = Res.string.cant_run_browser_integration.asStringSource(),
|
||||||
type = MessageDialogType.Error,
|
type = MessageDialogType.Error,
|
||||||
description = it.throwable.localizedMessage
|
description = it.throwable.localizedMessage.asStringSource()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -467,20 +471,20 @@ class AppComponent(
|
|||||||
"Too Many Error: "
|
"Too Many Error: "
|
||||||
} else {
|
} else {
|
||||||
"Error: "
|
"Error: "
|
||||||
}
|
}.asStringSource()
|
||||||
val reason = actualCause.message ?: "Unknown"
|
val reason = actualCause.message?.asStringSource() ?: Res.string.unknown.asStringSource()
|
||||||
sendNotification(
|
sendNotification(
|
||||||
"downloadId=${it.downloadItem.id}",
|
"downloadId=${it.downloadItem.id}",
|
||||||
title = it.downloadItem.name,
|
title = it.downloadItem.name.asStringSource(),
|
||||||
description = prefix + reason,
|
description = listOf(prefix, reason).combineStringSources(),
|
||||||
type = NotificationType.Error,
|
type = NotificationType.Error,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (it is DownloadManagerEvents.OnJobCompleted) {
|
if (it is DownloadManagerEvents.OnJobCompleted) {
|
||||||
sendNotification(
|
sendNotification(
|
||||||
tag = "downloadId=${it.downloadItem.id}",
|
tag = "downloadId=${it.downloadItem.id}",
|
||||||
title = it.downloadItem.name,
|
title = it.downloadItem.name.asStringSource(),
|
||||||
description = "Finished",
|
description = Res.string.finished.asStringSource(),
|
||||||
type = NotificationType.Success,
|
type = NotificationType.Success,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -490,9 +494,9 @@ class AppComponent(
|
|||||||
val item = downloadSystem.getDownloadItemById(id)
|
val item = downloadSystem.getDownloadItemById(id)
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
sendNotification(
|
sendNotification(
|
||||||
"Open File",
|
Res.string.open_file,
|
||||||
"Can't open file",
|
Res.string.cant_open_file.asStringSource(),
|
||||||
"Download Item not found",
|
Res.string.download_item_not_found.asStringSource(),
|
||||||
NotificationType.Error,
|
NotificationType.Error,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@ -505,9 +509,9 @@ class AppComponent(
|
|||||||
FileUtils.openFile(downloadSystem.getDownloadFile(downloadItem))
|
FileUtils.openFile(downloadSystem.getDownloadFile(downloadItem))
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
sendNotification(
|
sendNotification(
|
||||||
"Open File",
|
Res.string.open_file,
|
||||||
"Can't open file",
|
Res.string.cant_open_file.asStringSource(),
|
||||||
it.localizedMessage ?: "Unknown Error",
|
it.localizedMessage?.asStringSource() ?: Res.string.unknown_error.asStringSource(),
|
||||||
NotificationType.Error,
|
NotificationType.Error,
|
||||||
)
|
)
|
||||||
println("Can't open file:${it.message}")
|
println("Can't open file:${it.message}")
|
||||||
@ -518,9 +522,9 @@ class AppComponent(
|
|||||||
val item = downloadSystem.getDownloadItemById(id)
|
val item = downloadSystem.getDownloadItemById(id)
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
sendNotification(
|
sendNotification(
|
||||||
"Open Folder",
|
Res.string.open_folder,
|
||||||
"Can't open folder",
|
Res.string.cant_open_folder.asStringSource(),
|
||||||
"Download Item not found",
|
Res.string.download_item_not_found.asStringSource(),
|
||||||
NotificationType.Error,
|
NotificationType.Error,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@ -533,9 +537,9 @@ class AppComponent(
|
|||||||
FileUtils.openFolderOfFile(downloadSystem.getDownloadFile(downloadItem))
|
FileUtils.openFolderOfFile(downloadSystem.getDownloadFile(downloadItem))
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
sendNotification(
|
sendNotification(
|
||||||
"Open Folder",
|
Res.string.open_folder,
|
||||||
"Can't open folder",
|
Res.string.cant_open_folder.asStringSource(),
|
||||||
it.localizedMessage ?: "Unknown Error",
|
it.localizedMessage?.asStringSource() ?: Res.string.unknown_error.asStringSource(),
|
||||||
NotificationType.Error,
|
NotificationType.Error,
|
||||||
)
|
)
|
||||||
println("Can't open folder:${it.message}")
|
println("Can't open folder:${it.message}")
|
||||||
|
@ -7,13 +7,14 @@ import com.abdownloadmanager.desktop.ui.widget.MessageDialogType
|
|||||||
import ir.amirab.util.compose.action.AnAction
|
import ir.amirab.util.compose.action.AnAction
|
||||||
import ir.amirab.util.compose.action.MenuItem
|
import ir.amirab.util.compose.action.MenuItem
|
||||||
import ir.amirab.util.compose.action.simpleAction
|
import ir.amirab.util.compose.action.simpleAction
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
|
|
||||||
private val appComponent = Di.get<AppComponent>()
|
private val appComponent = Di.get<AppComponent>()
|
||||||
|
|
||||||
val dummyException by lazy {
|
val dummyException by lazy {
|
||||||
simpleAction(
|
simpleAction(
|
||||||
"Dummy Exception",
|
"Dummy Exception".asStringSource(),
|
||||||
MyIcons.info
|
MyIcons.info
|
||||||
) {
|
) {
|
||||||
error("This is a dummy exception that is thrown by developer")
|
error("This is a dummy exception that is thrown by developer")
|
||||||
@ -21,7 +22,7 @@ val dummyException by lazy {
|
|||||||
}
|
}
|
||||||
val dummyMessage by lazy {
|
val dummyMessage by lazy {
|
||||||
MenuItem.SubMenu(
|
MenuItem.SubMenu(
|
||||||
title = "Show Dialog Message",
|
title = "Show Dialog Message".asStringSource(),
|
||||||
icon = MyIcons.info,
|
icon = MyIcons.info,
|
||||||
items = listOf(
|
items = listOf(
|
||||||
MessageDialogType.Info,
|
MessageDialogType.Info,
|
||||||
@ -34,13 +35,13 @@ val dummyMessage by lazy {
|
|||||||
|
|
||||||
private fun createDummyMessage(type: MessageDialogType): AnAction {
|
private fun createDummyMessage(type: MessageDialogType): AnAction {
|
||||||
return simpleAction(
|
return simpleAction(
|
||||||
"$type Message",
|
"$type Message".asStringSource(),
|
||||||
MyIcons.info,
|
MyIcons.info,
|
||||||
) {
|
) {
|
||||||
appComponent.sendDialogNotification(
|
appComponent.sendDialogNotification(
|
||||||
type = type,
|
type = type,
|
||||||
title = "Dummy Message",
|
title = "Dummy Message".asStringSource(),
|
||||||
description = "This is a test message"
|
description = "This is a test message".asStringSource()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -12,6 +12,8 @@ import ir.amirab.util.compose.action.buildMenu
|
|||||||
import ir.amirab.util.compose.action.simpleAction
|
import ir.amirab.util.compose.action.simpleAction
|
||||||
import com.abdownloadmanager.desktop.utils.getIcon
|
import com.abdownloadmanager.desktop.utils.getIcon
|
||||||
import com.abdownloadmanager.desktop.utils.getName
|
import com.abdownloadmanager.desktop.utils.getName
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
import com.abdownloadmanager.utils.category.Category
|
import com.abdownloadmanager.utils.category.Category
|
||||||
import ir.amirab.downloader.downloaditem.DownloadCredentials
|
import ir.amirab.downloader.downloaditem.DownloadCredentials
|
||||||
import ir.amirab.downloader.queue.DownloadQueue
|
import ir.amirab.downloader.queue.DownloadQueue
|
||||||
@ -19,6 +21,7 @@ import ir.amirab.downloader.queue.activeQueuesFlow
|
|||||||
import ir.amirab.downloader.queue.inactiveQueuesFlow
|
import ir.amirab.downloader.queue.inactiveQueuesFlow
|
||||||
import com.abdownloadmanager.utils.extractors.linkextractor.DownloadCredentialFromStringExtractor
|
import com.abdownloadmanager.utils.extractors.linkextractor.DownloadCredentialFromStringExtractor
|
||||||
import ir.amirab.util.UrlUtils
|
import ir.amirab.util.UrlUtils
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
import ir.amirab.util.flow.combineStateFlows
|
import ir.amirab.util.flow.combineStateFlows
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
@ -39,13 +42,13 @@ private val activeQueuesFlow = downloadSystem
|
|||||||
)
|
)
|
||||||
|
|
||||||
val newDownloadAction = simpleAction(
|
val newDownloadAction = simpleAction(
|
||||||
"New Download",
|
Res.string.new_download.asStringSource(),
|
||||||
MyIcons.add,
|
MyIcons.add,
|
||||||
) {
|
) {
|
||||||
appComponent.openAddDownloadDialog(listOf(DownloadCredentials.empty()))
|
appComponent.openAddDownloadDialog(listOf(DownloadCredentials.empty()))
|
||||||
}
|
}
|
||||||
val newDownloadFromClipboardAction = simpleAction(
|
val newDownloadFromClipboardAction = simpleAction(
|
||||||
"Import from clipboard",
|
Res.string.import_from_clipboard.asStringSource(),
|
||||||
MyIcons.paste,
|
MyIcons.paste,
|
||||||
) {
|
) {
|
||||||
val contentsInClipboard = ClipboardUtil.read()
|
val contentsInClipboard = ClipboardUtil.read()
|
||||||
@ -61,14 +64,14 @@ val newDownloadFromClipboardAction = simpleAction(
|
|||||||
appComponent.openAddDownloadDialog(items)
|
appComponent.openAddDownloadDialog(items)
|
||||||
}
|
}
|
||||||
val batchDownloadAction = simpleAction(
|
val batchDownloadAction = simpleAction(
|
||||||
title = "Batch Download",
|
title = Res.string.batch_download.asStringSource(),
|
||||||
icon = MyIcons.download
|
icon = MyIcons.download
|
||||||
) {
|
) {
|
||||||
appComponent.openBatchDownload()
|
appComponent.openBatchDownload()
|
||||||
}
|
}
|
||||||
val stopQueueGroupAction = MenuItem.SubMenu(
|
val stopQueueGroupAction = MenuItem.SubMenu(
|
||||||
icon = MyIcons.stop,
|
icon = MyIcons.stop,
|
||||||
title = "Stop Queue",
|
title = Res.string.stop_queue.asStringSource(),
|
||||||
items = emptyList()
|
items = emptyList()
|
||||||
).apply {
|
).apply {
|
||||||
activeQueuesFlow
|
activeQueuesFlow
|
||||||
@ -82,7 +85,7 @@ val stopQueueGroupAction = MenuItem.SubMenu(
|
|||||||
|
|
||||||
val startQueueGroupAction = MenuItem.SubMenu(
|
val startQueueGroupAction = MenuItem.SubMenu(
|
||||||
icon = MyIcons.resume,
|
icon = MyIcons.resume,
|
||||||
title = "Start Queue",
|
title = Res.string.start_queue.asStringSource(),
|
||||||
items = emptyList()
|
items = emptyList()
|
||||||
).apply {
|
).apply {
|
||||||
appComponent.downloadSystem.queueManager
|
appComponent.downloadSystem.queueManager
|
||||||
@ -97,7 +100,7 @@ val startQueueGroupAction = MenuItem.SubMenu(
|
|||||||
|
|
||||||
|
|
||||||
val stopAllAction = simpleAction(
|
val stopAllAction = simpleAction(
|
||||||
"Stop All",
|
Res.string.stop_all.asStringSource(),
|
||||||
MyIcons.stop,
|
MyIcons.stop,
|
||||||
checkEnable = combineStateFlows(
|
checkEnable = combineStateFlows(
|
||||||
downloadSystem.downloadMonitor.activeDownloadCount,
|
downloadSystem.downloadMonitor.activeDownloadCount,
|
||||||
@ -113,19 +116,19 @@ val stopAllAction = simpleAction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val exitAction = simpleAction(
|
val exitAction = simpleAction(
|
||||||
"Exit",
|
Res.string.exit.asStringSource(),
|
||||||
MyIcons.exit,
|
MyIcons.exit,
|
||||||
) {
|
) {
|
||||||
appComponent.requestClose()
|
appComponent.requestClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
val browserIntegrations = MenuItem.SubMenu(
|
val browserIntegrations = MenuItem.SubMenu(
|
||||||
title = "Download Browser Integration",
|
title = Res.string.download_browser_integration.asStringSource(),
|
||||||
icon = MyIcons.download,
|
icon = MyIcons.download,
|
||||||
items = buildMenu {
|
items = buildMenu {
|
||||||
for (browserExtension in SharedConstants.browserIntegrations) {
|
for (browserExtension in SharedConstants.browserIntegrations) {
|
||||||
item(
|
item(
|
||||||
title = browserExtension.type.getName(),
|
title = browserExtension.type.getName().asStringSource(),
|
||||||
icon = browserExtension.type.getIcon(),
|
icon = browserExtension.type.getIcon(),
|
||||||
onClick = { UrlUtils.openUrl(browserExtension.url) }
|
onClick = { UrlUtils.openUrl(browserExtension.url) }
|
||||||
)
|
)
|
||||||
@ -134,13 +137,13 @@ val browserIntegrations = MenuItem.SubMenu(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val gotoSettingsAction = simpleAction(
|
val gotoSettingsAction = simpleAction(
|
||||||
"Settings",
|
Res.string.settings.asStringSource(),
|
||||||
MyIcons.settings,
|
MyIcons.settings,
|
||||||
) {
|
) {
|
||||||
appComponent.openSettings()
|
appComponent.openSettings()
|
||||||
}
|
}
|
||||||
val showDownloadList = simpleAction(
|
val showDownloadList = simpleAction(
|
||||||
"Show Downloads",
|
Res.string.show_downloads.asStringSource(),
|
||||||
MyIcons.settings,
|
MyIcons.settings,
|
||||||
) {
|
) {
|
||||||
appComponent.openHome()
|
appComponent.openHome()
|
||||||
@ -153,33 +156,33 @@ val showDownloadList = simpleAction(
|
|||||||
appComponent.updater.requestCheckForUpdate()
|
appComponent.updater.requestCheckForUpdate()
|
||||||
}*/
|
}*/
|
||||||
val openAboutAction = simpleAction(
|
val openAboutAction = simpleAction(
|
||||||
title = "About",
|
title = Res.string.about.asStringSource(),
|
||||||
icon = MyIcons.info,
|
icon = MyIcons.info,
|
||||||
) {
|
) {
|
||||||
appComponent.openAbout()
|
appComponent.openAbout()
|
||||||
}
|
}
|
||||||
val openOpenSourceThirdPartyLibraries = simpleAction(
|
val openOpenSourceThirdPartyLibraries = simpleAction(
|
||||||
title = "View OpenSource Libraries",
|
title = Res.string.view_the_open_source_licenses.asStringSource(),
|
||||||
icon = MyIcons.openSource,
|
icon = MyIcons.openSource,
|
||||||
) {
|
) {
|
||||||
appComponent.openOpenSourceLibraries()
|
appComponent.openOpenSourceLibraries()
|
||||||
}
|
}
|
||||||
|
|
||||||
val supportActionGroup = MenuItem.SubMenu(
|
val supportActionGroup = MenuItem.SubMenu(
|
||||||
title = "Support & Community",
|
title = Res.string.support_and_community.asStringSource(),
|
||||||
icon = MyIcons.group,
|
icon = MyIcons.group,
|
||||||
items = buildMenu {
|
items = buildMenu {
|
||||||
item("Website", MyIcons.appIcon) {
|
item(Res.string.website.asStringSource(), MyIcons.appIcon) {
|
||||||
UrlUtils.openUrl(AppInfo.website)
|
UrlUtils.openUrl(AppInfo.website)
|
||||||
}
|
}
|
||||||
item("Source Code", MyIcons.openSource) {
|
item(Res.string.source_code.asStringSource(), MyIcons.openSource) {
|
||||||
UrlUtils.openUrl(AppInfo.sourceCode)
|
UrlUtils.openUrl(AppInfo.sourceCode)
|
||||||
}
|
}
|
||||||
subMenu("Telegram", MyIcons.telegram) {
|
subMenu(Res.string.telegram.asStringSource(), MyIcons.telegram) {
|
||||||
item("Channel", MyIcons.speaker) {
|
item(Res.string.channel.asStringSource(), MyIcons.speaker) {
|
||||||
UrlUtils.openUrl(SharedConstants.telegramChannelUrl)
|
UrlUtils.openUrl(SharedConstants.telegramChannelUrl)
|
||||||
}
|
}
|
||||||
item("Group", MyIcons.group) {
|
item(Res.string.group.asStringSource(), MyIcons.group) {
|
||||||
UrlUtils.openUrl(SharedConstants.telegramGroupUrl)
|
UrlUtils.openUrl(SharedConstants.telegramGroupUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -187,7 +190,7 @@ val supportActionGroup = MenuItem.SubMenu(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val openQueuesAction = simpleAction(
|
val openQueuesAction = simpleAction(
|
||||||
title = "Open Queues",
|
title = Res.string.queues.asStringSource(),
|
||||||
icon = MyIcons.queue
|
icon = MyIcons.queue
|
||||||
) {
|
) {
|
||||||
appComponent.openQueues()
|
appComponent.openQueues()
|
||||||
@ -197,7 +200,7 @@ fun moveToQueueAction(
|
|||||||
queue: DownloadQueue,
|
queue: DownloadQueue,
|
||||||
itemId: List<Long>,
|
itemId: List<Long>,
|
||||||
): AnAction {
|
): AnAction {
|
||||||
return simpleAction(queue.getQueueModel().name) {
|
return simpleAction(queue.getQueueModel().name.asStringSource()) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
downloadSystem
|
downloadSystem
|
||||||
.queueManager
|
.queueManager
|
||||||
@ -212,7 +215,7 @@ fun createMoveToCategoryAction(
|
|||||||
category: Category,
|
category: Category,
|
||||||
itemIds: List<Long>,
|
itemIds: List<Long>,
|
||||||
): AnAction {
|
): AnAction {
|
||||||
return simpleAction(category.name) {
|
return simpleAction(category.name.asStringSource()) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
downloadSystem
|
downloadSystem
|
||||||
.categoryManager
|
.categoryManager
|
||||||
@ -227,7 +230,7 @@ fun createMoveToCategoryAction(
|
|||||||
fun stopQueueAction(
|
fun stopQueueAction(
|
||||||
queue: DownloadQueue,
|
queue: DownloadQueue,
|
||||||
): AnAction {
|
): AnAction {
|
||||||
return simpleAction(queue.getQueueModel().name) {
|
return simpleAction(queue.getQueueModel().name.asStringSource()) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
queue.stop()
|
queue.stop()
|
||||||
}
|
}
|
||||||
@ -237,14 +240,14 @@ fun stopQueueAction(
|
|||||||
fun startQueueAction(
|
fun startQueueAction(
|
||||||
queue: DownloadQueue,
|
queue: DownloadQueue,
|
||||||
): AnAction {
|
): AnAction {
|
||||||
return simpleAction(queue.getQueueModel().name) {
|
return simpleAction(queue.getQueueModel().name.asStringSource()) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
queue.start()
|
queue.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val newQueueAction = simpleAction("New Queue") {
|
val newQueueAction = simpleAction(Res.string.add_new_queue.asStringSource()) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
appComponent.openNewQueueDialog()
|
appComponent.openNewQueueDialog()
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,8 @@ import com.abdownloadmanager.utils.proxy.ProxyManager
|
|||||||
import ir.amirab.downloader.connection.proxy.ProxyStrategyProvider
|
import ir.amirab.downloader.connection.proxy.ProxyStrategyProvider
|
||||||
import ir.amirab.downloader.monitor.IDownloadMonitor
|
import ir.amirab.downloader.monitor.IDownloadMonitor
|
||||||
import ir.amirab.downloader.utils.EmptyFileCreator
|
import ir.amirab.downloader.utils.EmptyFileCreator
|
||||||
|
import ir.amirab.util.compose.localizationmanager.LanguageManager
|
||||||
|
import ir.amirab.util.compose.localizationmanager.LanguageStorage
|
||||||
import ir.amirab.util.config.datastore.kotlinxSerializationDataStore
|
import ir.amirab.util.config.datastore.kotlinxSerializationDataStore
|
||||||
|
|
||||||
val downloaderModule = module {
|
val downloaderModule = module {
|
||||||
@ -222,6 +224,9 @@ val appModule = module {
|
|||||||
single {
|
single {
|
||||||
ThemeManager(get(), get())
|
ThemeManager(get(), get())
|
||||||
}
|
}
|
||||||
|
single {
|
||||||
|
LanguageManager(get())
|
||||||
|
}
|
||||||
single {
|
single {
|
||||||
MyIcons
|
MyIcons
|
||||||
}.bind<IMyIcons>()
|
}.bind<IMyIcons>()
|
||||||
@ -241,7 +246,7 @@ val appModule = module {
|
|||||||
get(),
|
get(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}.bind<LanguageStorage>()
|
||||||
single {
|
single {
|
||||||
PageStatesStorage(
|
PageStatesStorage(
|
||||||
createMapConfigDatastore(
|
createMapConfigDatastore(
|
||||||
|
@ -9,6 +9,9 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.rememberWindowState
|
import androidx.compose.ui.window.rememberWindowState
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ShowAboutDialog(appComponent: AppComponent) {
|
fun ShowAboutDialog(appComponent: AppComponent) {
|
||||||
@ -37,7 +40,7 @@ fun AboutDialog(
|
|||||||
),
|
),
|
||||||
onCloseRequest = onClose
|
onCloseRequest = onClose
|
||||||
) {
|
) {
|
||||||
WindowTitle("About")
|
WindowTitle(myStringResource(Res.string.about))
|
||||||
AboutPage(
|
AboutPage(
|
||||||
close = onClose,
|
close = onClose,
|
||||||
onRequestShowOpenSourceLibraries = onRequestShowOpenSourceLibraries
|
onRequestShowOpenSourceLibraries = onRequestShowOpenSourceLibraries
|
||||||
|
@ -29,8 +29,12 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.abdownloadmanager.desktop.App
|
||||||
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
||||||
import com.abdownloadmanager.desktop.ui.util.ifThen
|
import com.abdownloadmanager.desktop.ui.util.ifThen
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AboutPage(
|
fun AboutPage(
|
||||||
@ -44,7 +48,7 @@ fun AboutPage(
|
|||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
Row(Modifier.fillMaxWidth().wrapContentWidth(Alignment.End)) {
|
Row(Modifier.fillMaxWidth().wrapContentWidth(Alignment.End)) {
|
||||||
ActionButton(
|
ActionButton(
|
||||||
"Close",
|
myStringResource(Res.string.close),
|
||||||
onClick = close
|
onClick = close
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -57,8 +61,7 @@ fun RenderAppInfo(
|
|||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
Modifier.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
.padding(horizontal = 8.dp)
|
.padding(horizontal = 8.dp),
|
||||||
,
|
|
||||||
) {
|
) {
|
||||||
ProvideTextStyle(
|
ProvideTextStyle(
|
||||||
TextStyle(fontSize = myTextSizes.base)
|
TextStyle(fontSize = myTextSizes.base)
|
||||||
@ -82,23 +85,30 @@ fun RenderAppInfo(
|
|||||||
)
|
)
|
||||||
Spacer(Modifier.height(2.dp))
|
Spacer(Modifier.height(2.dp))
|
||||||
WithContentAlpha(0.75f) {
|
WithContentAlpha(0.75f) {
|
||||||
Text("version ${AppInfo.version}", fontSize = myTextSizes.base)
|
Text(
|
||||||
|
myStringResource(
|
||||||
|
Res.string.version_n,
|
||||||
|
Res.string.version_n_createArgs(
|
||||||
|
value = AppInfo.version.toString()
|
||||||
|
)
|
||||||
|
), fontSize = myTextSizes.base
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
WithContentAlpha(1f) {
|
WithContentAlpha(1f) {
|
||||||
Text("Developed with ❤️ for you")
|
Text(myStringResource(Res.string.developed_with_love_for_you))
|
||||||
LinkText("Visit the project website", AppInfo.website)
|
LinkText(myStringResource(Res.string.visit_the_project_website), AppInfo.website)
|
||||||
|
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
Text("This is a free & Open Source software")
|
Text(myStringResource(Res.string.this_is_a_free_and_open_source_software))
|
||||||
LinkText("See the Source Code", AppInfo.sourceCode)
|
LinkText(myStringResource(Res.string.view_the_source_code), AppInfo.sourceCode)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Text("Powered by Open Source Libraries")
|
Text(myStringResource(Res.string.powered_by_open_source_software))
|
||||||
Text(
|
Text(
|
||||||
"See the Open Sources libraries",
|
myStringResource(Res.string.view_the_open_source_licenses),
|
||||||
style = LocalTextStyle.current.merge(LinkStyle),
|
style = LocalTextStyle.current.merge(LinkStyle),
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
onRequestShowOpenSourceLibraries()
|
onRequestShowOpenSourceLibraries()
|
||||||
@ -135,12 +145,11 @@ fun LinkText(
|
|||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
style = LocalTextStyle.current
|
style = LocalTextStyle.current
|
||||||
.merge(LinkStyle).ifThen(isHovered){
|
.merge(LinkStyle).ifThen(isHovered) {
|
||||||
copy(
|
copy(
|
||||||
textDecoration = TextDecoration.Underline
|
textDecoration = TextDecoration.Underline
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
,
|
|
||||||
overflow = overflow,
|
overflow = overflow,
|
||||||
maxLines = maxLines,
|
maxLines = maxLines,
|
||||||
)
|
)
|
||||||
|
@ -19,6 +19,9 @@ import androidx.compose.ui.unit.DpSize
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.WindowPosition
|
import androidx.compose.ui.window.WindowPosition
|
||||||
import androidx.compose.ui.window.rememberWindowState
|
import androidx.compose.ui.window.rememberWindowState
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -53,7 +56,7 @@ fun ShowAddDownloadDialogs(component: AddDownloadDialogManager) {
|
|||||||
window.minimumSize = Dimension(w, h)
|
window.minimumSize = Dimension(w, h)
|
||||||
}
|
}
|
||||||
// BringToFront()
|
// BringToFront()
|
||||||
WindowTitle("Add download")
|
WindowTitle(myStringResource(Res.string.add_download))
|
||||||
WindowIcon(MyIcons.appIcon)
|
WindowIcon(MyIcons.appIcon)
|
||||||
AddDownloadPage(addDownloadComponent)
|
AddDownloadPage(addDownloadComponent)
|
||||||
}
|
}
|
||||||
@ -76,7 +79,7 @@ fun ShowAddDownloadDialogs(component: AddDownloadDialogManager) {
|
|||||||
window.minimumSize = Dimension(w, h)
|
window.minimumSize = Dimension(w, h)
|
||||||
}
|
}
|
||||||
// BringToFront()
|
// BringToFront()
|
||||||
WindowTitle("Add download")
|
WindowTitle(myStringResource(Res.string.add_download))
|
||||||
WindowIcon(MyIcons.appIcon)
|
WindowIcon(MyIcons.appIcon)
|
||||||
AddMultiItemPage(addDownloadComponent)
|
AddMultiItemPage(addDownloadComponent)
|
||||||
}
|
}
|
||||||
|
@ -30,12 +30,15 @@ import com.abdownloadmanager.desktop.ui.icon.MyIcons
|
|||||||
import com.abdownloadmanager.desktop.ui.theme.myColors
|
import com.abdownloadmanager.desktop.ui.theme.myColors
|
||||||
import com.abdownloadmanager.desktop.ui.util.ifThen
|
import com.abdownloadmanager.desktop.ui.util.ifThen
|
||||||
import com.abdownloadmanager.desktop.ui.widget.CheckBox
|
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.div
|
||||||
import com.abdownloadmanager.desktop.utils.windowUtil.moveSafe
|
import com.abdownloadmanager.desktop.utils.windowUtil.moveSafe
|
||||||
import com.abdownloadmanager.utils.category.CategorySelectionMode
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
import com.abdownloadmanager.utils.compose.WithContentColor
|
import com.abdownloadmanager.utils.compose.WithContentColor
|
||||||
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
import java.awt.MouseInfo
|
import java.awt.MouseInfo
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -51,7 +54,7 @@ fun AddMultiItemPage(
|
|||||||
) {
|
) {
|
||||||
WithContentAlpha(1f) {
|
WithContentAlpha(1f) {
|
||||||
Text(
|
Text(
|
||||||
"Select Items you want to pick up for download",
|
myStringResource(Res.string.add_multi_download_page_header),
|
||||||
fontSize = myTextSizes.base
|
fontSize = myTextSizes.base
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -109,7 +112,7 @@ fun Footer(
|
|||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Row(Modifier.align(Alignment.Bottom)) {
|
Row(Modifier.align(Alignment.Bottom)) {
|
||||||
ActionButton(
|
ActionButton(
|
||||||
text = "Add",
|
text = myStringResource(Res.string.add),
|
||||||
onClick = {
|
onClick = {
|
||||||
component.openAddToQueueDialog()
|
component.openAddToQueueDialog()
|
||||||
},
|
},
|
||||||
@ -118,7 +121,7 @@ fun Footer(
|
|||||||
)
|
)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
ActionButton(
|
ActionButton(
|
||||||
text = "Cancel",
|
text = myStringResource(Res.string.cancel),
|
||||||
onClick = {
|
onClick = {
|
||||||
component.requestClose()
|
component.requestClose()
|
||||||
},
|
},
|
||||||
@ -139,7 +142,7 @@ private fun SaveSettings(
|
|||||||
) {
|
) {
|
||||||
var dropdownOpen by remember { mutableStateOf(false) }
|
var dropdownOpen by remember { mutableStateOf(false) }
|
||||||
val saveMode by component.saveMode.collectAsState()
|
val saveMode by component.saveMode.collectAsState()
|
||||||
Text("Save to:")
|
Text("${myStringResource(Res.string.save_to)}:")
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
SaveSolution(
|
SaveSolution(
|
||||||
saveMode = saveMode,
|
saveMode = saveMode,
|
||||||
@ -275,14 +278,14 @@ private fun SaveSolutionPopup(
|
|||||||
Modifier.padding(16.dp)
|
Modifier.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"Where should each item saved?",
|
myStringResource(Res.string.where_should_each_item_saved),
|
||||||
Modifier,
|
Modifier,
|
||||||
fontSize = myTextSizes.base
|
fontSize = myTextSizes.base
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
WithContentAlpha(0.75f) {
|
WithContentAlpha(0.75f) {
|
||||||
Text(
|
Text(
|
||||||
"There are multiple items! please select a way you want to save them",
|
myStringResource(Res.string.there_are_multiple_items_please_select_a_way_you_want_to_save_them),
|
||||||
Modifier,
|
Modifier,
|
||||||
fontSize = myTextSizes.sm,
|
fontSize = myTextSizes.sm,
|
||||||
)
|
)
|
||||||
@ -304,8 +307,8 @@ private fun SaveSolutionPopup(
|
|||||||
Column {
|
Column {
|
||||||
for (item in AddMultiItemSaveMode.entries) {
|
for (item in AddMultiItemSaveMode.entries) {
|
||||||
SaveSolutionItem(
|
SaveSolutionItem(
|
||||||
title = item.title,
|
title = item.title.rememberString(),
|
||||||
description = item.description,
|
description = item.description.rememberString(),
|
||||||
isSelected = selectedItem == item,
|
isSelected = selectedItem == item,
|
||||||
onClick = {
|
onClick = {
|
||||||
onIteSelected(item)
|
onIteSelected(item)
|
||||||
@ -348,7 +351,7 @@ private fun SaveSolutionHeader(
|
|||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp)
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
Text(
|
Text(
|
||||||
saveMode.title,
|
saveMode.title.rememberString(),
|
||||||
contentModifier,
|
contentModifier,
|
||||||
)
|
)
|
||||||
Spacer(
|
Spacer(
|
||||||
@ -434,24 +437,24 @@ private fun AllFilesInSameDirectory(
|
|||||||
onValueChange = setAlsoCategorize
|
onValueChange = setAlsoCategorize
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(4.dp))
|
||||||
Text("Auto categorize")
|
Text(myStringResource(Res.string.auto_categorize_downloads))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class AddMultiItemSaveMode(
|
enum class AddMultiItemSaveMode(
|
||||||
val title: String,
|
val title: StringSource,
|
||||||
val description: String,
|
val description: StringSource,
|
||||||
) {
|
) {
|
||||||
EachFileInTheirOwnCategory(
|
EachFileInTheirOwnCategory(
|
||||||
title = "Each item on its own category",
|
title = Res.string.each_item_on_its_own_category.asStringSource(),
|
||||||
description = "Each item will be placed in a category that have that file type",
|
description = Res.string.each_item_on_its_own_category_description.asStringSource(),
|
||||||
),
|
),
|
||||||
AllInOneCategory(
|
AllInOneCategory(
|
||||||
title = "All items in one Category",
|
title = Res.string.all_items_in_one_category.asStringSource(),
|
||||||
description = "All files will be saved in the selected category location",
|
description = Res.string.all_items_in_one_category_description.asStringSource(),
|
||||||
),
|
),
|
||||||
InSameLocation(
|
InSameLocation(
|
||||||
title = "All items in one Location",
|
title = Res.string.all_items_in_one_Location.asStringSource(),
|
||||||
description = "All items will be saved in the selected directory",
|
description = Res.string.all_items_in_one_Location_description.asStringSource(),
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -25,8 +25,11 @@ import androidx.compose.ui.input.key.onKeyEvent
|
|||||||
import androidx.compose.ui.input.pointer.isShiftPressed
|
import androidx.compose.ui.input.pointer.isShiftPressed
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
import com.abdownloadmanager.utils.FileIconProvider
|
import com.abdownloadmanager.utils.FileIconProvider
|
||||||
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AddMultiDownloadTable(
|
fun AddMultiDownloadTable(
|
||||||
@ -210,7 +213,8 @@ sealed class AddMultiItemTableCells : TableCell<DownloadUiChecker> {
|
|||||||
|
|
||||||
data object Check : AddMultiItemTableCells(),
|
data object Check : AddMultiItemTableCells(),
|
||||||
CustomCellRenderer {
|
CustomCellRenderer {
|
||||||
override val name: String = "#"
|
override val id: String = "#"
|
||||||
|
override val name: StringSource = "#".asStringSource()
|
||||||
override val size: CellSize = CellSize.Fixed(26.dp)
|
override val size: CellSize = CellSize.Fixed(26.dp)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -225,17 +229,20 @@ sealed class AddMultiItemTableCells : TableCell<DownloadUiChecker> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data object Name : AddMultiItemTableCells() {
|
data object Name : AddMultiItemTableCells() {
|
||||||
override val name: String = "Name"
|
override val id: String = "Name"
|
||||||
|
override val name: StringSource = Res.string.name.asStringSource()
|
||||||
override val size: CellSize = CellSize.Resizeable(120.dp..1000.dp, 350.dp)
|
override val size: CellSize = CellSize.Resizeable(120.dp..1000.dp, 350.dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object Link : AddMultiItemTableCells() {
|
data object Link : AddMultiItemTableCells() {
|
||||||
override val name: String = "Link"
|
override val id: String = "Link"
|
||||||
|
override val name: StringSource = Res.string.link.asStringSource()
|
||||||
override val size: CellSize = CellSize.Resizeable(120.dp..2000.dp, 240.dp)
|
override val size: CellSize = CellSize.Resizeable(120.dp..2000.dp, 240.dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object SizeCell : AddMultiItemTableCells() {
|
data object SizeCell : AddMultiItemTableCells() {
|
||||||
override val name: String = "Size"
|
override val id: String = "Size"
|
||||||
|
override val name: StringSource = Res.string.size.asStringSource()
|
||||||
override val size: CellSize = CellSize.Resizeable(100.dp..180.dp, 100.dp)
|
override val size: CellSize = CellSize.Resizeable(100.dp..180.dp, 100.dp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -289,7 +296,7 @@ private fun SizeCell(
|
|||||||
val length by downloadChecker.length.collectAsState()
|
val length by downloadChecker.length.collectAsState()
|
||||||
CellText(
|
CellText(
|
||||||
length?.let {
|
length?.let {
|
||||||
convertSizeToHumanReadable(it)
|
convertSizeToHumanReadable(it).rememberString()
|
||||||
} ?: ""
|
} ?: ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -29,8 +29,11 @@ import com.abdownloadmanager.desktop.ui.util.ifThen
|
|||||||
import com.abdownloadmanager.desktop.ui.widget.Text
|
import com.abdownloadmanager.desktop.ui.widget.Text
|
||||||
import com.abdownloadmanager.desktop.utils.div
|
import com.abdownloadmanager.desktop.utils.div
|
||||||
import com.abdownloadmanager.desktop.utils.windowUtil.moveSafe
|
import com.abdownloadmanager.desktop.utils.windowUtil.moveSafe
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
import com.abdownloadmanager.utils.compose.WithContentAlpha
|
import com.abdownloadmanager.utils.compose.WithContentAlpha
|
||||||
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
import java.awt.MouseInfo
|
import java.awt.MouseInfo
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -208,7 +211,7 @@ private fun <T> DropDownHeader(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
"No Category Selected",
|
myStringResource(Res.string.no_category_selected),
|
||||||
contentModifier
|
contentModifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,9 @@ import androidx.compose.ui.layout.onGloballyPositioned
|
|||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
import io.github.vinceglb.filekit.compose.rememberDirectoryPickerLauncher
|
import io.github.vinceglb.filekit.compose.rememberDirectoryPickerLauncher
|
||||||
import io.github.vinceglb.filekit.core.FileKitPlatformSettings
|
import io.github.vinceglb.filekit.core.FileKitPlatformSettings
|
||||||
import ir.amirab.util.desktop.LocalWindow
|
import ir.amirab.util.desktop.LocalWindow
|
||||||
@ -36,7 +39,7 @@ fun LocationTextField(
|
|||||||
var showLastUsedLocations by remember { mutableStateOf(false) }
|
var showLastUsedLocations by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val downloadLauncherFolderPickerLauncher = rememberDirectoryPickerLauncher(
|
val downloadLauncherFolderPickerLauncher = rememberDirectoryPickerLauncher(
|
||||||
title = "Download Location",
|
title = myStringResource(Res.string.download_location),
|
||||||
initialDirectory = remember(text) {
|
initialDirectory = remember(text) {
|
||||||
runCatching {
|
runCatching {
|
||||||
File(text).canonicalPath
|
File(text).canonicalPath
|
||||||
@ -57,7 +60,7 @@ fun LocationTextField(
|
|||||||
AddDownloadPageTextField(
|
AddDownloadPageTextField(
|
||||||
text,
|
text,
|
||||||
setText,
|
setText,
|
||||||
"Location",
|
myStringResource(Res.string.location),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.onGloballyPositioned {
|
.onGloballyPositioned {
|
||||||
|
@ -24,6 +24,9 @@ import androidx.compose.ui.unit.DpOffset
|
|||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.rememberDialogState
|
import androidx.compose.ui.window.rememberDialogState
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
import ir.amirab.downloader.queue.DownloadQueue
|
import ir.amirab.downloader.queue.DownloadQueue
|
||||||
import java.awt.MouseInfo
|
import java.awt.MouseInfo
|
||||||
|
|
||||||
@ -82,7 +85,7 @@ fun ShowAddToQueueDialog(
|
|||||||
) {
|
) {
|
||||||
WindowDraggableArea(Modifier.fillMaxWidth()) {
|
WindowDraggableArea(Modifier.fillMaxWidth()) {
|
||||||
Text(
|
Text(
|
||||||
"Choose Queue to add",
|
myStringResource(Res.string.select_queue),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@ -138,11 +141,11 @@ fun ShowAddToQueueDialog(
|
|||||||
){
|
){
|
||||||
IconActionButton(
|
IconActionButton(
|
||||||
MyIcons.add,
|
MyIcons.add,
|
||||||
contentDescription = "Add new queue",
|
contentDescription = myStringResource(Res.string.add_new_queue),
|
||||||
onClick = newQueueAction
|
onClick = newQueueAction
|
||||||
)
|
)
|
||||||
ActionButton(
|
ActionButton(
|
||||||
text = "Without Queue",
|
text = myStringResource(Res.string.without_queue),
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
onClick = {
|
onClick = {
|
||||||
onQueueSelected(null)
|
onQueueSelected(null)
|
||||||
|
@ -32,8 +32,12 @@ import androidx.compose.ui.unit.*
|
|||||||
import androidx.compose.ui.window.*
|
import androidx.compose.ui.window.*
|
||||||
import com.abdownloadmanager.desktop.pages.addDownload.shared.*
|
import com.abdownloadmanager.desktop.pages.addDownload.shared.*
|
||||||
import com.abdownloadmanager.desktop.utils.mvi.HandleEffects
|
import com.abdownloadmanager.desktop.utils.mvi.HandleEffects
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
import com.abdownloadmanager.utils.category.rememberIconPainter
|
import com.abdownloadmanager.utils.category.rememberIconPainter
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
import ir.amirab.downloader.utils.OnDuplicateStrategy
|
import ir.amirab.downloader.utils.OnDuplicateStrategy
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
import java.awt.MouseInfo
|
import java.awt.MouseInfo
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -96,7 +100,7 @@ fun AddDownloadPage(
|
|||||||
onValueChange = { component.setUseCategory(it) }
|
onValueChange = { component.setUseCategory(it) }
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(4.dp))
|
||||||
Text("Use Category")
|
Text(myStringResource(Res.string.use_category))
|
||||||
}
|
}
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
CategorySelect(
|
CategorySelect(
|
||||||
@ -125,7 +129,7 @@ fun AddDownloadPage(
|
|||||||
component.setFolder(it)
|
component.setFolder(it)
|
||||||
},
|
},
|
||||||
errorText = when (canAddResult) {
|
errorText = when (canAddResult) {
|
||||||
CanAddResult.CantWriteInThisFolder -> "Can't write to this folder"
|
CanAddResult.CantWriteInThisFolder -> myStringResource(Res.string.cant_write_to_this_folder)
|
||||||
else -> null
|
else -> null
|
||||||
},
|
},
|
||||||
lastUsedLocations = component.lastUsedLocations.collectAsState().value
|
lastUsedLocations = component.lastUsedLocations.collectAsState().value
|
||||||
@ -140,13 +144,13 @@ fun AddDownloadPage(
|
|||||||
errorText = when (canAddResult) {
|
errorText = when (canAddResult) {
|
||||||
is CanAddResult.DownloadAlreadyExists -> {
|
is CanAddResult.DownloadAlreadyExists -> {
|
||||||
if (onDuplicateStrategy == null) {
|
if (onDuplicateStrategy == null) {
|
||||||
"File name already exists"
|
myStringResource(Res.string.file_name_already_exists)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CanAddResult.InvalidFileName -> "Invalid filename"
|
CanAddResult.InvalidFileName -> myStringResource(Res.string.invalid_file_name)
|
||||||
else -> null
|
else -> null
|
||||||
}.takeIf { name.isNotEmpty() }
|
}.takeIf { name.isNotEmpty() }
|
||||||
)
|
)
|
||||||
@ -237,14 +241,14 @@ private fun ShowSolutionsOnDuplicateDownload(component: AddSingleDownloadCompone
|
|||||||
Modifier.padding(16.dp)
|
Modifier.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"Select a solution",
|
myStringResource(Res.string.select_a_solution),
|
||||||
Modifier,
|
Modifier,
|
||||||
fontSize = myTextSizes.base
|
fontSize = myTextSizes.base
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
WithContentAlpha(0.75f) {
|
WithContentAlpha(0.75f) {
|
||||||
Text(
|
Text(
|
||||||
"The link you provided is already in download lists please specify what you want to do",
|
myStringResource(Res.string.select_download_strategy_description),
|
||||||
Modifier,
|
Modifier,
|
||||||
fontSize = myTextSizes.sm,
|
fontSize = myTextSizes.sm,
|
||||||
)
|
)
|
||||||
@ -262,24 +266,24 @@ private fun ShowSolutionsOnDuplicateDownload(component: AddSingleDownloadCompone
|
|||||||
Column {
|
Column {
|
||||||
OnDuplicateStrategySolutionItem(
|
OnDuplicateStrategySolutionItem(
|
||||||
isSelected = onDuplicateStrategy == OnDuplicateStrategy.AddNumbered,
|
isSelected = onDuplicateStrategy == OnDuplicateStrategy.AddNumbered,
|
||||||
title = "Add a numbered file",
|
title = myStringResource(Res.string.download_strategy_add_a_numbered_file),
|
||||||
description = "Add an index after the end of download file name",
|
description = myStringResource(Res.string.download_strategy_add_a_numbered_file_description),
|
||||||
) {
|
) {
|
||||||
component.setOnDuplicateStrategy(OnDuplicateStrategy.AddNumbered)
|
component.setOnDuplicateStrategy(OnDuplicateStrategy.AddNumbered)
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
OnDuplicateStrategySolutionItem(
|
OnDuplicateStrategySolutionItem(
|
||||||
isSelected = onDuplicateStrategy == OnDuplicateStrategy.OverrideDownload,
|
isSelected = onDuplicateStrategy == OnDuplicateStrategy.OverrideDownload,
|
||||||
title = "Override existing file",
|
title = myStringResource(Res.string.download_strategy_override_existing_file),
|
||||||
description = "Remove existing download and write to that file",
|
description = myStringResource(Res.string.download_strategy_override_existing_file_description),
|
||||||
) {
|
) {
|
||||||
component.setOnDuplicateStrategy(OnDuplicateStrategy.OverrideDownload)
|
component.setOnDuplicateStrategy(OnDuplicateStrategy.OverrideDownload)
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
OnDuplicateStrategySolutionItem(
|
OnDuplicateStrategySolutionItem(
|
||||||
isSelected = null,
|
isSelected = null,
|
||||||
title = "Show downloaded file",
|
title = myStringResource(Res.string.download_strategy_show_downloaded_file),
|
||||||
description = "Show already existing download item , so you can press on resume or open it",
|
description = myStringResource(Res.string.download_strategy_show_downloaded_file_description),
|
||||||
) {
|
) {
|
||||||
component.openDownloadFileForCurrentLink()
|
component.openDownloadFileForCurrentLink()
|
||||||
close()
|
close()
|
||||||
@ -432,13 +436,13 @@ private fun PrimaryMainConfigActionButton(
|
|||||||
fun ConfigActionsButtons(component: AddSingleDownloadComponent) {
|
fun ConfigActionsButtons(component: AddSingleDownloadComponent) {
|
||||||
val responseInfo by component.linkResponseInfo.collectAsState()
|
val responseInfo by component.linkResponseInfo.collectAsState()
|
||||||
Row {
|
Row {
|
||||||
IconActionButton(MyIcons.refresh, "Refresh") {
|
IconActionButton(MyIcons.refresh, myStringResource(Res.string.refresh)) {
|
||||||
component.refresh()
|
component.refresh()
|
||||||
}
|
}
|
||||||
Spacer(Modifier.width(6.dp))
|
Spacer(Modifier.width(6.dp))
|
||||||
IconActionButton(
|
IconActionButton(
|
||||||
MyIcons.settings,
|
MyIcons.settings,
|
||||||
"Settings",
|
myStringResource(Res.string.settings),
|
||||||
indicateActive = component.showMoreSettings,
|
indicateActive = component.showMoreSettings,
|
||||||
requiresAttention = responseInfo?.requireBasicAuth ?: false
|
requiresAttention = responseInfo?.requireBasicAuth ?: false
|
||||||
) {
|
) {
|
||||||
@ -454,14 +458,14 @@ private fun MainActionButtons(component: AddSingleDownloadComponent) {
|
|||||||
val canAddResult by component.canAddResult.collectAsState()
|
val canAddResult by component.canAddResult.collectAsState()
|
||||||
if (canAddResult is CanAddResult.DownloadAlreadyExists && onDuplicateStrategy == null) {
|
if (canAddResult is CanAddResult.DownloadAlreadyExists && onDuplicateStrategy == null) {
|
||||||
MainConfigActionButton(
|
MainConfigActionButton(
|
||||||
text = "Show solutions...",
|
text = myStringResource(Res.string.show_solutions),
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
onClick = { component.showSolutionsOnDuplicateDownloadUi = true },
|
onClick = { component.showSolutionsOnDuplicateDownloadUi = true },
|
||||||
)
|
)
|
||||||
if (component.shouldShowOpenFile.collectAsState().value) {
|
if (component.shouldShowOpenFile.collectAsState().value) {
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
MainConfigActionButton(
|
MainConfigActionButton(
|
||||||
text = "Open File",
|
text = myStringResource(Res.string.open_file),
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
onClick = { component.openExistingFile() },
|
onClick = { component.openExistingFile() },
|
||||||
)
|
)
|
||||||
@ -469,7 +473,7 @@ private fun MainActionButtons(component: AddSingleDownloadComponent) {
|
|||||||
} else {
|
} else {
|
||||||
val canAddToDownloads by component.canAddToDownloads.collectAsState()
|
val canAddToDownloads by component.canAddToDownloads.collectAsState()
|
||||||
MainConfigActionButton(
|
MainConfigActionButton(
|
||||||
text = "Add",
|
text = myStringResource(Res.string.add),
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
enabled = canAddToDownloads,
|
enabled = canAddToDownloads,
|
||||||
onClick = {
|
onClick = {
|
||||||
@ -478,7 +482,7 @@ private fun MainActionButtons(component: AddSingleDownloadComponent) {
|
|||||||
)
|
)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
PrimaryMainConfigActionButton(
|
PrimaryMainConfigActionButton(
|
||||||
text = "Download",
|
text = myStringResource(Res.string.download),
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
enabled = canAddToDownloads,
|
enabled = canAddToDownloads,
|
||||||
onClick = {
|
onClick = {
|
||||||
@ -488,7 +492,7 @@ private fun MainActionButtons(component: AddSingleDownloadComponent) {
|
|||||||
if (onDuplicateStrategy != null) {
|
if (onDuplicateStrategy != null) {
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
MainConfigActionButton(
|
MainConfigActionButton(
|
||||||
text = "Change solution",
|
text = myStringResource(Res.string.change_solution),
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
onClick = { component.showSolutionsOnDuplicateDownloadUi = true },
|
onClick = { component.showSolutionsOnDuplicateDownloadUi = true },
|
||||||
)
|
)
|
||||||
@ -499,7 +503,7 @@ private fun MainActionButtons(component: AddSingleDownloadComponent) {
|
|||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
MainConfigActionButton(
|
MainConfigActionButton(
|
||||||
text = "Cancel",
|
text = myStringResource(Res.string.cancel),
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
onClick = {
|
onClick = {
|
||||||
component.onRequestClose()
|
component.onRequestClose()
|
||||||
@ -558,10 +562,10 @@ fun RenderFileTypeAndSize(
|
|||||||
}.takeIf {
|
}.takeIf {
|
||||||
// this is a length of a html page (error)
|
// this is a length of a html page (error)
|
||||||
fileInfo.isSuccessFul
|
fileInfo.isSuccessFul
|
||||||
} ?: "unknown"
|
} ?: Res.string.unknown.asStringSource()
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
size,
|
size.rememberString(),
|
||||||
fontSize = myTextSizes.sm,
|
fontSize = myTextSizes.sm,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@ -609,7 +613,7 @@ private fun UrlTextField(
|
|||||||
AddDownloadPageTextField(
|
AddDownloadPageTextField(
|
||||||
text,
|
text,
|
||||||
setText,
|
setText,
|
||||||
"Download link",
|
myStringResource(Res.string.download_link),
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
end = {
|
end = {
|
||||||
MyTextFieldIcon(MyIcons.paste) {
|
MyTextFieldIcon(MyIcons.paste) {
|
||||||
@ -632,7 +636,7 @@ private fun NameTextField(
|
|||||||
AddDownloadPageTextField(
|
AddDownloadPageTextField(
|
||||||
text,
|
text,
|
||||||
setText,
|
setText,
|
||||||
"Name",
|
myStringResource(Res.string.name),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
errorText = errorText,
|
errorText = errorText,
|
||||||
)
|
)
|
||||||
|
@ -10,6 +10,8 @@ import com.abdownloadmanager.desktop.utils.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects
|
import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects
|
||||||
import com.abdownloadmanager.desktop.utils.mvi.supportEffects
|
import com.abdownloadmanager.desktop.utils.mvi.supportEffects
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
import com.abdownloadmanager.utils.extractors.linkextractor.DownloadCredentialFromStringExtractor
|
import com.abdownloadmanager.utils.extractors.linkextractor.DownloadCredentialFromStringExtractor
|
||||||
import com.arkivanov.decompose.ComponentContext
|
import com.arkivanov.decompose.ComponentContext
|
||||||
import ir.amirab.downloader.connection.DownloaderClient
|
import ir.amirab.downloader.connection.DownloaderClient
|
||||||
@ -31,6 +33,8 @@ import com.abdownloadmanager.utils.FileIconProvider
|
|||||||
import com.abdownloadmanager.utils.category.Category
|
import com.abdownloadmanager.utils.category.Category
|
||||||
import com.abdownloadmanager.utils.category.CategoryItem
|
import com.abdownloadmanager.utils.category.CategoryItem
|
||||||
import com.abdownloadmanager.utils.category.CategoryManager
|
import com.abdownloadmanager.utils.category.CategoryManager
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
|
import ir.amirab.util.compose.asStringSourceWithARgs
|
||||||
|
|
||||||
sealed interface AddSingleDownloadPageEffects {
|
sealed interface AddSingleDownloadPageEffects {
|
||||||
data class SuggestUrl(val link: String) : AddSingleDownloadPageEffects
|
data class SuggestUrl(val link: String) : AddSingleDownloadPageEffects
|
||||||
@ -235,17 +239,17 @@ class AddSingleDownloadComponent(
|
|||||||
|
|
||||||
val configurables = listOf(
|
val configurables = listOf(
|
||||||
SpeedLimitConfigurable(
|
SpeedLimitConfigurable(
|
||||||
"Speed Limit",
|
Res.string.download_item_settings_speed_limit.asStringSource(),
|
||||||
"Limit the speed of download for this file",
|
Res.string.download_item_settings_speed_limit_description.asStringSource(),
|
||||||
backedBy = speedLimit,
|
backedBy = speedLimit,
|
||||||
describe = {
|
describe = {
|
||||||
if (it == 0L) "Unlimited"
|
if (it == 0L) Res.string.unlimited.asStringSource()
|
||||||
else convertSpeedToHumanReadable(it)
|
else convertSpeedToHumanReadable(it).asStringSource()
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
IntConfigurable(
|
IntConfigurable(
|
||||||
"Thread count",
|
Res.string.settings_download_thread_count.asStringSource(),
|
||||||
"Limit the threads of download for this file",
|
Res.string.settings_download_thread_count_description.asStringSource(),
|
||||||
backedBy = threadCount.mapTwoWayStateFlow(
|
backedBy = threadCount.mapTwoWayStateFlow(
|
||||||
map = {
|
map = {
|
||||||
it ?: 0
|
it ?: 0
|
||||||
@ -256,13 +260,18 @@ class AddSingleDownloadComponent(
|
|||||||
),
|
),
|
||||||
range = 0..32,
|
range = 0..32,
|
||||||
describe = {
|
describe = {
|
||||||
if (it == 0) "use Global setting"
|
if (it == 0) Res.string.use_global_settings.asStringSource()
|
||||||
else "$it thread for this download"
|
else Res.string.download_item_settings_thread_count_describe
|
||||||
|
.asStringSourceWithARgs(
|
||||||
|
Res.string.download_item_settings_thread_count_describe_createArgs(
|
||||||
|
count = it.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
StringConfigurable(
|
StringConfigurable(
|
||||||
"Username",
|
Res.string.username.asStringSource(),
|
||||||
"username if the link is a protected resource",
|
Res.string.download_item_settings_username_description.asStringSource(),
|
||||||
backedBy = createMutableStateFlowFromStateFlow(
|
backedBy = createMutableStateFlowFromStateFlow(
|
||||||
flow = credentials.mapStateFlow {
|
flow = credentials.mapStateFlow {
|
||||||
it.username.orEmpty()
|
it.username.orEmpty()
|
||||||
@ -272,12 +281,12 @@ class AddSingleDownloadComponent(
|
|||||||
}, scope
|
}, scope
|
||||||
),
|
),
|
||||||
describe = {
|
describe = {
|
||||||
""
|
"".asStringSource()
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
StringConfigurable(
|
StringConfigurable(
|
||||||
"Password",
|
Res.string.password.asStringSource(),
|
||||||
"Password if the link is a protected resource",
|
Res.string.download_item_settings_password_description.asStringSource(),
|
||||||
backedBy = createMutableStateFlowFromStateFlow(
|
backedBy = createMutableStateFlowFromStateFlow(
|
||||||
flow = credentials.mapStateFlow {
|
flow = credentials.mapStateFlow {
|
||||||
it.password.orEmpty()
|
it.password.orEmpty()
|
||||||
@ -287,7 +296,7 @@ class AddSingleDownloadComponent(
|
|||||||
}, scope
|
}, scope
|
||||||
),
|
),
|
||||||
describe = {
|
describe = {
|
||||||
""
|
"".asStringSource()
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -27,16 +27,21 @@ import com.abdownloadmanager.desktop.ui.util.ifThen
|
|||||||
import com.abdownloadmanager.desktop.ui.widget.*
|
import com.abdownloadmanager.desktop.ui.widget.*
|
||||||
import com.abdownloadmanager.desktop.utils.ClipboardUtil
|
import com.abdownloadmanager.desktop.utils.ClipboardUtil
|
||||||
import com.abdownloadmanager.desktop.utils.div
|
import com.abdownloadmanager.desktop.utils.div
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
import com.abdownloadmanager.utils.compose.LocalContentColor
|
import com.abdownloadmanager.utils.compose.LocalContentColor
|
||||||
import com.abdownloadmanager.utils.compose.WithContentAlpha
|
import com.abdownloadmanager.utils.compose.WithContentAlpha
|
||||||
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
import ir.amirab.util.compose.IconSource
|
import ir.amirab.util.compose.IconSource
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BatchDownload(
|
fun BatchDownload(
|
||||||
component: BatchDownloadComponent,
|
component: BatchDownloadComponent,
|
||||||
) {
|
) {
|
||||||
WindowTitle("Batch Download")
|
WindowTitle(myStringResource(Res.string.batch_download))
|
||||||
val link by component.link.collectAsState()
|
val link by component.link.collectAsState()
|
||||||
val setLink = component::setLink
|
val setLink = component::setLink
|
||||||
val start by component.start.collectAsState()
|
val start by component.start.collectAsState()
|
||||||
@ -59,13 +64,13 @@ fun BatchDownload(
|
|||||||
) {
|
) {
|
||||||
LabeledContent(
|
LabeledContent(
|
||||||
label = {
|
label = {
|
||||||
Text("Enter a link that contains wildcards (use *)")
|
Text(myStringResource(Res.string.batch_download_link_help))
|
||||||
},
|
},
|
||||||
content = {
|
content = {
|
||||||
BatchDownloadPageTextField(
|
BatchDownloadPageTextField(
|
||||||
text = link,
|
text = link,
|
||||||
onTextChange = setLink,
|
onTextChange = setLink,
|
||||||
placeholder = "Link: https://example.com/photo-*.png",
|
placeholder = "https://example.com/photo-*.png",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.focusRequester(linkFocusRequester)
|
.focusRequester(linkFocusRequester)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
@ -82,10 +87,15 @@ fun BatchDownload(
|
|||||||
},
|
},
|
||||||
errorText = when (val v = validationResult) {
|
errorText = when (val v = validationResult) {
|
||||||
BatchDownloadValidationResult.URLInvalid -> {
|
BatchDownloadValidationResult.URLInvalid -> {
|
||||||
"Invalid URL"
|
myStringResource(Res.string.invalid_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
is BatchDownloadValidationResult.MaxRangeExceed -> "List is too large! maximum ${v.allowed} items allowed"
|
is BatchDownloadValidationResult.MaxRangeExceed -> myStringResource(
|
||||||
|
Res.string.list_is_too_large_maximum_n_items_allowed,
|
||||||
|
Res.string.list_is_too_large_maximum_n_items_allowed_createArgs(
|
||||||
|
count = v.allowed.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
BatchDownloadValidationResult.Others -> null
|
BatchDownloadValidationResult.Others -> null
|
||||||
BatchDownloadValidationResult.Ok -> null
|
BatchDownloadValidationResult.Ok -> null
|
||||||
}
|
}
|
||||||
@ -95,7 +105,7 @@ fun BatchDownload(
|
|||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
LabeledContent(
|
LabeledContent(
|
||||||
label = {
|
label = {
|
||||||
Text("Enter range")
|
Text(myStringResource(Res.string.enter_range))
|
||||||
},
|
},
|
||||||
content = {
|
content = {
|
||||||
Row(
|
Row(
|
||||||
@ -107,7 +117,7 @@ fun BatchDownload(
|
|||||||
placeholder = "",
|
placeholder = "",
|
||||||
modifier = Modifier.width(90.dp),
|
modifier = Modifier.width(90.dp),
|
||||||
start = {
|
start = {
|
||||||
Text("From:", Modifier.padding(horizontal = 8.dp))
|
Text("${myStringResource(Res.string.from)}:", Modifier.padding(horizontal = 8.dp))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
@ -120,7 +130,7 @@ fun BatchDownload(
|
|||||||
placeholder = "",
|
placeholder = "",
|
||||||
modifier = Modifier.width(90.dp),
|
modifier = Modifier.width(90.dp),
|
||||||
start = {
|
start = {
|
||||||
Text("To:", Modifier.padding(horizontal = 8.dp))
|
Text("${myStringResource(Res.string.to)}:", Modifier.padding(horizontal = 8.dp))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -129,7 +139,7 @@ fun BatchDownload(
|
|||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
LabeledContent(
|
LabeledContent(
|
||||||
label = {
|
label = {
|
||||||
Text("Wildcard length")
|
Text(myStringResource(Res.string.wildcard_length))
|
||||||
},
|
},
|
||||||
content = {
|
content = {
|
||||||
WildcardLengthUi(
|
WildcardLengthUi(
|
||||||
@ -152,7 +162,7 @@ fun BatchDownload(
|
|||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
LabeledContent(
|
LabeledContent(
|
||||||
label = {
|
label = {
|
||||||
Text("First Link")
|
Text(myStringResource(Res.string.first_link))
|
||||||
},
|
},
|
||||||
content = {
|
content = {
|
||||||
LinkPreview(component.startLinkResult.collectAsState().value)
|
LinkPreview(component.startLinkResult.collectAsState().value)
|
||||||
@ -161,7 +171,7 @@ fun BatchDownload(
|
|||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
LabeledContent(
|
LabeledContent(
|
||||||
label = {
|
label = {
|
||||||
Text("Last Link")
|
Text(myStringResource(Res.string.last_link))
|
||||||
},
|
},
|
||||||
content = {
|
content = {
|
||||||
LinkPreview(component.endLinkResult.collectAsState().value)
|
LinkPreview(component.endLinkResult.collectAsState().value)
|
||||||
@ -175,12 +185,12 @@ fun BatchDownload(
|
|||||||
modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End)
|
modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End)
|
||||||
) {
|
) {
|
||||||
ActionButton(
|
ActionButton(
|
||||||
text = "OK",
|
text = myStringResource(Res.string.ok),
|
||||||
enabled = component.canConfirm.collectAsState().value,
|
enabled = component.canConfirm.collectAsState().value,
|
||||||
onClick = component::confirm
|
onClick = component::confirm
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
ActionButton("Cancel", onClick = component.onClose)
|
ActionButton(myStringResource(Res.string.ok), onClick = component.onClose)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -197,8 +207,12 @@ fun LinkPreview(link: String) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class WildcardSelect {
|
enum class WildcardSelect(
|
||||||
Auto, Unspecified, Custom;
|
val text: StringSource,
|
||||||
|
) {
|
||||||
|
Auto(Res.string.auto.asStringSource()),
|
||||||
|
Unspecified(Res.string.unspecified.asStringSource()),
|
||||||
|
Custom(Res.string.custom.asStringSource());
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromWildcardLength(wildcardLength: WildcardLength): WildcardSelect {
|
fun fromWildcardLength(wildcardLength: WildcardLength): WildcardSelect {
|
||||||
@ -235,7 +249,7 @@ private fun WildcardLengthUi(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
render = {
|
render = {
|
||||||
Text(it.toString())
|
Text(it.text.rememberString())
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
AnimatedVisibility(wildcardLength is WildcardLength.Custom) {
|
AnimatedVisibility(wildcardLength is WildcardLength.Custom) {
|
||||||
|
@ -20,11 +20,14 @@ import com.abdownloadmanager.desktop.ui.theme.myTextSizes
|
|||||||
import com.abdownloadmanager.desktop.ui.util.ifThen
|
import com.abdownloadmanager.desktop.ui.util.ifThen
|
||||||
import com.abdownloadmanager.desktop.ui.widget.*
|
import com.abdownloadmanager.desktop.ui.widget.*
|
||||||
import com.abdownloadmanager.desktop.utils.div
|
import com.abdownloadmanager.desktop.utils.div
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
import com.abdownloadmanager.utils.compose.WithContentAlpha
|
import com.abdownloadmanager.utils.compose.WithContentAlpha
|
||||||
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
||||||
import io.github.vinceglb.filekit.compose.rememberDirectoryPickerLauncher
|
import io.github.vinceglb.filekit.compose.rememberDirectoryPickerLauncher
|
||||||
import io.github.vinceglb.filekit.core.FileKitPlatformSettings
|
import io.github.vinceglb.filekit.core.FileKitPlatformSettings
|
||||||
import ir.amirab.util.compose.IconSource
|
import ir.amirab.util.compose.IconSource
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
import ir.amirab.util.desktop.LocalWindow
|
import ir.amirab.util.desktop.LocalWindow
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@ -33,8 +36,13 @@ fun NewCategory(
|
|||||||
categoryComponent: CategoryComponent,
|
categoryComponent: CategoryComponent,
|
||||||
) {
|
) {
|
||||||
WindowTitle(
|
WindowTitle(
|
||||||
if (categoryComponent.isEditMode) "Edit Category"
|
myStringResource(
|
||||||
else "Add Category"
|
if (categoryComponent.isEditMode) {
|
||||||
|
Res.string.edit_category
|
||||||
|
} else {
|
||||||
|
Res.string.add_category
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -80,10 +88,12 @@ fun NewCategory(
|
|||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
Row(Modifier.fillMaxWidth().wrapContentWidth(Alignment.End)) {
|
Row(Modifier.fillMaxWidth().wrapContentWidth(Alignment.End)) {
|
||||||
ActionButton(
|
ActionButton(
|
||||||
when (categoryComponent.isEditMode) {
|
myStringResource(
|
||||||
true -> "Change"
|
when (categoryComponent.isEditMode) {
|
||||||
false -> "Add"
|
true -> Res.string.change
|
||||||
},
|
false -> Res.string.add
|
||||||
|
}
|
||||||
|
),
|
||||||
enabled = categoryComponent.canSubmit.collectAsState().value,
|
enabled = categoryComponent.canSubmit.collectAsState().value,
|
||||||
onClick = {
|
onClick = {
|
||||||
categoryComponent.submit()
|
categoryComponent.submit()
|
||||||
@ -91,7 +101,7 @@ fun NewCategory(
|
|||||||
)
|
)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
ActionButton(
|
ActionButton(
|
||||||
"Cancel",
|
myStringResource(Res.string.cancel),
|
||||||
onClick = {
|
onClick = {
|
||||||
categoryComponent.close()
|
categoryComponent.close()
|
||||||
}
|
}
|
||||||
@ -116,7 +126,7 @@ fun CategoryDefaultPath(
|
|||||||
} ?: defaultDownloadLocation
|
} ?: defaultDownloadLocation
|
||||||
}
|
}
|
||||||
val downloadFolderPickerLauncher = rememberDirectoryPickerLauncher(
|
val downloadFolderPickerLauncher = rememberDirectoryPickerLauncher(
|
||||||
title = "Category Download Location",
|
title = myStringResource(Res.string.category_download_location),
|
||||||
initialDirectory = initialDirectory,
|
initialDirectory = initialDirectory,
|
||||||
platformSettings = FileKitPlatformSettings(
|
platformSettings = FileKitPlatformSettings(
|
||||||
parentWindow = LocalWindow.current
|
parentWindow = LocalWindow.current
|
||||||
@ -126,8 +136,8 @@ fun CategoryDefaultPath(
|
|||||||
}
|
}
|
||||||
|
|
||||||
WithLabel(
|
WithLabel(
|
||||||
"Category Download Location",
|
label = myStringResource(Res.string.category_download_location),
|
||||||
helpText = """When this category chosen in "Add Download Page" use this directory as "Download Location"""
|
helpText = myStringResource(Res.string.category_download_location_description)
|
||||||
) {
|
) {
|
||||||
CategoryPageTextField(
|
CategoryPageTextField(
|
||||||
text = path,
|
text = path,
|
||||||
@ -150,14 +160,14 @@ fun CategoryAutoTypes(
|
|||||||
onTypesChanged: (String) -> Unit,
|
onTypesChanged: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
WithLabel(
|
WithLabel(
|
||||||
label = "Category file types",
|
label = myStringResource(Res.string.category_file_types),
|
||||||
helpText = "Automatically put these file types to this category. (when you add new download)\nSeparate file extensions with space (ext1 ext2 ...) "
|
helpText = myStringResource(Res.string.category_file_types_description)
|
||||||
) {
|
) {
|
||||||
CategoryPageTextField(
|
CategoryPageTextField(
|
||||||
text = types,
|
text = types,
|
||||||
onTextChange = onTypesChanged,
|
onTextChange = onTypesChanged,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
placeholder = "ext1 ext2 ext3 (separate with space)",
|
placeholder = "ext1 ext2 ext3",
|
||||||
singleLine = false,
|
singleLine = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -171,8 +181,8 @@ fun CategoryAutoUrls(
|
|||||||
onUrlPatternChanged: (String) -> Unit,
|
onUrlPatternChanged: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
OptionalWithLabel(
|
OptionalWithLabel(
|
||||||
label = "URL patterns",
|
label = myStringResource(Res.string.url_patterns),
|
||||||
helpText = "Automatically put download from these URLs to this category. (when you add new download)\nSeparate URLs with space, you can also use * for wildcard",
|
helpText = myStringResource(Res.string.url_patterns_description),
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
setEnabled = setEnabled
|
setEnabled = setEnabled
|
||||||
) {
|
) {
|
||||||
@ -194,7 +204,7 @@ fun CategoryName(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
WithLabel(
|
WithLabel(
|
||||||
"Category Name",
|
myStringResource(Res.string.category_name),
|
||||||
modifier,
|
modifier,
|
||||||
) {
|
) {
|
||||||
CategoryPageTextField(
|
CategoryPageTextField(
|
||||||
@ -266,7 +276,7 @@ private fun CategoryIcon(
|
|||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
WithLabel(
|
WithLabel(
|
||||||
"Icon"
|
myStringResource(Res.string.icon)
|
||||||
) {
|
) {
|
||||||
RenderIcon(
|
RenderIcon(
|
||||||
icon = iconSource,
|
icon = iconSource,
|
||||||
|
@ -8,6 +8,9 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.rememberWindowState
|
import androidx.compose.ui.window.rememberWindowState
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ShowOpenSourceLibraries(appComponent: AppComponent){
|
fun ShowOpenSourceLibraries(appComponent: AppComponent){
|
||||||
@ -31,7 +34,7 @@ fun ShowOpenSourceLibraries(
|
|||||||
size = DpSize(650.dp, 400.dp)
|
size = DpSize(650.dp, 400.dp)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
WindowTitle("Open Source ThirdParty Libraries")
|
WindowTitle(myStringResource(Res.string.open_source_software_used_in_this_app))
|
||||||
ExternalLibsPage()
|
ExternalLibsPage()
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -21,10 +21,15 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
import com.mikepenz.aboutlibraries.entity.Developer
|
import com.mikepenz.aboutlibraries.entity.Developer
|
||||||
import com.mikepenz.aboutlibraries.entity.Library
|
import com.mikepenz.aboutlibraries.entity.Library
|
||||||
import com.mikepenz.aboutlibraries.entity.License
|
import com.mikepenz.aboutlibraries.entity.License
|
||||||
import com.mikepenz.aboutlibraries.entity.Organization
|
import com.mikepenz.aboutlibraries.entity.Organization
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -67,10 +72,10 @@ fun LibraryDialog(
|
|||||||
}
|
}
|
||||||
val links = buildList {
|
val links = buildList {
|
||||||
library.scm?.url?.let {
|
library.scm?.url?.let {
|
||||||
add("SourceCode" to it)
|
add(Res.string.source_code.asStringSource() to it)
|
||||||
}
|
}
|
||||||
library.website?.let {
|
library.website?.let {
|
||||||
add("Website" to it)
|
add(Res.string.website.asStringSource() to it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
links.takeIf { it.isNotEmpty() }?.let {
|
links.takeIf { it.isNotEmpty() }?.let {
|
||||||
@ -83,7 +88,7 @@ fun LibraryDialog(
|
|||||||
Modifier.fillMaxWidth(),
|
Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.End,
|
horizontalArrangement = Arrangement.End,
|
||||||
) {
|
) {
|
||||||
ActionButton("Close", onClick = {
|
ActionButton(myStringResource(Res.string.close), onClick = {
|
||||||
onCloseRequest()
|
onCloseRequest()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -93,8 +98,8 @@ fun LibraryDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LibraryLinks(links: List<Pair<String, String>>) {
|
private fun LibraryLinks(links: List<Pair<StringSource, String>>) {
|
||||||
KeyValue("Links") {
|
KeyValue(myStringResource(Res.string.links)) {
|
||||||
ListOfNamesWithLinks(links)
|
ListOfNamesWithLinks(links)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -110,12 +115,12 @@ private fun LibraryDescription(description: String) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LibraryLicenseInfo(licenses: ImmutableSet<License>) {
|
private fun LibraryLicenseInfo(licenses: ImmutableSet<License>) {
|
||||||
KeyValue("License") {
|
KeyValue(myStringResource(Res.string.license)) {
|
||||||
val l = licenses.map {
|
val l = licenses.map {
|
||||||
it.name to it.url
|
it.name.asStringSource() to it.url
|
||||||
}
|
}
|
||||||
if (l.isEmpty()) {
|
if (l.isEmpty()) {
|
||||||
Text("no license found")
|
Text(myStringResource(Res.string.no_license_found))
|
||||||
} else {
|
} else {
|
||||||
ListOfNamesWithLinks(l)
|
ListOfNamesWithLinks(l)
|
||||||
}
|
}
|
||||||
@ -124,23 +129,23 @@ private fun LibraryLicenseInfo(licenses: ImmutableSet<License>) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LibraryDevelopers(devs: List<Developer>) {
|
private fun LibraryDevelopers(devs: List<Developer>) {
|
||||||
KeyValue("Developers") {
|
KeyValue(myStringResource(Res.string.developers)) {
|
||||||
ListOfNamesWithLinks(
|
ListOfNamesWithLinks(
|
||||||
devs
|
devs
|
||||||
.filter { it.name != null }
|
.filter { it.name != null }
|
||||||
.map {
|
.map {
|
||||||
it.name!! to it.organisationUrl
|
it.name!!.asStringSource() to it.organisationUrl
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ListOfNamesWithLinks(map: List<Pair<String, String?>>) {
|
private fun ListOfNamesWithLinks(map: List<Pair<StringSource, String?>>) {
|
||||||
Row {
|
Row {
|
||||||
for ((i, v) in map.withIndex()) {
|
for ((i, v) in map.withIndex()) {
|
||||||
val (name, link) = v
|
val (name, link) = v
|
||||||
MaybeLinkText(name, link)
|
MaybeLinkText(name.rememberString(), link)
|
||||||
if (i < map.lastIndex) {
|
if (i < map.lastIndex) {
|
||||||
Text(", ")
|
Text(", ")
|
||||||
}
|
}
|
||||||
@ -150,7 +155,7 @@ private fun ListOfNamesWithLinks(map: List<Pair<String, String?>>) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryOrganization(organization: Organization) {
|
fun LibraryOrganization(organization: Organization) {
|
||||||
KeyValue("Organization") {
|
KeyValue(myStringResource(Res.string.organization)) {
|
||||||
MaybeLinkText(organization.name, organization.url)
|
MaybeLinkText(organization.name, organization.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,27 +4,33 @@ import com.abdownloadmanager.desktop.ui.widget.customtable.CellSize
|
|||||||
import com.abdownloadmanager.desktop.ui.widget.customtable.SortableCell
|
import com.abdownloadmanager.desktop.ui.widget.customtable.SortableCell
|
||||||
import com.abdownloadmanager.desktop.ui.widget.customtable.TableCell
|
import com.abdownloadmanager.desktop.ui.widget.customtable.TableCell
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
import com.mikepenz.aboutlibraries.entity.Library
|
import com.mikepenz.aboutlibraries.entity.Library
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
|
|
||||||
sealed interface LibraryCells : TableCell<Library> {
|
sealed interface LibraryCells : TableCell<Library> {
|
||||||
data object Name : LibraryCells,
|
data object Name : LibraryCells,
|
||||||
SortableCell<Library> {
|
SortableCell<Library> {
|
||||||
override fun sortBy(item: Library): Comparable<*> = item.name
|
override fun sortBy(item: Library): Comparable<*> = item.name
|
||||||
override val name: String = "Name"
|
override val id: String = "Name"
|
||||||
|
override val name: StringSource = Res.string.name.asStringSource()
|
||||||
override val size: CellSize = CellSize.Resizeable(100.dp..1000.dp,250.dp)
|
override val size: CellSize = CellSize.Resizeable(100.dp..1000.dp,250.dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object Author : LibraryCells,
|
data object Author : LibraryCells,
|
||||||
SortableCell<Library> {
|
SortableCell<Library> {
|
||||||
override fun sortBy(item: Library): Comparable<*> = item.licenses.firstOrNull()?.name.orEmpty()
|
override fun sortBy(item: Library): Comparable<*> = item.licenses.firstOrNull()?.name.orEmpty()
|
||||||
override val name: String = "Author"
|
override val id: String = "Author"
|
||||||
|
override val name: StringSource = Res.string.author.asStringSource()
|
||||||
override val size: CellSize = CellSize.Resizeable(100.dp..200.dp, 150.dp)
|
override val size: CellSize = CellSize.Resizeable(100.dp..200.dp, 150.dp)
|
||||||
}
|
}
|
||||||
data object License : LibraryCells,
|
data object License : LibraryCells,
|
||||||
SortableCell<Library> {
|
SortableCell<Library> {
|
||||||
override fun sortBy(item: Library): Comparable<*> = item.licenses.firstOrNull()?.name.orEmpty()
|
override fun sortBy(item: Library): Comparable<*> = item.licenses.firstOrNull()?.name.orEmpty()
|
||||||
|
|
||||||
override val name: String = "License"
|
override val id: String = "License"
|
||||||
|
override val name: StringSource = Res.string.license.asStringSource()
|
||||||
override val size: CellSize = CellSize.Resizeable(100.dp..200.dp, 150.dp)
|
override val size: CellSize = CellSize.Resizeable(100.dp..200.dp, 150.dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ private fun ActionButton(
|
|||||||
MyIcon(it, null, Modifier.size(16.dp))
|
MyIcon(it, null, Modifier.size(16.dp))
|
||||||
}
|
}
|
||||||
Spacer(Modifier.size(2.dp))
|
Spacer(Modifier.size(2.dp))
|
||||||
Text(title, maxLines = 1, fontSize = myTextSizes.sm)
|
Text(title.rememberString(), maxLines = 1, fontSize = myTextSizes.sm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -120,7 +120,7 @@ private fun GroupActionButton(
|
|||||||
MyIcon(it, null, Modifier.size(16.dp))
|
MyIcon(it, null, Modifier.size(16.dp))
|
||||||
}
|
}
|
||||||
Spacer(Modifier.size(2.dp))
|
Spacer(Modifier.size(2.dp))
|
||||||
Text(title, maxLines = 1, fontSize = myTextSizes.sm)
|
Text(title.rememberString(), maxLines = 1, fontSize = myTextSizes.sm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,8 @@ import androidx.compose.ui.unit.DpSize
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.abdownloadmanager.desktop.pages.category.CategoryDialogManager
|
import com.abdownloadmanager.desktop.pages.category.CategoryDialogManager
|
||||||
import com.abdownloadmanager.desktop.storage.AppSettingsStorage
|
import com.abdownloadmanager.desktop.storage.AppSettingsStorage
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
import com.abdownloadmanager.utils.FileIconProvider
|
import com.abdownloadmanager.utils.FileIconProvider
|
||||||
import com.abdownloadmanager.utils.category.Category
|
import com.abdownloadmanager.utils.category.Category
|
||||||
import com.abdownloadmanager.utils.category.CategoryItemWithId
|
import com.abdownloadmanager.utils.category.CategoryItemWithId
|
||||||
@ -37,6 +39,8 @@ import ir.amirab.util.flow.combineStateFlows
|
|||||||
import ir.amirab.util.flow.mapStateFlow
|
import ir.amirab.util.flow.mapStateFlow
|
||||||
import ir.amirab.util.flow.mapTwoWayStateFlow
|
import ir.amirab.util.flow.mapTwoWayStateFlow
|
||||||
import com.abdownloadmanager.utils.extractors.linkextractor.DownloadCredentialFromStringExtractor
|
import com.abdownloadmanager.utils.extractors.linkextractor.DownloadCredentialFromStringExtractor
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
|
import ir.amirab.util.compose.asStringSourceWithARgs
|
||||||
import ir.amirab.util.osfileutil.FileUtils
|
import ir.amirab.util.osfileutil.FileUtils
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
@ -44,7 +48,6 @@ import kotlinx.coroutines.launch
|
|||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class FilterState {
|
class FilterState {
|
||||||
@ -102,7 +105,7 @@ class DownloadActions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val openFileAction = simpleAction(
|
val openFileAction = simpleAction(
|
||||||
title = "Open",
|
title = Res.string.open.asStringSource(),
|
||||||
icon = MyIcons.fileOpen,
|
icon = MyIcons.fileOpen,
|
||||||
checkEnable = defaultItem.mapStateFlow {
|
checkEnable = defaultItem.mapStateFlow {
|
||||||
it?.statusOrFinished() is DownloadJobStatus.Finished
|
it?.statusOrFinished() is DownloadJobStatus.Finished
|
||||||
@ -116,7 +119,7 @@ class DownloadActions(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val openFolderAction = simpleAction(
|
val openFolderAction = simpleAction(
|
||||||
title = "Open Folder",
|
title = Res.string.open_folder.asStringSource(),
|
||||||
icon = MyIcons.folderOpen,
|
icon = MyIcons.folderOpen,
|
||||||
checkEnable = defaultItem.mapStateFlow {
|
checkEnable = defaultItem.mapStateFlow {
|
||||||
it?.statusOrFinished() is DownloadJobStatus.Finished
|
it?.statusOrFinished() is DownloadJobStatus.Finished
|
||||||
@ -130,7 +133,7 @@ class DownloadActions(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val deleteAction = simpleAction(
|
val deleteAction = simpleAction(
|
||||||
title = "Delete",
|
title = Res.string.delete.asStringSource(),
|
||||||
icon = MyIcons.remove,
|
icon = MyIcons.remove,
|
||||||
checkEnable = selections.mapStateFlow { it.isNotEmpty() },
|
checkEnable = selections.mapStateFlow { it.isNotEmpty() },
|
||||||
onActionPerformed = {
|
onActionPerformed = {
|
||||||
@ -141,7 +144,7 @@ class DownloadActions(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val resumeAction = simpleAction(
|
val resumeAction = simpleAction(
|
||||||
title = "Resume",
|
title = Res.string.resume.asStringSource(),
|
||||||
icon = MyIcons.resume,
|
icon = MyIcons.resume,
|
||||||
checkEnable = resumableSelections.mapStateFlow {
|
checkEnable = resumableSelections.mapStateFlow {
|
||||||
it.isNotEmpty()
|
it.isNotEmpty()
|
||||||
@ -158,7 +161,7 @@ class DownloadActions(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val reDownloadAction = simpleAction(
|
val reDownloadAction = simpleAction(
|
||||||
"Restart Download",
|
Res.string.restart_download.asStringSource(),
|
||||||
MyIcons.refresh
|
MyIcons.refresh
|
||||||
) {
|
) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@ -174,7 +177,7 @@ class DownloadActions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val pauseAction = simpleAction(
|
val pauseAction = simpleAction(
|
||||||
title = "Pause",
|
title = Res.string.pause.asStringSource(),
|
||||||
icon = MyIcons.pause,
|
icon = MyIcons.pause,
|
||||||
checkEnable = pausableSelections.mapStateFlow {
|
checkEnable = pausableSelections.mapStateFlow {
|
||||||
it.isNotEmpty()
|
it.isNotEmpty()
|
||||||
@ -191,7 +194,7 @@ class DownloadActions(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val copyDownloadLinkAction = simpleAction(
|
val copyDownloadLinkAction = simpleAction(
|
||||||
title = "Copy link",
|
title = Res.string.copy_link.asStringSource(),
|
||||||
icon = MyIcons.copy,
|
icon = MyIcons.copy,
|
||||||
checkEnable =
|
checkEnable =
|
||||||
selections.mapStateFlow { it.isNotEmpty() },
|
selections.mapStateFlow { it.isNotEmpty() },
|
||||||
@ -206,7 +209,7 @@ class DownloadActions(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val openDownloadDialogAction = simpleAction("Show Properties", MyIcons.info) {
|
val openDownloadDialogAction = simpleAction(Res.string.show_properties.asStringSource(), MyIcons.info) {
|
||||||
selections.value.map { it.id }
|
selections.value.map { it.id }
|
||||||
.forEach { id ->
|
.forEach { id ->
|
||||||
downloadDialogManager.openDownloadDialog(id)
|
downloadDialogManager.openDownloadDialog(id)
|
||||||
@ -214,7 +217,7 @@ class DownloadActions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val moveToQueueItems = MenuItem.SubMenu(
|
private val moveToQueueItems = MenuItem.SubMenu(
|
||||||
title = "Move To Queue",
|
title = Res.string.move_to_queue.asStringSource(),
|
||||||
items = emptyList()
|
items = emptyList()
|
||||||
).apply {
|
).apply {
|
||||||
merge(
|
merge(
|
||||||
@ -229,7 +232,7 @@ class DownloadActions(
|
|||||||
}.launchIn(scope)
|
}.launchIn(scope)
|
||||||
}
|
}
|
||||||
private val moveToCategoryAction = MenuItem.SubMenu(
|
private val moveToCategoryAction = MenuItem.SubMenu(
|
||||||
title = "Move To Category",
|
title = Res.string.move_to_category.asStringSource(),
|
||||||
items = emptyList()
|
items = emptyList()
|
||||||
).apply {
|
).apply {
|
||||||
merge(
|
merge(
|
||||||
@ -293,7 +296,7 @@ class CategoryActions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val openCategoryFolderAction = simpleAction(
|
val openCategoryFolderAction = simpleAction(
|
||||||
title = "Open Folder",
|
title = Res.string.open_folder.asStringSource(),
|
||||||
icon = MyIcons.folderOpen,
|
icon = MyIcons.folderOpen,
|
||||||
checkEnable = mainItemExists,
|
checkEnable = mainItemExists,
|
||||||
onActionPerformed = {
|
onActionPerformed = {
|
||||||
@ -306,7 +309,7 @@ class CategoryActions(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val deleteAction = simpleAction(
|
val deleteAction = simpleAction(
|
||||||
title = "Delete Category",
|
title = Res.string.delete_category.asStringSource(),
|
||||||
icon = MyIcons.remove,
|
icon = MyIcons.remove,
|
||||||
checkEnable = mainItemExists,
|
checkEnable = mainItemExists,
|
||||||
onActionPerformed = {
|
onActionPerformed = {
|
||||||
@ -318,7 +321,7 @@ class CategoryActions(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
val editAction = simpleAction(
|
val editAction = simpleAction(
|
||||||
title = "Edit Category",
|
title = Res.string.edit_category.asStringSource(),
|
||||||
icon = MyIcons.settings,
|
icon = MyIcons.settings,
|
||||||
checkEnable = mainItemExists,
|
checkEnable = mainItemExists,
|
||||||
onActionPerformed = {
|
onActionPerformed = {
|
||||||
@ -331,7 +334,7 @@ class CategoryActions(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val addCategoryAction = simpleAction(
|
val addCategoryAction = simpleAction(
|
||||||
title = "Add Category",
|
title = Res.string.add_category.asStringSource(),
|
||||||
icon = MyIcons.add,
|
icon = MyIcons.add,
|
||||||
onActionPerformed = {
|
onActionPerformed = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@ -340,7 +343,7 @@ class CategoryActions(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
val categorizeItemsAction = simpleAction(
|
val categorizeItemsAction = simpleAction(
|
||||||
title = "Auto Categorise Items",
|
title = Res.string.auto_categorize_downloads.asStringSource(),
|
||||||
icon = MyIcons.refresh,
|
icon = MyIcons.refresh,
|
||||||
onActionPerformed = {
|
onActionPerformed = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@ -349,7 +352,7 @@ class CategoryActions(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
val resetToDefaultAction = simpleAction(
|
val resetToDefaultAction = simpleAction(
|
||||||
title = "Restore Defaults",
|
title = Res.string.restore_defaults.asStringSource(),
|
||||||
icon = MyIcons.undo,
|
icon = MyIcons.undo,
|
||||||
checkEnable = categoryManager
|
checkEnable = categoryManager
|
||||||
.categoriesFlow
|
.categoriesFlow
|
||||||
@ -496,7 +499,7 @@ class HomeComponent(
|
|||||||
|
|
||||||
|
|
||||||
val menu: List<MenuItem.SubMenu> = buildMenu {
|
val menu: List<MenuItem.SubMenu> = buildMenu {
|
||||||
subMenu("File") {
|
subMenu(Res.string.file.asStringSource()) {
|
||||||
+newDownloadAction
|
+newDownloadAction
|
||||||
+newDownloadFromClipboardAction
|
+newDownloadFromClipboardAction
|
||||||
+batchDownloadAction
|
+batchDownloadAction
|
||||||
@ -504,7 +507,7 @@ class HomeComponent(
|
|||||||
+exitAction
|
+exitAction
|
||||||
|
|
||||||
}
|
}
|
||||||
subMenu("Tasks") {
|
subMenu(Res.string.tasks.asStringSource()) {
|
||||||
// +toggleQueueAction
|
// +toggleQueueAction
|
||||||
+startQueueGroupAction
|
+startQueueGroupAction
|
||||||
+stopQueueGroupAction
|
+stopQueueGroupAction
|
||||||
@ -512,21 +515,21 @@ class HomeComponent(
|
|||||||
+stopAllAction
|
+stopAllAction
|
||||||
separator()
|
separator()
|
||||||
subMenu(
|
subMenu(
|
||||||
title = "Remove",
|
title = Res.string.delete.asStringSource(),
|
||||||
icon = MyIcons.remove
|
icon = MyIcons.remove
|
||||||
) {
|
) {
|
||||||
item("All Finished") {
|
item(Res.string.all_finished.asStringSource()) {
|
||||||
requestDelete(downloadSystem.getFinishedDownloadIds())
|
requestDelete(downloadSystem.getFinishedDownloadIds())
|
||||||
}
|
}
|
||||||
item("All Unfinished") {
|
item(Res.string.all_unfinished.asStringSource()) {
|
||||||
requestDelete(downloadSystem.getUnfinishedDownloadIds())
|
requestDelete(downloadSystem.getUnfinishedDownloadIds())
|
||||||
}
|
}
|
||||||
item("Entire List") {
|
item(Res.string.entire_list.asStringSource()) {
|
||||||
requestDelete(downloadSystem.getAllDownloadIds())
|
requestDelete(downloadSystem.getAllDownloadIds())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
subMenu("Tools") {
|
subMenu(Res.string.tools.asStringSource()) {
|
||||||
if (AppInfo.isInDebugMode()) {
|
if (AppInfo.isInDebugMode()) {
|
||||||
+dummyException
|
+dummyException
|
||||||
+dummyMessage
|
+dummyMessage
|
||||||
@ -536,7 +539,7 @@ class HomeComponent(
|
|||||||
separator()
|
separator()
|
||||||
+gotoSettingsAction
|
+gotoSettingsAction
|
||||||
}
|
}
|
||||||
subMenu("Help") {
|
subMenu(Res.string.help.asStringSource()) {
|
||||||
//TODO Enable Updater
|
//TODO Enable Updater
|
||||||
// +checkForUpdateAction
|
// +checkForUpdateAction
|
||||||
+supportActionGroup
|
+supportActionGroup
|
||||||
@ -558,9 +561,15 @@ class HomeComponent(
|
|||||||
MenuItem.SubMenu(
|
MenuItem.SubMenu(
|
||||||
icon = null,
|
icon = null,
|
||||||
title = if (selectionList.size == 1) {
|
title = if (selectionList.size == 1) {
|
||||||
downloadActions.defaultItem.value?.name ?: ""
|
(downloadActions.defaultItem.value?.name ?: "")
|
||||||
|
.asStringSource()
|
||||||
} else {
|
} else {
|
||||||
"${selectionList.size} Selected"
|
Res.string.n_items_selected
|
||||||
|
.asStringSourceWithARgs(
|
||||||
|
Res.string.n_items_selected_createArgs(
|
||||||
|
count = selectionList.size.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
items = downloadActions.menu
|
items = downloadActions.menu
|
||||||
)
|
)
|
||||||
@ -786,9 +795,9 @@ class HomeComponent(
|
|||||||
val dItem = downloadSystem.getDownloadItemById(id) ?: return@launch
|
val dItem = downloadSystem.getDownloadItemById(id) ?: return@launch
|
||||||
if (dItem.status != DownloadStatus.Completed) {
|
if (dItem.status != DownloadStatus.Completed) {
|
||||||
notificationSender.sendNotification(
|
notificationSender.sendNotification(
|
||||||
"Open File",
|
Res.string.open_file,
|
||||||
"Can't open file",
|
Res.string.cant_open_file.asStringSource(),
|
||||||
"Not finished",
|
Res.string.not_finished.asStringSource(),
|
||||||
NotificationType.Error,
|
NotificationType.Error,
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
|
@ -44,9 +44,15 @@ import androidx.compose.ui.platform.LocalWindowInfo
|
|||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import com.abdownloadmanager.desktop.ui.customwindow.*
|
import com.abdownloadmanager.desktop.ui.customwindow.*
|
||||||
import com.abdownloadmanager.desktop.ui.widget.menu.ShowOptionsInDropDown
|
import com.abdownloadmanager.desktop.ui.widget.menu.ShowOptionsInDropDown
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
import com.abdownloadmanager.utils.category.Category
|
import com.abdownloadmanager.utils.category.Category
|
||||||
import com.abdownloadmanager.utils.category.rememberIconPainter
|
import com.abdownloadmanager.utils.category.rememberIconPainter
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
import ir.amirab.util.compose.action.MenuItem
|
import ir.amirab.util.compose.action.MenuItem
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
|
import ir.amirab.util.compose.localizationmanager.WithLanguageDirection
|
||||||
import java.awt.datatransfer.DataFlavor
|
import java.awt.datatransfer.DataFlavor
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@ -54,7 +60,6 @@ import java.io.File
|
|||||||
@Composable
|
@Composable
|
||||||
fun HomePage(component: HomeComponent) {
|
fun HomePage(component: HomeComponent) {
|
||||||
val listState by component.downloadList.collectAsState()
|
val listState by component.downloadList.collectAsState()
|
||||||
WindowTitle(AppInfo.name)
|
|
||||||
var isDragging by remember { mutableStateOf(false) }
|
var isDragging by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
var showDeletePromptState by remember {
|
var showDeletePromptState by remember {
|
||||||
@ -84,16 +89,16 @@ fun HomePage(component: HomeComponent) {
|
|||||||
|
|
||||||
is HomeEffects.AutoCategorize -> {
|
is HomeEffects.AutoCategorize -> {
|
||||||
showConfirmPrompt = ConfirmPromptState(
|
showConfirmPrompt = ConfirmPromptState(
|
||||||
title = "Auto categorize downloads",
|
title = Res.string.confirm_auto_categorize_downloads_title.asStringSource(),
|
||||||
description = "Any uncategorized item will be automatically added to it's related category.",
|
description = Res.string.confirm_auto_categorize_downloads_description.asStringSource(),
|
||||||
onConfirm = component::onConfirmAutoCategorize
|
onConfirm = component::onConfirmAutoCategorize
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is HomeEffects.ResetCategoriesToDefault -> {
|
is HomeEffects.ResetCategoriesToDefault -> {
|
||||||
showConfirmPrompt = ConfirmPromptState(
|
showConfirmPrompt = ConfirmPromptState(
|
||||||
title = "Reset to Default Categories",
|
title = Res.string.confirm_reset_to_default_categories_title.asStringSource(),
|
||||||
description = "this will REMOVE all categories and brings backs default categories",
|
description = Res.string.confirm_reset_to_default_categories_description.asStringSource(),
|
||||||
onConfirm = component::onConfirmResetCategories
|
onConfirm = component::onConfirmResetCategories
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -207,9 +212,11 @@ fun HomePage(component: HomeComponent) {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (!mergeTopBar) {
|
if (!mergeTopBar) {
|
||||||
Spacer(Modifier.height(4.dp))
|
WithTitleBarDirection {
|
||||||
TopBar(component)
|
Spacer(Modifier.height(4.dp))
|
||||||
Spacer(Modifier.height(6.dp))
|
TopBar(component)
|
||||||
|
Spacer(Modifier.height(6.dp))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer(
|
Spacer(
|
||||||
Modifier.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
@ -340,14 +347,19 @@ private fun ShowDeletePrompts(
|
|||||||
.widthIn(max = 260.dp)
|
.widthIn(max = 260.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"Confirm Delete",
|
myStringResource(Res.string.confirm_delete_download_items_title),
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = myTextSizes.xl,
|
fontSize = myTextSizes.xl,
|
||||||
color = myColors.onBackground,
|
color = myColors.onBackground,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
Text(
|
Text(
|
||||||
"Are you sure you want to delete ${deletePromptState.downloadList.size} item ?",
|
myStringResource(
|
||||||
|
Res.string.confirm_delete_download_items_description,
|
||||||
|
Res.string.confirm_delete_download_items_description_createArgs(
|
||||||
|
count = deletePromptState.downloadList.size.toString()
|
||||||
|
),
|
||||||
|
),
|
||||||
fontSize = myTextSizes.base,
|
fontSize = myTextSizes.base,
|
||||||
color = myColors.onBackground,
|
color = myColors.onBackground,
|
||||||
)
|
)
|
||||||
@ -366,7 +378,7 @@ private fun ShowDeletePrompts(
|
|||||||
})
|
})
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
"Also delete file from disk",
|
myStringResource(Res.string.also_delete_file_from_disk),
|
||||||
fontSize = myTextSizes.base,
|
fontSize = myTextSizes.base,
|
||||||
color = myColors.onBackground,
|
color = myColors.onBackground,
|
||||||
)
|
)
|
||||||
@ -378,13 +390,13 @@ private fun ShowDeletePrompts(
|
|||||||
) {
|
) {
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
ActionButton(
|
ActionButton(
|
||||||
text = "Delete",
|
text = myStringResource(Res.string.delete),
|
||||||
onClick = onConfirm,
|
onClick = onConfirm,
|
||||||
borderColor = SolidColor(myColors.error),
|
borderColor = SolidColor(myColors.error),
|
||||||
contentColor = myColors.error,
|
contentColor = myColors.error,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
ActionButton(text = "Cancel", onClick = onCancel)
|
ActionButton(text = myStringResource(Res.string.cancel), onClick = onCancel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -415,14 +427,14 @@ private fun ShowConfirmPrompt(
|
|||||||
.widthIn(max = 260.dp)
|
.widthIn(max = 260.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = promptState.title,
|
text = promptState.title.rememberString(),
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = myTextSizes.xl,
|
fontSize = myTextSizes.xl,
|
||||||
color = myColors.onBackground,
|
color = myColors.onBackground,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
Text(
|
Text(
|
||||||
text = promptState.description,
|
text = promptState.description.rememberString(),
|
||||||
fontSize = myTextSizes.base,
|
fontSize = myTextSizes.base,
|
||||||
color = myColors.onBackground,
|
color = myColors.onBackground,
|
||||||
)
|
)
|
||||||
@ -433,13 +445,12 @@ private fun ShowConfirmPrompt(
|
|||||||
) {
|
) {
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
ActionButton(
|
ActionButton(
|
||||||
text = "Delete",
|
text = myStringResource(Res.string.ok),
|
||||||
onClick = onConfirm,
|
onClick = onConfirm,
|
||||||
borderColor = SolidColor(myColors.error),
|
|
||||||
contentColor = myColors.error,
|
contentColor = myColors.error,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
ActionButton(text = "Cancel", onClick = onCancel)
|
ActionButton(text = myStringResource(Res.string.cancel), onClick = onCancel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -470,20 +481,30 @@ private fun ShowDeleteCategoryPrompt(
|
|||||||
.widthIn(max = 260.dp)
|
.widthIn(max = 260.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"""Removing "${deletePromptState.category.name}" Category""",
|
myStringResource(
|
||||||
|
Res.string.confirm_delete_category_item_title,
|
||||||
|
Res.string.confirm_delete_category_item_title_createArgs(
|
||||||
|
name = deletePromptState.category.name
|
||||||
|
),
|
||||||
|
),
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = myTextSizes.xl,
|
fontSize = myTextSizes.xl,
|
||||||
color = myColors.onBackground,
|
color = myColors.onBackground,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
Text(
|
Text(
|
||||||
"""Are you sure you want to delete "${deletePromptState.category.name}" Category ?""",
|
myStringResource(
|
||||||
|
Res.string.confirm_delete_category_item_description,
|
||||||
|
Res.string.confirm_delete_category_item_description_createArgs(
|
||||||
|
value = deletePromptState.category.name
|
||||||
|
)
|
||||||
|
),
|
||||||
fontSize = myTextSizes.base,
|
fontSize = myTextSizes.base,
|
||||||
color = myColors.onBackground,
|
color = myColors.onBackground,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
Text(
|
Text(
|
||||||
"Your downloads won't be deleted",
|
myStringResource(Res.string.your_download_will_not_be_deleted),
|
||||||
fontSize = myTextSizes.base,
|
fontSize = myTextSizes.base,
|
||||||
color = myColors.onBackground,
|
color = myColors.onBackground,
|
||||||
)
|
)
|
||||||
@ -494,13 +515,13 @@ private fun ShowDeleteCategoryPrompt(
|
|||||||
) {
|
) {
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
ActionButton(
|
ActionButton(
|
||||||
text = "Delete",
|
text = myStringResource(Res.string.delete),
|
||||||
onClick = onConfirm,
|
onClick = onConfirm,
|
||||||
borderColor = SolidColor(myColors.error),
|
borderColor = SolidColor(myColors.error),
|
||||||
contentColor = myColors.error,
|
contentColor = myColors.error,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
ActionButton(text = "Cancel", onClick = onCancel)
|
ActionButton(text = myStringResource(Res.string.cancel), onClick = onCancel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -520,8 +541,8 @@ data class CategoryDeletePromptState(
|
|||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class ConfirmPromptState(
|
data class ConfirmPromptState(
|
||||||
val title: String,
|
val title: StringSource,
|
||||||
val description: String,
|
val description: StringSource,
|
||||||
val onConfirm: () -> Unit,
|
val onConfirm: () -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -553,21 +574,26 @@ fun DragWidget(
|
|||||||
Modifier.size(36.dp),
|
Modifier.size(36.dp),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Drop link or file here.",
|
text = myStringResource(Res.string.drop_link_or_file_here),
|
||||||
fontSize = myTextSizes.xl
|
fontSize = myTextSizes.xl
|
||||||
)
|
)
|
||||||
if (linkCount != null) {
|
if (linkCount != null) {
|
||||||
when {
|
when {
|
||||||
linkCount > 0 -> {
|
linkCount > 0 -> {
|
||||||
Text(
|
Text(
|
||||||
"$linkCount links will be imported",
|
myStringResource(
|
||||||
|
Res.string.n_links_will_be_imported,
|
||||||
|
Res.string.n_links_will_be_imported_createArgs(
|
||||||
|
count = linkCount.toString()
|
||||||
|
)
|
||||||
|
),
|
||||||
fontSize = myTextSizes.base,
|
fontSize = myTextSizes.base,
|
||||||
color = myColors.success,
|
color = myColors.success,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
linkCount == 0 -> {
|
linkCount == 0 -> {
|
||||||
Text("Nothing will be imported")
|
Text(myStringResource(Res.string.nothing_will_be_imported))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -644,7 +670,7 @@ fun CategoryOption(
|
|||||||
ShowOptionsInDropDown(
|
ShowOptionsInDropDown(
|
||||||
MenuItem.SubMenu(
|
MenuItem.SubMenu(
|
||||||
icon = categoryOptionMenuState.categoryItem?.rememberIconPainter(),
|
icon = categoryOptionMenuState.categoryItem?.rememberIconPainter(),
|
||||||
title = categoryOptionMenuState.categoryItem?.name.orEmpty(),
|
title = categoryOptionMenuState.categoryItem?.name.orEmpty().asStringSource(),
|
||||||
categoryOptionMenuState.menu,
|
categoryOptionMenuState.menu,
|
||||||
),
|
),
|
||||||
onDismiss
|
onDismiss
|
||||||
@ -728,20 +754,22 @@ fun HomeSearch(
|
|||||||
val searchBoxInteractionSource = remember { MutableInteractionSource() }
|
val searchBoxInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
val isFocused by searchBoxInteractionSource.collectIsFocusedAsState()
|
val isFocused by searchBoxInteractionSource.collectIsFocusedAsState()
|
||||||
SearchBox(
|
WithLanguageDirection {
|
||||||
text = component.filterState.textToSearch,
|
SearchBox(
|
||||||
onTextChange = {
|
text = component.filterState.textToSearch,
|
||||||
component.filterState.textToSearch = it
|
onTextChange = {
|
||||||
},
|
component.filterState.textToSearch = it
|
||||||
textPadding = textPadding,
|
},
|
||||||
interactionSource = searchBoxInteractionSource,
|
textPadding = textPadding,
|
||||||
modifier = modifier
|
interactionSource = searchBoxInteractionSource,
|
||||||
.width(
|
modifier = modifier
|
||||||
animateDpAsState(
|
.width(
|
||||||
if (isFocused) 220.dp else 180.dp
|
animateDpAsState(
|
||||||
).value
|
if (isFocused) 220.dp else 180.dp
|
||||||
)
|
).value
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,7 +11,11 @@ import com.abdownloadmanager.desktop.actions.handle
|
|||||||
import com.abdownloadmanager.desktop.ui.customwindow.CustomWindow
|
import com.abdownloadmanager.desktop.ui.customwindow.CustomWindow
|
||||||
import com.abdownloadmanager.desktop.ui.customwindow.rememberWindowController
|
import com.abdownloadmanager.desktop.ui.customwindow.rememberWindowController
|
||||||
import com.abdownloadmanager.desktop.ui.icon.MyIcons
|
import com.abdownloadmanager.desktop.ui.icon.MyIcons
|
||||||
|
import com.abdownloadmanager.desktop.utils.AppInfo
|
||||||
import com.abdownloadmanager.desktop.utils.mvi.HandleEffects
|
import com.abdownloadmanager.desktop.utils.mvi.HandleEffects
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -25,10 +29,9 @@ fun HomeWindow(
|
|||||||
position = WindowPosition.Aligned(Alignment.Center)
|
position = WindowPosition.Aligned(Alignment.Center)
|
||||||
)
|
)
|
||||||
val onCloseRequest = onCLoseRequest
|
val onCloseRequest = onCLoseRequest
|
||||||
val windowTitle = "AB Download Manager"
|
|
||||||
val windowIcon = MyIcons.appIcon
|
val windowIcon = MyIcons.appIcon
|
||||||
val windowController = rememberWindowController(
|
val windowController = rememberWindowController(
|
||||||
windowTitle,
|
AppInfo.name,
|
||||||
windowIcon.rememberPainter(),
|
windowIcon.rememberPainter(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -25,13 +25,18 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.input.key.*
|
import androidx.compose.ui.input.key.*
|
||||||
import androidx.compose.ui.input.pointer.*
|
import androidx.compose.ui.input.pointer.*
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
import com.abdownloadmanager.utils.FileIconProvider
|
import com.abdownloadmanager.utils.FileIconProvider
|
||||||
import com.abdownloadmanager.utils.category.CategoryManager
|
import com.abdownloadmanager.utils.category.CategoryManager
|
||||||
import com.abdownloadmanager.utils.category.rememberCategoryOf
|
import com.abdownloadmanager.utils.category.rememberCategoryOf
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
import ir.amirab.downloader.monitor.IDownloadItemState
|
import ir.amirab.downloader.monitor.IDownloadItemState
|
||||||
import ir.amirab.downloader.monitor.remainingOrNull
|
import ir.amirab.downloader.monitor.remainingOrNull
|
||||||
import ir.amirab.downloader.monitor.speedOrNull
|
import ir.amirab.downloader.monitor.speedOrNull
|
||||||
import ir.amirab.downloader.monitor.statusOrFinished
|
import ir.amirab.downloader.monitor.statusOrFinished
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
|
||||||
@ -143,7 +148,7 @@ fun DownloadList(
|
|||||||
),
|
),
|
||||||
drawOnEmpty = {
|
drawOnEmpty = {
|
||||||
WithContentAlpha(0.75f) {
|
WithContentAlpha(0.75f) {
|
||||||
Text("List is empty.", Modifier.align(Alignment.Center))
|
Text(myStringResource(Res.string.list_is_empty), Modifier.align(Alignment.Center))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
wrapHeader = {
|
wrapHeader = {
|
||||||
@ -288,7 +293,8 @@ fun DownloadList(
|
|||||||
sealed interface DownloadListCells : TableCell<IDownloadItemState> {
|
sealed interface DownloadListCells : TableCell<IDownloadItemState> {
|
||||||
data object Check : DownloadListCells,
|
data object Check : DownloadListCells,
|
||||||
CustomCellRenderer {
|
CustomCellRenderer {
|
||||||
override val name: String = "#"
|
override val id: String = "#"
|
||||||
|
override val name: StringSource = "#".asStringSource()
|
||||||
override val size: CellSize = CellSize.Fixed(26.dp)
|
override val size: CellSize = CellSize.Fixed(26.dp)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -308,7 +314,8 @@ sealed interface DownloadListCells : TableCell<IDownloadItemState> {
|
|||||||
SortableCell<IDownloadItemState> {
|
SortableCell<IDownloadItemState> {
|
||||||
override fun sortBy(item: IDownloadItemState): Comparable<*> = item.name
|
override fun sortBy(item: IDownloadItemState): Comparable<*> = item.name
|
||||||
|
|
||||||
override val name: String = "Name"
|
override val id: String = "Name"
|
||||||
|
override val name: StringSource = Res.string.name.asStringSource()
|
||||||
override val size: CellSize = CellSize.Resizeable(50.dp..1000.dp, 200.dp)
|
override val size: CellSize = CellSize.Resizeable(50.dp..1000.dp, 200.dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,7 +323,8 @@ sealed interface DownloadListCells : TableCell<IDownloadItemState> {
|
|||||||
SortableCell<IDownloadItemState> {
|
SortableCell<IDownloadItemState> {
|
||||||
override fun sortBy(item: IDownloadItemState): Comparable<*> = item.statusOrFinished().order
|
override fun sortBy(item: IDownloadItemState): Comparable<*> = item.statusOrFinished().order
|
||||||
|
|
||||||
override val name: String = "Status"
|
override val id: String = "Status"
|
||||||
|
override val name: StringSource = Res.string.status.asStringSource()
|
||||||
override val size: CellSize = CellSize.Resizeable(100.dp..140.dp, 120.dp)
|
override val size: CellSize = CellSize.Resizeable(100.dp..140.dp, 120.dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,7 +332,8 @@ sealed interface DownloadListCells : TableCell<IDownloadItemState> {
|
|||||||
SortableCell<IDownloadItemState> {
|
SortableCell<IDownloadItemState> {
|
||||||
override fun sortBy(item: IDownloadItemState): Comparable<*> = item.contentLength
|
override fun sortBy(item: IDownloadItemState): Comparable<*> = item.contentLength
|
||||||
|
|
||||||
override val name: String = "Size"
|
override val id: String = "Size"
|
||||||
|
override val name: StringSource = Res.string.size.asStringSource()
|
||||||
override val size: CellSize = CellSize.Resizeable(70.dp..110.dp, 70.dp)
|
override val size: CellSize = CellSize.Resizeable(70.dp..110.dp, 70.dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -332,7 +341,8 @@ sealed interface DownloadListCells : TableCell<IDownloadItemState> {
|
|||||||
SortableCell<IDownloadItemState> {
|
SortableCell<IDownloadItemState> {
|
||||||
override fun sortBy(item: IDownloadItemState): Comparable<*> = item.speedOrNull() ?: 0L
|
override fun sortBy(item: IDownloadItemState): Comparable<*> = item.speedOrNull() ?: 0L
|
||||||
|
|
||||||
override val name: String = "Speed"
|
override val id: String = "Speed"
|
||||||
|
override val name: StringSource = Res.string.speed.asStringSource()
|
||||||
override val size: CellSize = CellSize.Resizeable(70.dp..110.dp, 80.dp)
|
override val size: CellSize = CellSize.Resizeable(70.dp..110.dp, 80.dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,7 +350,8 @@ sealed interface DownloadListCells : TableCell<IDownloadItemState> {
|
|||||||
SortableCell<IDownloadItemState> {
|
SortableCell<IDownloadItemState> {
|
||||||
override fun sortBy(item: IDownloadItemState): Comparable<*> = item.remainingOrNull() ?: Long.MAX_VALUE
|
override fun sortBy(item: IDownloadItemState): Comparable<*> = item.remainingOrNull() ?: Long.MAX_VALUE
|
||||||
|
|
||||||
override val name: String = "Time Left"
|
override val id: String = "Time Left"
|
||||||
|
override val name: StringSource = Res.string.time_left.asStringSource()
|
||||||
override val size: CellSize = CellSize.Resizeable(70.dp..150.dp, 100.dp)
|
override val size: CellSize = CellSize.Resizeable(70.dp..150.dp, 100.dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,7 +359,8 @@ sealed interface DownloadListCells : TableCell<IDownloadItemState> {
|
|||||||
SortableCell<IDownloadItemState> {
|
SortableCell<IDownloadItemState> {
|
||||||
override fun sortBy(item: IDownloadItemState): Comparable<*> = item.dateAdded
|
override fun sortBy(item: IDownloadItemState): Comparable<*> = item.dateAdded
|
||||||
|
|
||||||
override val name: String = "Date Added"
|
override val id: String = "Date Added"
|
||||||
|
override val name: StringSource = Res.string.date_added.asStringSource()
|
||||||
override val size: CellSize = CellSize.Resizeable(90.dp..150.dp, 100.dp)
|
override val size: CellSize = CellSize.Resizeable(90.dp..150.dp, 100.dp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,9 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.input.pointer.PointerIcon
|
import androidx.compose.ui.input.pointer.PointerIcon
|
||||||
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchBox(
|
fun SearchBox(
|
||||||
@ -27,7 +30,7 @@ fun SearchBox(
|
|||||||
onTextChange: (String) -> Unit,
|
onTextChange: (String) -> Unit,
|
||||||
textPadding: PaddingValues = PaddingValues(horizontal = 8.dp),
|
textPadding: PaddingValues = PaddingValues(horizontal = 8.dp),
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
placeholder: String = "Search in the List",
|
placeholder: String = myStringResource(Res.string.search_in_the_list),
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
) {
|
) {
|
||||||
val shape = RoundedCornerShape(12.dp)
|
val shape = RoundedCornerShape(12.dp)
|
||||||
@ -44,7 +47,8 @@ fun SearchBox(
|
|||||||
animateFloatAsState(if (text.isBlank()) 0.9f else 1f).value
|
animateFloatAsState(if (text.isBlank()) 0.9f else 1f).value
|
||||||
) {
|
) {
|
||||||
MyIcon(
|
MyIcon(
|
||||||
MyIcons.search, "Search",
|
MyIcons.search,
|
||||||
|
myStringResource(Res.string.search),
|
||||||
Modifier
|
Modifier
|
||||||
.padding(start = 8.dp)
|
.padding(start = 8.dp)
|
||||||
.size(16.dp)
|
.size(16.dp)
|
||||||
@ -55,7 +59,7 @@ fun SearchBox(
|
|||||||
AnimatedContent(text.isNotBlank()) {
|
AnimatedContent(text.isNotBlank()) {
|
||||||
MyIcon(
|
MyIcon(
|
||||||
MyIcons.clear,
|
MyIcons.clear,
|
||||||
"Clear",
|
myStringResource(Res.string.clear),
|
||||||
Modifier
|
Modifier
|
||||||
.padding(end = 8.dp)
|
.padding(end = 8.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
|
@ -19,13 +19,17 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
import com.abdownloadmanager.utils.FileIconProvider
|
import com.abdownloadmanager.utils.FileIconProvider
|
||||||
import com.abdownloadmanager.utils.category.Category
|
import com.abdownloadmanager.utils.category.Category
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
import ir.amirab.downloader.downloaditem.DownloadJobStatus
|
import ir.amirab.downloader.downloaditem.DownloadJobStatus
|
||||||
import ir.amirab.downloader.monitor.CompletedDownloadItemState
|
import ir.amirab.downloader.monitor.CompletedDownloadItemState
|
||||||
import ir.amirab.downloader.monitor.IDownloadItemState
|
import ir.amirab.downloader.monitor.IDownloadItemState
|
||||||
import ir.amirab.downloader.monitor.ProcessingDownloadItemState
|
import ir.amirab.downloader.monitor.ProcessingDownloadItemState
|
||||||
import ir.amirab.downloader.utils.ExceptionUtils
|
import ir.amirab.downloader.utils.ExceptionUtils
|
||||||
|
import ir.amirab.util.compose.resources.MyStringResource
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.datetime.*
|
import kotlinx.datetime.*
|
||||||
@ -115,7 +119,7 @@ fun NameCell(
|
|||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
category?.name ?: "General", maxLines = 1, fontSize = myTextSizes.xs,
|
category?.name ?: myStringResource(Res.string.general), maxLines = 1, fontSize = myTextSizes.xs,
|
||||||
color = LocalContentColor.current / 50
|
color = LocalContentColor.current / 50
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -186,7 +190,7 @@ fun SizeCell(
|
|||||||
) {
|
) {
|
||||||
item.contentLength.let {
|
item.contentLength.let {
|
||||||
Text(
|
Text(
|
||||||
convertSizeToHumanReadable(it),
|
convertSizeToHumanReadable(it).rememberString(),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
fontSize = myTextSizes.base,
|
fontSize = myTextSizes.base,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
@ -247,17 +251,69 @@ fun StatusCell(
|
|||||||
|
|
||||||
DownloadJobStatus.Finished,
|
DownloadJobStatus.Finished,
|
||||||
DownloadJobStatus.Resuming,
|
DownloadJobStatus.Resuming,
|
||||||
-> SimpleStatus(itemState.status.toString())
|
-> SimpleStatus(myStringResource(itemState.status.toStringResource()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is CompletedDownloadItemState -> {
|
is CompletedDownloadItemState -> {
|
||||||
SimpleStatus("Finished")
|
SimpleStatus(myStringResource(Res.string.finished))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DownloadJobStatus.toStringResource(): MyStringResource {
|
||||||
|
return when (this) {
|
||||||
|
is DownloadJobStatus.Canceled -> {
|
||||||
|
Res.string.canceled
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadJobStatus.Downloading -> {
|
||||||
|
Res.string.downloading
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadJobStatus.Finished -> {
|
||||||
|
Res.string.finished
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadJobStatus.IDLE -> {
|
||||||
|
Res.string.idle
|
||||||
|
}
|
||||||
|
|
||||||
|
is DownloadJobStatus.PreparingFile -> {
|
||||||
|
Res.string.preparing_file
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadJobStatus.Resuming -> {
|
||||||
|
Res.string.resuming
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DownloadProgressStatus.toStringResource(): MyStringResource {
|
||||||
|
return when (this) {
|
||||||
|
DownloadProgressStatus.Added -> {
|
||||||
|
Res.string.added
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadProgressStatus.Error -> {
|
||||||
|
Res.string.error
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadProgressStatus.Paused -> {
|
||||||
|
Res.string.paused
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadProgressStatus.CreatingFile -> {
|
||||||
|
Res.string.creating_file
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadProgressStatus.Downloading -> {
|
||||||
|
Res.string.downloading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SimpleStatus(string: String) {
|
private fun SimpleStatus(string: String) {
|
||||||
@ -285,11 +341,12 @@ private fun ProgressAndPercent(
|
|||||||
DownloadProgressStatus.CreatingFile -> myColors.infoGradient
|
DownloadProgressStatus.CreatingFile -> myColors.infoGradient
|
||||||
DownloadProgressStatus.Downloading -> myColors.primaryGradient
|
DownloadProgressStatus.Downloading -> myColors.primaryGradient
|
||||||
}
|
}
|
||||||
|
val statusString = myStringResource(status.toStringResource())
|
||||||
Column {
|
Column {
|
||||||
val statusText = if (gotAnyProgress) {
|
val statusText = if (gotAnyProgress) {
|
||||||
"${percent ?: "."}% $status"
|
"${percent ?: "."}% $statusString"
|
||||||
} else {
|
} else {
|
||||||
"$status"
|
statusString
|
||||||
}
|
}
|
||||||
SimpleStatus(statusText)
|
SimpleStatus(statusText)
|
||||||
if (status != DownloadProgressStatus.Added) {
|
if (status != DownloadProgressStatus.Added) {
|
||||||
|
@ -17,7 +17,6 @@ import androidx.compose.foundation.shape.CircleShape
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import com.abdownloadmanager.desktop.ui.widget.Text
|
import com.abdownloadmanager.desktop.ui.widget.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@ -28,14 +27,18 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.abdownloadmanager.desktop.ui.theme.myColors
|
import com.abdownloadmanager.desktop.ui.theme.myColors
|
||||||
import com.abdownloadmanager.desktop.utils.div
|
import com.abdownloadmanager.desktop.utils.div
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
import com.abdownloadmanager.utils.category.Category
|
import com.abdownloadmanager.utils.category.Category
|
||||||
import com.abdownloadmanager.utils.category.rememberIconPainter
|
import com.abdownloadmanager.utils.category.rememberIconPainter
|
||||||
import ir.amirab.downloader.downloaditem.DownloadStatus
|
import ir.amirab.downloader.downloaditem.DownloadStatus
|
||||||
import ir.amirab.downloader.monitor.IDownloadItemState
|
import ir.amirab.downloader.monitor.IDownloadItemState
|
||||||
import ir.amirab.downloader.monitor.statusOrFinished
|
import ir.amirab.downloader.monitor.statusOrFinished
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
|
|
||||||
class DownloadStatusCategoryFilterByList(
|
class DownloadStatusCategoryFilterByList(
|
||||||
name: String,
|
name: StringSource,
|
||||||
icon: IconSource,
|
icon: IconSource,
|
||||||
val acceptedStatus: List<DownloadStatus>,
|
val acceptedStatus: List<DownloadStatus>,
|
||||||
) : DownloadStatusCategoryFilter(name, icon) {
|
) : DownloadStatusCategoryFilter(name, icon) {
|
||||||
@ -47,7 +50,7 @@ class DownloadStatusCategoryFilterByList(
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract class DownloadStatusCategoryFilter(
|
abstract class DownloadStatusCategoryFilter(
|
||||||
val name: String,
|
val name: StringSource,
|
||||||
val icon: IconSource,
|
val icon: IconSource,
|
||||||
) {
|
) {
|
||||||
abstract fun accept(iDownloadStatus: IDownloadItemState): Boolean
|
abstract fun accept(iDownloadStatus: IDownloadItemState): Boolean
|
||||||
@ -58,18 +61,18 @@ object DefinedStatusCategories {
|
|||||||
|
|
||||||
|
|
||||||
val All = object : DownloadStatusCategoryFilter(
|
val All = object : DownloadStatusCategoryFilter(
|
||||||
"All",
|
Res.string.all.asStringSource(),
|
||||||
MyIcons.folder,
|
MyIcons.folder,
|
||||||
) {
|
) {
|
||||||
override fun accept(iDownloadStatus: IDownloadItemState): Boolean = true
|
override fun accept(iDownloadStatus: IDownloadItemState): Boolean = true
|
||||||
}
|
}
|
||||||
val Finished = DownloadStatusCategoryFilterByList(
|
val Finished = DownloadStatusCategoryFilterByList(
|
||||||
"Finished",
|
Res.string.finished.asStringSource(),
|
||||||
MyIcons.folder,
|
MyIcons.folder,
|
||||||
listOf(DownloadStatus.Completed)
|
listOf(DownloadStatus.Completed)
|
||||||
)
|
)
|
||||||
val Unfinished = DownloadStatusCategoryFilterByList(
|
val Unfinished = DownloadStatusCategoryFilterByList(
|
||||||
"Unfinished",
|
Res.string.Unfinished.asStringSource(),
|
||||||
MyIcons.folder,
|
MyIcons.folder,
|
||||||
listOf(
|
listOf(
|
||||||
DownloadStatus.Error,
|
DownloadStatus.Error,
|
||||||
@ -203,7 +206,7 @@ fun StatusFilterItem(
|
|||||||
)
|
)
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
statusFilter.name,
|
statusFilter.name.rememberString(),
|
||||||
Modifier.weight(1f),
|
Modifier.weight(1f),
|
||||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
||||||
fontSize = myTextSizes.lg,
|
fontSize = myTextSizes.lg,
|
||||||
|
@ -9,13 +9,16 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NewQueue(
|
fun NewQueue(
|
||||||
onQueueCreate: (String) -> Unit,
|
onQueueCreate: (String) -> Unit,
|
||||||
onCloseRequest: () -> Unit,
|
onCloseRequest: () -> Unit,
|
||||||
) {
|
) {
|
||||||
WindowTitle("New Queue")
|
WindowTitle(myStringResource(Res.string.add_new_queue))
|
||||||
var name by remember {
|
var name by remember {
|
||||||
mutableStateOf("")
|
mutableStateOf("")
|
||||||
}
|
}
|
||||||
@ -34,7 +37,7 @@ fun NewQueue(
|
|||||||
.focusRequester(focusRequester)
|
.focusRequester(focusRequester)
|
||||||
.padding(horizontal = 8.dp)
|
.padding(horizontal = 8.dp)
|
||||||
.widthIn(max = 400.dp),
|
.widthIn(max = 400.dp),
|
||||||
placeholder = "Queue name...",
|
placeholder = myStringResource(Res.string.queue_name),
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
@ -46,14 +49,14 @@ fun NewQueue(
|
|||||||
horizontalArrangement = Arrangement.End,
|
horizontalArrangement = Arrangement.End,
|
||||||
) {
|
) {
|
||||||
ActionButton(
|
ActionButton(
|
||||||
text = "Create",
|
text = myStringResource(Res.string.add),
|
||||||
onClick = {
|
onClick = {
|
||||||
onQueueCreate(name)
|
onQueueCreate(name)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(4.dp))
|
||||||
ActionButton(
|
ActionButton(
|
||||||
text = "Cancel",
|
text = myStringResource(Res.string.cancel),
|
||||||
onClick = {
|
onClick = {
|
||||||
onCloseRequest()
|
onCloseRequest()
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,15 @@ import ir.amirab.util.flow.createMutableStateFlowFromStateFlow
|
|||||||
import ir.amirab.util.flow.mapStateFlow
|
import ir.amirab.util.flow.mapStateFlow
|
||||||
import com.abdownloadmanager.desktop.utils.newScopeBasedOn
|
import com.abdownloadmanager.desktop.utils.newScopeBasedOn
|
||||||
import androidx.compose.runtime.toMutableStateList
|
import androidx.compose.runtime.toMutableStateList
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
import com.arkivanov.decompose.ComponentContext
|
import com.arkivanov.decompose.ComponentContext
|
||||||
import ir.amirab.downloader.monitor.IDownloadItemState
|
import ir.amirab.downloader.monitor.IDownloadItemState
|
||||||
import ir.amirab.downloader.monitor.IDownloadMonitor
|
import ir.amirab.downloader.monitor.IDownloadMonitor
|
||||||
import ir.amirab.downloader.queue.DownloadQueue
|
import ir.amirab.downloader.queue.DownloadQueue
|
||||||
import ir.amirab.downloader.queue.QueueManager
|
import ir.amirab.downloader.queue.QueueManager
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
|
import ir.amirab.util.compose.asStringSourceWithARgs
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
@ -69,7 +73,7 @@ class QueueInfoComponent(
|
|||||||
|
|
||||||
|
|
||||||
val configurations: List<ConfigurableGroup> =
|
val configurations: List<ConfigurableGroup> =
|
||||||
createConfigurableList(downloadQueue, scope)
|
createConfigurableList(downloadQueue, scope)
|
||||||
|
|
||||||
|
|
||||||
private fun createConfigurableList(
|
private fun createConfigurableList(
|
||||||
@ -84,11 +88,11 @@ class QueueInfoComponent(
|
|||||||
}
|
}
|
||||||
return listOf(
|
return listOf(
|
||||||
ConfigurableGroup(
|
ConfigurableGroup(
|
||||||
groupTitle = MutableStateFlow("General"),
|
groupTitle = MutableStateFlow(Res.string.general.asStringSource()),
|
||||||
nestedConfigurable = listOf(
|
nestedConfigurable = listOf(
|
||||||
StringConfigurable(
|
StringConfigurable(
|
||||||
"Name",
|
Res.string.name.asStringSource(),
|
||||||
"Specify A name for this queue",
|
Res.string.queue_name_help.asStringSource(),
|
||||||
backedBy = createMutableStateFlowFromStateFlow(
|
backedBy = createMutableStateFlowFromStateFlow(
|
||||||
scope = scope,
|
scope = scope,
|
||||||
flow = downloadQueue.queueModel.mapStateFlow() {
|
flow = downloadQueue.queueModel.mapStateFlow() {
|
||||||
@ -101,11 +105,18 @@ class QueueInfoComponent(
|
|||||||
validate = {
|
validate = {
|
||||||
it.length in 1..32
|
it.length in 1..32
|
||||||
},
|
},
|
||||||
describe = { "Queue name is $it" },
|
describe = {
|
||||||
|
Res.string.queue_name_describe
|
||||||
|
.asStringSourceWithARgs(
|
||||||
|
Res.string.queue_name_describe_createArgs(
|
||||||
|
value = it
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
),
|
),
|
||||||
IntConfigurable(
|
IntConfigurable(
|
||||||
"Max Concurrent",
|
Res.string.queue_max_concurrent_download.asStringSource(),
|
||||||
"Max download for this queue",
|
Res.string.queue_max_concurrent_download_description.asStringSource(),
|
||||||
backedBy = createMutableStateFlowFromStateFlow(
|
backedBy = createMutableStateFlowFromStateFlow(
|
||||||
scope = scope,
|
scope = scope,
|
||||||
flow = downloadQueue.queueModel.mapStateFlow() {
|
flow = downloadQueue.queueModel.mapStateFlow() {
|
||||||
@ -115,13 +126,13 @@ class QueueInfoComponent(
|
|||||||
downloadQueue.setMaxConcurrent(newValue)
|
downloadQueue.setMaxConcurrent(newValue)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
describe = { "${it}" },
|
describe = { "$it".asStringSource() },
|
||||||
range = 1..32,
|
range = 1..32,
|
||||||
renderMode = IntConfigurable.RenderMode.TextField,
|
renderMode = IntConfigurable.RenderMode.TextField,
|
||||||
),
|
),
|
||||||
BooleanConfigurable(
|
BooleanConfigurable(
|
||||||
"Automatic stop",
|
Res.string.queue_automatic_stop.asStringSource(),
|
||||||
"Automatic stop queue when there is no item in it",
|
Res.string.queue_automatic_stop_description.asStringSource(),
|
||||||
backedBy = createMutableStateFlowFromStateFlow(
|
backedBy = createMutableStateFlowFromStateFlow(
|
||||||
scope = scope,
|
scope = scope,
|
||||||
flow = downloadQueue.queueModel.mapStateFlow() {
|
flow = downloadQueue.queueModel.mapStateFlow() {
|
||||||
@ -132,19 +143,19 @@ class QueueInfoComponent(
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
describe = {
|
describe = {
|
||||||
if (it) "Enabled"
|
if (it) Res.string.enabled.asStringSource()
|
||||||
else "Disabled"
|
else Res.string.disabled.asStringSource()
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ConfigurableGroup(
|
ConfigurableGroup(
|
||||||
groupTitle = MutableStateFlow("Scheduler"),
|
groupTitle = MutableStateFlow(Res.string.queue_scheduler.asStringSource()),
|
||||||
nestedVisible = enabledSchedulerFlow,
|
nestedVisible = enabledSchedulerFlow,
|
||||||
mainConfigurable = BooleanConfigurable(
|
mainConfigurable = BooleanConfigurable(
|
||||||
"Enable Scheduler",
|
Res.string.queue_enable_scheduler.asStringSource(),
|
||||||
description = "",
|
description = "".asStringSource(),
|
||||||
describe = { "" },
|
describe = { "".asStringSource() },
|
||||||
backedBy = createMutableStateFlowFromStateFlow(
|
backedBy = createMutableStateFlowFromStateFlow(
|
||||||
flow = enabledSchedulerFlow,
|
flow = enabledSchedulerFlow,
|
||||||
scope = scope,
|
scope = scope,
|
||||||
@ -157,8 +168,8 @@ class QueueInfoComponent(
|
|||||||
),
|
),
|
||||||
nestedConfigurable = listOf(
|
nestedConfigurable = listOf(
|
||||||
DayOfWeekConfigurable(
|
DayOfWeekConfigurable(
|
||||||
"Active days",
|
Res.string.queue_active_days.asStringSource(),
|
||||||
"which days schedulers function ?",
|
Res.string.queue_active_days_description.asStringSource(),
|
||||||
backedBy = createMutableStateFlowFromStateFlow(
|
backedBy = createMutableStateFlowFromStateFlow(
|
||||||
scope = scope,
|
scope = scope,
|
||||||
flow = downloadQueue.queueModel.mapStateFlow() {
|
flow = downloadQueue.queueModel.mapStateFlow() {
|
||||||
@ -173,11 +184,11 @@ class QueueInfoComponent(
|
|||||||
validate = {
|
validate = {
|
||||||
it.isNotEmpty()
|
it.isNotEmpty()
|
||||||
},
|
},
|
||||||
describe = { "" },
|
describe = { "".asStringSource() },
|
||||||
),
|
),
|
||||||
TimeConfigurable(
|
TimeConfigurable(
|
||||||
"Auto Start download",
|
Res.string.queue_scheduler_auto_start_time.asStringSource(),
|
||||||
"",
|
"".asStringSource(),
|
||||||
backedBy = createMutableStateFlowFromStateFlow(
|
backedBy = createMutableStateFlowFromStateFlow(
|
||||||
scope = scope,
|
scope = scope,
|
||||||
flow = downloadQueue.queueModel.mapStateFlow() {
|
flow = downloadQueue.queueModel.mapStateFlow() {
|
||||||
@ -189,12 +200,12 @@ class QueueInfoComponent(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
describe = { "" },
|
describe = { "".asStringSource() },
|
||||||
),
|
),
|
||||||
BooleanConfigurable(
|
BooleanConfigurable(
|
||||||
"Enable Auto Stop",
|
Res.string.queue_scheduler_enable_auto_stop_time.asStringSource(),
|
||||||
description = "",
|
description = "".asStringSource(),
|
||||||
describe = { "" },
|
describe = { "".asStringSource() },
|
||||||
backedBy = createMutableStateFlowFromStateFlow(
|
backedBy = createMutableStateFlowFromStateFlow(
|
||||||
scope = scope,
|
scope = scope,
|
||||||
flow = enabledEndTimeFlow,
|
flow = enabledEndTimeFlow,
|
||||||
@ -207,8 +218,8 @@ class QueueInfoComponent(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
TimeConfigurable(
|
TimeConfigurable(
|
||||||
"Auto Stop download",
|
Res.string.queue_scheduler_auto_stop_time.asStringSource(),
|
||||||
"",
|
"".asStringSource(),
|
||||||
backedBy = createMutableStateFlowFromStateFlow(
|
backedBy = createMutableStateFlowFromStateFlow(
|
||||||
scope = scope,
|
scope = scope,
|
||||||
flow = downloadQueue.queueModel.mapStateFlow() {
|
flow = downloadQueue.queueModel.mapStateFlow() {
|
||||||
@ -220,7 +231,7 @@ class QueueInfoComponent(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
describe = { "" },
|
describe = { "".asStringSource() },
|
||||||
visible = enabledEndTimeFlow,
|
visible = enabledEndTimeFlow,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -35,10 +35,15 @@ import androidx.compose.ui.platform.LocalWindowInfo
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
import ir.amirab.downloader.downloaditem.DownloadJobStatus
|
import ir.amirab.downloader.downloaditem.DownloadJobStatus
|
||||||
import ir.amirab.downloader.monitor.IDownloadItemState
|
import ir.amirab.downloader.monitor.IDownloadItemState
|
||||||
import ir.amirab.downloader.monitor.statusOrFinished
|
import ir.amirab.downloader.monitor.statusOrFinished
|
||||||
import ir.amirab.downloader.queue.DownloadQueue
|
import ir.amirab.downloader.queue.DownloadQueue
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.burnoutcrew.reorderable.*
|
import org.burnoutcrew.reorderable.*
|
||||||
|
|
||||||
@ -47,7 +52,7 @@ import org.burnoutcrew.reorderable.*
|
|||||||
fun QueuePage(component: QueuesComponent) {
|
fun QueuePage(component: QueuesComponent) {
|
||||||
val queues = component.queuesState
|
val queues = component.queuesState
|
||||||
val activeItem: DownloadQueue = component.selectedItem
|
val activeItem: DownloadQueue = component.selectedItem
|
||||||
WindowTitle("Queues")
|
WindowTitle(myStringResource(Res.string.queues))
|
||||||
val borderShape = RoundedCornerShape(6.dp)
|
val borderShape = RoundedCornerShape(6.dp)
|
||||||
val borderColor = myColors.onBackground / 5
|
val borderColor = myColors.onBackground / 5
|
||||||
Column {
|
Column {
|
||||||
@ -100,11 +105,13 @@ private fun Actions(
|
|||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(4.dp))
|
||||||
}
|
}
|
||||||
ActionButton(
|
ActionButton(
|
||||||
text = if (isActive) {
|
text = myStringResource(
|
||||||
"Stop Queue"
|
if (isActive) {
|
||||||
} else {
|
Res.string.stop_queue
|
||||||
"Start Queue"
|
} else {
|
||||||
},
|
Res.string.start_queue
|
||||||
|
}
|
||||||
|
),
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@ -118,7 +125,7 @@ private fun Actions(
|
|||||||
)
|
)
|
||||||
space()
|
space()
|
||||||
ActionButton(
|
ActionButton(
|
||||||
text = "Close",
|
text = myStringResource(Res.string.close),
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
onClick = {
|
onClick = {
|
||||||
component.close()
|
component.close()
|
||||||
@ -127,9 +134,9 @@ private fun Actions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class QueueInfoPages(val title: String, val icon: IconSource) {
|
enum class QueueInfoPages(val title: StringSource, val icon: IconSource) {
|
||||||
Config("Config", MyIcons.settings),
|
Config(Res.string.config.asStringSource(), MyIcons.settings),
|
||||||
Items("Items", MyIcons.queue),
|
Items(Res.string.items.asStringSource(), MyIcons.queue),
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -232,7 +239,7 @@ fun RenderQueueItems(
|
|||||||
val space = 4.dp
|
val space = 4.dp
|
||||||
IconActionButton(
|
IconActionButton(
|
||||||
icon = MyIcons.remove,
|
icon = MyIcons.remove,
|
||||||
contentDescription = "remove",
|
contentDescription = myStringResource(Res.string.remove),
|
||||||
onClick = {
|
onClick = {
|
||||||
component.deleteItems()
|
component.deleteItems()
|
||||||
},
|
},
|
||||||
@ -241,7 +248,7 @@ fun RenderQueueItems(
|
|||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
IconActionButton(
|
IconActionButton(
|
||||||
icon = MyIcons.down,
|
icon = MyIcons.down,
|
||||||
contentDescription = "Move down",
|
contentDescription = myStringResource(Res.string.move_down),
|
||||||
onClick = {
|
onClick = {
|
||||||
component.moveDownItems()
|
component.moveDownItems()
|
||||||
},
|
},
|
||||||
@ -250,7 +257,7 @@ fun RenderQueueItems(
|
|||||||
Spacer(Modifier.width(space))
|
Spacer(Modifier.width(space))
|
||||||
IconActionButton(
|
IconActionButton(
|
||||||
icon = MyIcons.up,
|
icon = MyIcons.up,
|
||||||
contentDescription = "Move up",
|
contentDescription = myStringResource(Res.string.move_up),
|
||||||
onClick = {
|
onClick = {
|
||||||
component.moveUpItems()
|
component.moveUpItems()
|
||||||
},
|
},
|
||||||
@ -413,7 +420,7 @@ private fun QueueListSection(
|
|||||||
) {
|
) {
|
||||||
IconActionButton(
|
IconActionButton(
|
||||||
icon = MyIcons.add,
|
icon = MyIcons.add,
|
||||||
contentDescription = "Add Queue",
|
contentDescription = myStringResource(Res.string.add_new_queue),
|
||||||
onClick = {
|
onClick = {
|
||||||
component.addQueue()
|
component.addQueue()
|
||||||
}
|
}
|
||||||
@ -421,7 +428,7 @@ private fun QueueListSection(
|
|||||||
spacer()
|
spacer()
|
||||||
IconActionButton(
|
IconActionButton(
|
||||||
icon = MyIcons.remove,
|
icon = MyIcons.remove,
|
||||||
contentDescription = "Delete Queue",
|
contentDescription = myStringResource(Res.string.remove_queue),
|
||||||
enabled = component.canDeleteThisQueue(selectedItem),
|
enabled = component.canDeleteThisQueue(selectedItem),
|
||||||
onClick = {
|
onClick = {
|
||||||
component.requestDeleteQueue(selectedItem)
|
component.requestDeleteQueue(selectedItem)
|
||||||
|
@ -11,25 +11,32 @@ import com.abdownloadmanager.desktop.utils.convertSpeedToHumanReadable
|
|||||||
import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects
|
import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects
|
||||||
import com.abdownloadmanager.desktop.utils.mvi.supportEffects
|
import com.abdownloadmanager.desktop.utils.mvi.supportEffects
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
import com.abdownloadmanager.utils.proxy.ProxyManager
|
import com.abdownloadmanager.utils.proxy.ProxyManager
|
||||||
import com.abdownloadmanager.utils.proxy.ProxyMode
|
import com.abdownloadmanager.utils.proxy.ProxyMode
|
||||||
import com.arkivanov.decompose.ComponentContext
|
import com.arkivanov.decompose.ComponentContext
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
|
import ir.amirab.util.compose.asStringSourceWithARgs
|
||||||
|
import ir.amirab.util.compose.localizationmanager.LanguageInfo
|
||||||
|
import ir.amirab.util.compose.localizationmanager.LanguageManager
|
||||||
import ir.amirab.util.osfileutil.FileUtils
|
import ir.amirab.util.osfileutil.FileUtils
|
||||||
import ir.amirab.util.flow.createMutableStateFlowFromStateFlow
|
import ir.amirab.util.flow.createMutableStateFlowFromStateFlow
|
||||||
|
import ir.amirab.util.flow.mapStateFlow
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
sealed class SettingSections(
|
sealed class SettingSections(
|
||||||
val icon: IconSource,
|
val icon: IconSource,
|
||||||
val name: String,
|
val name: StringSource,
|
||||||
) {
|
) {
|
||||||
data object Appearance : SettingSections(MyIcons.appearance, "Appearance")
|
data object Appearance : SettingSections(MyIcons.appearance, Res.string.appearance.asStringSource())
|
||||||
|
|
||||||
// TODO ADD Network section (proxy , etc..)
|
// TODO ADD Network section (proxy , etc..)
|
||||||
// data object Network : SettingSections(MyIcons.network, "Network")
|
// data object Network : SettingSections(MyIcons.network, "Network")
|
||||||
data object DownloadEngine : SettingSections(MyIcons.downloadEngine, "Download Engine")
|
data object DownloadEngine : SettingSections(MyIcons.downloadEngine, Res.string.download_engine.asStringSource())
|
||||||
data object BrowserIntegration : SettingSections(MyIcons.network, "Browser Integration")
|
data object BrowserIntegration : SettingSections(MyIcons.network, Res.string.browser_integration.asStringSource())
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SettingSectionGetter {
|
interface SettingSectionGetter {
|
||||||
@ -38,27 +45,32 @@ interface SettingSectionGetter {
|
|||||||
|
|
||||||
fun threadCountConfig(appRepository: AppRepository): IntConfigurable {
|
fun threadCountConfig(appRepository: AppRepository): IntConfigurable {
|
||||||
return IntConfigurable(
|
return IntConfigurable(
|
||||||
title = "Thread Count",
|
title = Res.string.settings_download_thread_count.asStringSource(),
|
||||||
description = "Maximum download thread per download item",
|
description = Res.string.settings_download_thread_count_description.asStringSource(),
|
||||||
backedBy = appRepository.threadCount,
|
backedBy = appRepository.threadCount,
|
||||||
range = 1..32,
|
range = 1..32,
|
||||||
renderMode = IntConfigurable.RenderMode.TextField,
|
renderMode = IntConfigurable.RenderMode.TextField,
|
||||||
describe = {
|
describe = {
|
||||||
"a download can have up to $it thread"
|
Res.string.settings_download_thread_count_describe
|
||||||
|
.asStringSourceWithARgs(
|
||||||
|
Res.string.settings_download_thread_count_describe_createArgs(
|
||||||
|
count = it.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dynamicPartDownloadConfig(appRepository: AppRepository): BooleanConfigurable {
|
fun dynamicPartDownloadConfig(appRepository: AppRepository): BooleanConfigurable {
|
||||||
return BooleanConfigurable(
|
return BooleanConfigurable(
|
||||||
title = "Dynamic part creation",
|
title = Res.string.settings_dynamic_part_creation.asStringSource(),
|
||||||
description = "When a part is finished create another part by splitting other parts to improve download speed",
|
description = Res.string.settings_dynamic_part_creation_description.asStringSource(),
|
||||||
backedBy = appRepository.dynamicPartCreation,
|
backedBy = appRepository.dynamicPartCreation,
|
||||||
describe = {
|
describe = {
|
||||||
if (it) {
|
if (it) {
|
||||||
"Enabled"
|
Res.string.enabled.asStringSource()
|
||||||
} else {
|
} else {
|
||||||
"Disabled"
|
Res.string.disabled.asStringSource()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -66,14 +78,14 @@ fun dynamicPartDownloadConfig(appRepository: AppRepository): BooleanConfigurable
|
|||||||
|
|
||||||
fun useServerLastModified(appRepository: AppRepository): BooleanConfigurable {
|
fun useServerLastModified(appRepository: AppRepository): BooleanConfigurable {
|
||||||
return BooleanConfigurable(
|
return BooleanConfigurable(
|
||||||
title = "Server's Last-Modified Time",
|
title = Res.string.settings_use_server_last_modified_time.asStringSource(),
|
||||||
description = "When downloading a file, use server's last modified time for the local file",
|
description = Res.string.settings_use_server_last_modified_time_description.asStringSource(),
|
||||||
backedBy = appRepository.useServerLastModifiedTime,
|
backedBy = appRepository.useServerLastModifiedTime,
|
||||||
describe = {
|
describe = {
|
||||||
if (it) {
|
if (it) {
|
||||||
"Enabled"
|
Res.string.enabled.asStringSource()
|
||||||
} else {
|
} else {
|
||||||
"Disabled"
|
Res.string.disabled.asStringSource()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -81,14 +93,14 @@ fun useServerLastModified(appRepository: AppRepository): BooleanConfigurable {
|
|||||||
|
|
||||||
fun useSparseFileAllocation(appRepository: AppRepository): BooleanConfigurable {
|
fun useSparseFileAllocation(appRepository: AppRepository): BooleanConfigurable {
|
||||||
return BooleanConfigurable(
|
return BooleanConfigurable(
|
||||||
title = "Sparse File Allocation",
|
title = Res.string.settings_use_sparse_file_allocation.asStringSource(),
|
||||||
description = "Create files more efficiently, especially on SSDs, by reducing unnecessary data writing. This can speed up download starts and save disk space. If downloads start slowly, consider disabling this option, as it may not be properly supported on some devices.",
|
description = Res.string.settings_use_sparse_file_allocation_description.asStringSource(),
|
||||||
backedBy = appRepository.useSparseFileAllocation,
|
backedBy = appRepository.useSparseFileAllocation,
|
||||||
describe = {
|
describe = {
|
||||||
if (it) {
|
if (it) {
|
||||||
"Enabled"
|
Res.string.enabled.asStringSource()
|
||||||
} else {
|
} else {
|
||||||
"Disabled"
|
Res.string.disabled.asStringSource()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -96,14 +108,14 @@ fun useSparseFileAllocation(appRepository: AppRepository): BooleanConfigurable {
|
|||||||
|
|
||||||
fun speedLimitConfig(appRepository: AppRepository): SpeedLimitConfigurable {
|
fun speedLimitConfig(appRepository: AppRepository): SpeedLimitConfigurable {
|
||||||
return SpeedLimitConfigurable(
|
return SpeedLimitConfigurable(
|
||||||
title = "Global Speed Limiter",
|
title = Res.string.settings_global_speed_limiter.asStringSource(),
|
||||||
description = "Global download speed limit (0 means unlimited)",
|
description = Res.string.settings_global_speed_limiter_description.asStringSource(),
|
||||||
backedBy = appRepository.speedLimiter,
|
backedBy = appRepository.speedLimiter,
|
||||||
describe = {
|
describe = {
|
||||||
if (it == 0L) {
|
if (it == 0L) {
|
||||||
"Unlimited"
|
Res.string.unlimited.asStringSource()
|
||||||
} else {
|
} else {
|
||||||
convertSpeedToHumanReadable(it)
|
convertSpeedToHumanReadable(it).asStringSource()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -111,48 +123,56 @@ fun speedLimitConfig(appRepository: AppRepository): SpeedLimitConfigurable {
|
|||||||
|
|
||||||
fun useAverageSpeedConfig(appRepository: AppRepository): BooleanConfigurable {
|
fun useAverageSpeedConfig(appRepository: AppRepository): BooleanConfigurable {
|
||||||
return BooleanConfigurable(
|
return BooleanConfigurable(
|
||||||
title = "Show Average Speed",
|
title = Res.string.settings_show_average_speed.asStringSource(),
|
||||||
description = "Download speed in average or precision",
|
description = Res.string.settings_show_average_speed_description.asStringSource(),
|
||||||
backedBy = appRepository.useAverageSpeed,
|
backedBy = appRepository.useAverageSpeed,
|
||||||
describe = {
|
describe = {
|
||||||
if (it) "Average Speed"
|
if (it) Res.string.average_speed.asStringSource()
|
||||||
else "Exact Speed"
|
else Res.string.exact_speed.asStringSource()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun defaultDownloadFolderConfig(appSettings: AppSettingsStorage): FolderConfigurable {
|
fun defaultDownloadFolderConfig(appSettings: AppSettingsStorage): FolderConfigurable {
|
||||||
return FolderConfigurable(
|
return FolderConfigurable(
|
||||||
title = "Default Download Folder",
|
title = Res.string.settings_default_download_folder.asStringSource(),
|
||||||
description = "When you add new download this location is used by default",
|
description = Res.string.settings_default_download_folder_description.asStringSource(),
|
||||||
backedBy = appSettings.defaultDownloadFolder,
|
backedBy = appSettings.defaultDownloadFolder,
|
||||||
validate = {
|
validate = {
|
||||||
FileUtils.canWriteInThisFolder(it)
|
FileUtils.canWriteInThisFolder(it)
|
||||||
},
|
},
|
||||||
describe = {
|
describe = {
|
||||||
"\"$it\" will be used"
|
Res.string
|
||||||
|
.settings_default_download_folder_describe
|
||||||
|
.asStringSourceWithARgs(
|
||||||
|
Res.string.settings_default_download_folder_describe_createArgs(
|
||||||
|
folder = it
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun proxyConfig(proxyManager: ProxyManager, scope: CoroutineScope): ProxyConfigurable {
|
fun proxyConfig(proxyManager: ProxyManager, scope: CoroutineScope): ProxyConfigurable {
|
||||||
return ProxyConfigurable(
|
return ProxyConfigurable(
|
||||||
title = "Use Proxy",
|
title = Res.string.settings_use_proxy.asStringSource(),
|
||||||
description = "Use proxy for downloading files",
|
description = Res.string.settings_use_proxy_description.asStringSource(),
|
||||||
backedBy = proxyManager.proxyData,
|
backedBy = proxyManager.proxyData,
|
||||||
|
|
||||||
validate = {
|
validate = {
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
describe = {
|
describe = {
|
||||||
val str = when (it.proxyMode) {
|
when (it.proxyMode) {
|
||||||
ProxyMode.Direct -> "No proxy"
|
ProxyMode.Direct -> Res.string.settings_use_proxy_describe_no_proxy.asStringSource()
|
||||||
ProxyMode.UseSystem -> "System proxy"
|
ProxyMode.UseSystem -> Res.string.settings_use_proxy_describe_system_proxy.asStringSource()
|
||||||
ProxyMode.Manual -> it.proxyWithRules.proxy.run {
|
ProxyMode.Manual -> Res.string.settings_use_proxy_describe_manual_proxy
|
||||||
"$type $host:$port"
|
.asStringSourceWithARgs(
|
||||||
}
|
Res.string.settings_use_proxy_describe_manual_proxy_createArgs(
|
||||||
|
value = it.proxyWithRules.proxy.run { "$type $host:$port" }
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
"$str will be used"
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -192,8 +212,8 @@ fun themeConfig(
|
|||||||
val currentThemeName = themeManager.currentThemeInfo
|
val currentThemeName = themeManager.currentThemeInfo
|
||||||
val themes = themeManager.possibleThemesToSelect
|
val themes = themeManager.possibleThemesToSelect
|
||||||
return ThemeConfigurable(
|
return ThemeConfigurable(
|
||||||
title = "Theme",
|
title = Res.string.settings_theme.asStringSource(),
|
||||||
description = "Select theme",
|
description = Res.string.settings_theme_description.asStringSource(),
|
||||||
backedBy = createMutableStateFlowFromStateFlow(
|
backedBy = createMutableStateFlowFromStateFlow(
|
||||||
flow = currentThemeName,
|
flow = currentThemeName,
|
||||||
updater = {
|
updater = {
|
||||||
@ -203,21 +223,48 @@ fun themeConfig(
|
|||||||
),
|
),
|
||||||
possibleValues = themes.value,
|
possibleValues = themes.value,
|
||||||
describe = {
|
describe = {
|
||||||
it.name
|
it.name.asStringSource()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun languageConfig(
|
||||||
|
languageManager: LanguageManager,
|
||||||
|
scope: CoroutineScope,
|
||||||
|
): EnumConfigurable<LanguageInfo> {
|
||||||
|
val currentLanguageName = languageManager.selectedLanguage
|
||||||
|
val allLanguages = languageManager.languageList
|
||||||
|
return EnumConfigurable(
|
||||||
|
title = Res.string.settings_language.asStringSource(),
|
||||||
|
description = "".asStringSource(),
|
||||||
|
backedBy = createMutableStateFlowFromStateFlow(
|
||||||
|
flow = currentLanguageName.mapStateFlow { l ->
|
||||||
|
allLanguages.value.find {
|
||||||
|
it.languageCode == l
|
||||||
|
} ?: LanguageManager.DefaultLanguageInfo
|
||||||
|
},
|
||||||
|
updater = {
|
||||||
|
languageManager.selectLanguage(it.languageCode)
|
||||||
|
},
|
||||||
|
scope = scope,
|
||||||
|
),
|
||||||
|
possibleValues = allLanguages.value,
|
||||||
|
describe = {
|
||||||
|
it.nativeName.asStringSource()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mergeTopBarWithTitleBarConfig(appSettings: AppSettingsStorage): BooleanConfigurable {
|
fun mergeTopBarWithTitleBarConfig(appSettings: AppSettingsStorage): BooleanConfigurable {
|
||||||
return BooleanConfigurable(
|
return BooleanConfigurable(
|
||||||
title = "Compact Top Bar",
|
title = Res.string.settings_compact_top_bar.asStringSource(),
|
||||||
description = "Merge top bar with title bar when the main window has enough width",
|
description = Res.string.settings_compact_top_bar_description.asStringSource(),
|
||||||
backedBy = appSettings.mergeTopBarWithTitleBar,
|
backedBy = appSettings.mergeTopBarWithTitleBar,
|
||||||
describe = {
|
describe = {
|
||||||
if (it) {
|
if (it) {
|
||||||
"Enabled"
|
Res.string.enabled.asStringSource()
|
||||||
} else {
|
} else {
|
||||||
"Disabled"
|
Res.string.disabled.asStringSource()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -225,15 +272,15 @@ fun mergeTopBarWithTitleBarConfig(appSettings: AppSettingsStorage): BooleanConfi
|
|||||||
|
|
||||||
fun autoStartConfig(appSettings: AppSettingsStorage): BooleanConfigurable {
|
fun autoStartConfig(appSettings: AppSettingsStorage): BooleanConfigurable {
|
||||||
return BooleanConfigurable(
|
return BooleanConfigurable(
|
||||||
title = "Start On Boot",
|
title = Res.string.settings_start_on_boot.asStringSource(),
|
||||||
description = "Auto start application on user logins",
|
description = Res.string.settings_start_on_boot_description.asStringSource(),
|
||||||
backedBy = appSettings.autoStartOnBoot,
|
backedBy = appSettings.autoStartOnBoot,
|
||||||
renderMode = BooleanConfigurable.RenderMode.Switch,
|
renderMode = BooleanConfigurable.RenderMode.Switch,
|
||||||
describe = {
|
describe = {
|
||||||
if (it) {
|
if (it) {
|
||||||
"Auto Start Enabled"
|
Res.string.enabled.asStringSource()
|
||||||
} else {
|
} else {
|
||||||
"Auto Start Disabled"
|
Res.string.disabled.asStringSource()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -241,15 +288,15 @@ fun autoStartConfig(appSettings: AppSettingsStorage): BooleanConfigurable {
|
|||||||
|
|
||||||
fun playSoundNotification(appSettings: AppSettingsStorage): BooleanConfigurable {
|
fun playSoundNotification(appSettings: AppSettingsStorage): BooleanConfigurable {
|
||||||
return BooleanConfigurable(
|
return BooleanConfigurable(
|
||||||
title = "Notification Sound",
|
title = Res.string.settings_notification_sound.asStringSource(),
|
||||||
description = "Play sound on new notification",
|
description = Res.string.settings_notification_sound_description.asStringSource(),
|
||||||
backedBy = appSettings.notificationSound,
|
backedBy = appSettings.notificationSound,
|
||||||
renderMode = BooleanConfigurable.RenderMode.Switch,
|
renderMode = BooleanConfigurable.RenderMode.Switch,
|
||||||
describe = {
|
describe = {
|
||||||
if (it) {
|
if (it) {
|
||||||
"Play sounds"
|
Res.string.enabled.asStringSource()
|
||||||
} else {
|
} else {
|
||||||
"Muted"
|
Res.string.disabled.asStringSource()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -257,15 +304,15 @@ fun playSoundNotification(appSettings: AppSettingsStorage): BooleanConfigurable
|
|||||||
|
|
||||||
fun browserIntegrationEnabled(appRepository: AppRepository): BooleanConfigurable {
|
fun browserIntegrationEnabled(appRepository: AppRepository): BooleanConfigurable {
|
||||||
return BooleanConfigurable(
|
return BooleanConfigurable(
|
||||||
title = "Browser Integration",
|
title = Res.string.settings_browser_integration.asStringSource(),
|
||||||
description = "Accept downloads from browsers",
|
description = Res.string.settings_browser_integration_description.asStringSource(),
|
||||||
backedBy = appRepository.integrationEnabled,
|
backedBy = appRepository.integrationEnabled,
|
||||||
renderMode = BooleanConfigurable.RenderMode.Switch,
|
renderMode = BooleanConfigurable.RenderMode.Switch,
|
||||||
describe = {
|
describe = {
|
||||||
if (it) {
|
if (it) {
|
||||||
"Enabled"
|
Res.string.enabled.asStringSource()
|
||||||
} else {
|
} else {
|
||||||
"Disabled"
|
Res.string.disabled.asStringSource()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -273,11 +320,16 @@ fun browserIntegrationEnabled(appRepository: AppRepository): BooleanConfigurable
|
|||||||
|
|
||||||
fun browserIntegrationPort(appRepository: AppRepository): IntConfigurable {
|
fun browserIntegrationPort(appRepository: AppRepository): IntConfigurable {
|
||||||
return IntConfigurable(
|
return IntConfigurable(
|
||||||
title = "Server Port",
|
title = Res.string.settings_browser_integration_server_port.asStringSource(),
|
||||||
description = "port for browser integration",
|
description = Res.string.settings_browser_integration_server_port_description.asStringSource(),
|
||||||
backedBy = appRepository.integrationPort,
|
backedBy = appRepository.integrationPort,
|
||||||
describe = {
|
describe = {
|
||||||
"listen to $it"
|
Res.string.settings_browser_integration_server_port_describe
|
||||||
|
.asStringSourceWithARgs(
|
||||||
|
Res.string.settings_browser_integration_server_port_describe_createArgs(
|
||||||
|
port = it.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
range = 0..65000,
|
range = 0..65000,
|
||||||
)
|
)
|
||||||
@ -296,11 +348,13 @@ class SettingsComponent(
|
|||||||
val appRepository by inject<AppRepository>()
|
val appRepository by inject<AppRepository>()
|
||||||
val proxyManager by inject<ProxyManager>()
|
val proxyManager by inject<ProxyManager>()
|
||||||
val themeManager by inject<ThemeManager>()
|
val themeManager by inject<ThemeManager>()
|
||||||
|
val languageManager by inject<LanguageManager>()
|
||||||
val allConfigs = object : SettingSectionGetter {
|
val allConfigs = object : SettingSectionGetter {
|
||||||
override operator fun get(key: SettingSections): List<Configurable<*>> {
|
override operator fun get(key: SettingSections): List<Configurable<*>> {
|
||||||
return when (key) {
|
return when (key) {
|
||||||
Appearance -> listOf(
|
Appearance -> listOf(
|
||||||
themeConfig(themeManager, scope),
|
themeConfig(themeManager, scope),
|
||||||
|
languageConfig(languageManager, scope),
|
||||||
// uiScaleConfig(appSettings),
|
// uiScaleConfig(appSettings),
|
||||||
autoStartConfig(appSettings),
|
autoStartConfig(appSettings),
|
||||||
mergeTopBarWithTitleBarConfig(appSettings),
|
mergeTopBarWithTitleBarConfig(appSettings),
|
||||||
|
@ -23,6 +23,9 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
import kotlinx.coroutines.channels.ticker
|
import kotlinx.coroutines.channels.ticker
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -46,7 +49,7 @@ private fun SideBar(
|
|||||||
for (i in settingsComponent.pages) {
|
for (i in settingsComponent.pages) {
|
||||||
SideBarItem(
|
SideBarItem(
|
||||||
icon = i.icon,
|
icon = i.icon,
|
||||||
name = i.name,
|
name = i.name.rememberString(),
|
||||||
isSelected = settingsComponent.currentPage == i,
|
isSelected = settingsComponent.currentPage == i,
|
||||||
onClick = {
|
onClick = {
|
||||||
settingsComponent.currentPage = i
|
settingsComponent.currentPage = i
|
||||||
@ -101,7 +104,7 @@ fun SettingsPage(
|
|||||||
settingsComponent: SettingsComponent,
|
settingsComponent: SettingsComponent,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
) {
|
) {
|
||||||
WindowTitle("Settings")
|
WindowTitle(myStringResource(Res.string.settings))
|
||||||
// WindowIcon(MyIcons.settings)
|
// WindowIcon(MyIcons.settings)
|
||||||
WindowIcon(MyIcons.appIcon)
|
WindowIcon(MyIcons.appIcon)
|
||||||
Row {
|
Row {
|
||||||
|
@ -6,6 +6,7 @@ import com.abdownloadmanager.desktop.pages.settings.ThemeInfo
|
|||||||
import com.abdownloadmanager.desktop.pages.settings.configurable.BooleanConfigurable.RenderMode
|
import com.abdownloadmanager.desktop.pages.settings.configurable.BooleanConfigurable.RenderMode
|
||||||
import com.abdownloadmanager.desktop.ui.theme.MyColors
|
import com.abdownloadmanager.desktop.ui.theme.MyColors
|
||||||
import com.abdownloadmanager.utils.proxy.ProxyData
|
import com.abdownloadmanager.utils.proxy.ProxyData
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@ -17,11 +18,11 @@ private val DefaultEnabledValue get() = MutableStateFlow(true)
|
|||||||
private val DefaultVisibleValue get() = MutableStateFlow(true)
|
private val DefaultVisibleValue get() = MutableStateFlow(true)
|
||||||
|
|
||||||
sealed class Configurable<T>(
|
sealed class Configurable<T>(
|
||||||
val title: String,
|
val title: StringSource,
|
||||||
val description: String,
|
val description: StringSource,
|
||||||
val backedBy: MutableStateFlow<T>,
|
val backedBy: MutableStateFlow<T>,
|
||||||
val validate: (T) -> Boolean = { true },
|
val validate: (T) -> Boolean = { true },
|
||||||
val describe: (T) -> String,
|
val describe: (T) -> StringSource,
|
||||||
val enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
val enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
||||||
val visible: StateFlow<Boolean> = DefaultVisibleValue,
|
val visible: StateFlow<Boolean> = DefaultVisibleValue,
|
||||||
) {
|
) {
|
||||||
@ -39,10 +40,10 @@ sealed class Configurable<T>(
|
|||||||
|
|
||||||
//primitives
|
//primitives
|
||||||
class IntConfigurable(
|
class IntConfigurable(
|
||||||
title: String,
|
title: StringSource,
|
||||||
description: String,
|
description: StringSource,
|
||||||
backedBy: MutableStateFlow<Int>,
|
backedBy: MutableStateFlow<Int>,
|
||||||
describe: ((Int) -> String),
|
describe: ((Int) -> StringSource),
|
||||||
val range: IntRange,
|
val range: IntRange,
|
||||||
val renderMode: RenderMode = RenderMode.TextField,
|
val renderMode: RenderMode = RenderMode.TextField,
|
||||||
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
||||||
@ -62,10 +63,10 @@ class IntConfigurable(
|
|||||||
}
|
}
|
||||||
|
|
||||||
sealed class BaseLongConfigurable(
|
sealed class BaseLongConfigurable(
|
||||||
title: String,
|
title: StringSource,
|
||||||
description: String,
|
description: StringSource,
|
||||||
backedBy: MutableStateFlow<Long>,
|
backedBy: MutableStateFlow<Long>,
|
||||||
describe: ((Long) -> String),
|
describe: ((Long) -> StringSource),
|
||||||
val range: LongRange,
|
val range: LongRange,
|
||||||
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
||||||
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
||||||
@ -82,10 +83,10 @@ sealed class BaseLongConfigurable(
|
|||||||
)
|
)
|
||||||
|
|
||||||
class LongConfigurable(
|
class LongConfigurable(
|
||||||
title: String,
|
title: StringSource,
|
||||||
description: String,
|
description: StringSource,
|
||||||
backedBy: MutableStateFlow<Long>,
|
backedBy: MutableStateFlow<Long>,
|
||||||
describe: ((Long) -> String),
|
describe: ((Long) -> StringSource),
|
||||||
range: LongRange,
|
range: LongRange,
|
||||||
val renderMode: RenderMode = RenderMode.TextField,
|
val renderMode: RenderMode = RenderMode.TextField,
|
||||||
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
||||||
@ -105,10 +106,10 @@ class LongConfigurable(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class BooleanConfigurable(
|
class BooleanConfigurable(
|
||||||
title: String,
|
title: StringSource,
|
||||||
description: String,
|
description: StringSource,
|
||||||
backedBy: MutableStateFlow<Boolean>,
|
backedBy: MutableStateFlow<Boolean>,
|
||||||
describe: ((Boolean) -> String),
|
describe: ((Boolean) -> StringSource),
|
||||||
val renderMode: RenderMode = RenderMode.Switch,
|
val renderMode: RenderMode = RenderMode.Switch,
|
||||||
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
||||||
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
||||||
@ -127,14 +128,14 @@ class BooleanConfigurable(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class FloatConfigurable(
|
class FloatConfigurable(
|
||||||
title: String,
|
title: StringSource,
|
||||||
description: String,
|
description: StringSource,
|
||||||
backedBy: MutableStateFlow<Float>,
|
backedBy: MutableStateFlow<Float>,
|
||||||
val range: ClosedFloatingPointRange<Float>,
|
val range: ClosedFloatingPointRange<Float>,
|
||||||
val steps: Int = 0,
|
val steps: Int = 0,
|
||||||
val renderMode: RenderMode = RenderMode.TextField,
|
val renderMode: RenderMode = RenderMode.TextField,
|
||||||
|
|
||||||
describe: ((Float) -> String),
|
describe: ((Float) -> StringSource),
|
||||||
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
||||||
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
||||||
) : Configurable<Float>(
|
) : Configurable<Float>(
|
||||||
@ -152,10 +153,10 @@ class FloatConfigurable(
|
|||||||
}
|
}
|
||||||
|
|
||||||
open class StringConfigurable(
|
open class StringConfigurable(
|
||||||
title: String,
|
title: StringSource,
|
||||||
description: String,
|
description: StringSource,
|
||||||
backedBy: MutableStateFlow<String>,
|
backedBy: MutableStateFlow<String>,
|
||||||
describe: ((String) -> String),
|
describe: ((String) -> StringSource),
|
||||||
validate: (String) -> Boolean = { true },
|
validate: (String) -> Boolean = { true },
|
||||||
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
||||||
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
||||||
@ -170,10 +171,10 @@ open class StringConfigurable(
|
|||||||
)
|
)
|
||||||
|
|
||||||
class FolderConfigurable(
|
class FolderConfigurable(
|
||||||
title: String,
|
title: StringSource,
|
||||||
description: String,
|
description: StringSource,
|
||||||
backedBy: MutableStateFlow<String>,
|
backedBy: MutableStateFlow<String>,
|
||||||
describe: ((String) -> String),
|
describe: ((String) -> StringSource),
|
||||||
validate: (String) -> Boolean,
|
validate: (String) -> Boolean,
|
||||||
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
||||||
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
||||||
@ -188,10 +189,10 @@ class FolderConfigurable(
|
|||||||
)
|
)
|
||||||
|
|
||||||
sealed class BaseEnumConfigurable<T>(
|
sealed class BaseEnumConfigurable<T>(
|
||||||
title: String,
|
title: StringSource,
|
||||||
description: String,
|
description: StringSource,
|
||||||
backedBy: MutableStateFlow<T>,
|
backedBy: MutableStateFlow<T>,
|
||||||
describe: ((T) -> String),
|
describe: ((T) -> StringSource),
|
||||||
val possibleValues: List<T>,
|
val possibleValues: List<T>,
|
||||||
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
||||||
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
||||||
@ -209,10 +210,10 @@ sealed class BaseEnumConfigurable<T>(
|
|||||||
|
|
||||||
//more complex
|
//more complex
|
||||||
open class EnumConfigurable<T>(
|
open class EnumConfigurable<T>(
|
||||||
title: String,
|
title: StringSource,
|
||||||
description: String,
|
description: StringSource,
|
||||||
backedBy: MutableStateFlow<T>,
|
backedBy: MutableStateFlow<T>,
|
||||||
describe: ((T) -> String),
|
describe: ((T) -> StringSource),
|
||||||
possibleValues: List<T>,
|
possibleValues: List<T>,
|
||||||
val renderMode: RenderMode = RenderMode.Spinner,
|
val renderMode: RenderMode = RenderMode.Spinner,
|
||||||
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
||||||
@ -232,10 +233,10 @@ open class EnumConfigurable<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ThemeConfigurable(
|
class ThemeConfigurable(
|
||||||
title: String,
|
title: StringSource,
|
||||||
description: String,
|
description: StringSource,
|
||||||
backedBy: MutableStateFlow<ThemeInfo>,
|
backedBy: MutableStateFlow<ThemeInfo>,
|
||||||
describe: (ThemeInfo) -> String,
|
describe: (ThemeInfo) -> StringSource,
|
||||||
possibleValues: List<ThemeInfo>,
|
possibleValues: List<ThemeInfo>,
|
||||||
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
||||||
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
||||||
@ -250,10 +251,10 @@ class ThemeConfigurable(
|
|||||||
)
|
)
|
||||||
|
|
||||||
class SpeedLimitConfigurable(
|
class SpeedLimitConfigurable(
|
||||||
title: String,
|
title: StringSource,
|
||||||
description: String,
|
description: StringSource,
|
||||||
backedBy: MutableStateFlow<Long>,
|
backedBy: MutableStateFlow<Long>,
|
||||||
describe: (Long) -> String,
|
describe: (Long) -> StringSource,
|
||||||
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
||||||
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
||||||
) : BaseLongConfigurable(
|
) : BaseLongConfigurable(
|
||||||
@ -267,10 +268,10 @@ class SpeedLimitConfigurable(
|
|||||||
)
|
)
|
||||||
|
|
||||||
class TimeConfigurable(
|
class TimeConfigurable(
|
||||||
title: String,
|
title: StringSource,
|
||||||
description: String,
|
description: StringSource,
|
||||||
backedBy: MutableStateFlow<LocalTime>,
|
backedBy: MutableStateFlow<LocalTime>,
|
||||||
describe: (LocalTime) -> String,
|
describe: (LocalTime) -> StringSource,
|
||||||
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
||||||
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
||||||
) : Configurable<LocalTime>(
|
) : Configurable<LocalTime>(
|
||||||
@ -283,10 +284,10 @@ class TimeConfigurable(
|
|||||||
)
|
)
|
||||||
|
|
||||||
class DayOfWeekConfigurable(
|
class DayOfWeekConfigurable(
|
||||||
title: String,
|
title: StringSource,
|
||||||
description: String,
|
description: StringSource,
|
||||||
backedBy: MutableStateFlow<Set<DayOfWeek>>,
|
backedBy: MutableStateFlow<Set<DayOfWeek>>,
|
||||||
describe: (Set<DayOfWeek>) -> String,
|
describe: (Set<DayOfWeek>) -> StringSource,
|
||||||
validate: (Set<DayOfWeek>) -> Boolean,
|
validate: (Set<DayOfWeek>) -> Boolean,
|
||||||
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
||||||
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
||||||
@ -301,10 +302,10 @@ class DayOfWeekConfigurable(
|
|||||||
)
|
)
|
||||||
|
|
||||||
class ProxyConfigurable(
|
class ProxyConfigurable(
|
||||||
title: String,
|
title: StringSource,
|
||||||
description: String,
|
description: StringSource,
|
||||||
backedBy: MutableStateFlow<ProxyData>,
|
backedBy: MutableStateFlow<ProxyData>,
|
||||||
describe: (ProxyData) -> String,
|
describe: (ProxyData) -> StringSource,
|
||||||
validate: (ProxyData) -> Boolean,
|
validate: (ProxyData) -> Boolean,
|
||||||
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
enabled: StateFlow<Boolean> = DefaultEnabledValue,
|
||||||
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
visible: StateFlow<Boolean> = DefaultVisibleValue,
|
||||||
|
@ -2,12 +2,13 @@ package com.abdownloadmanager.desktop.pages.settings.configurable.widgets
|
|||||||
|
|
||||||
import com.abdownloadmanager.desktop.pages.settings.configurable.Configurable
|
import com.abdownloadmanager.desktop.pages.settings.configurable.Configurable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
data class ConfigurableGroup(
|
data class ConfigurableGroup(
|
||||||
val groupTitle:StateFlow<String?> = MutableStateFlow(null),
|
val groupTitle: StateFlow<StringSource?> = MutableStateFlow(null),
|
||||||
val mainConfigurable:Configurable<*>?=null,
|
val mainConfigurable:Configurable<*>?=null,
|
||||||
val nestedEnabled:StateFlow<Boolean> =MutableStateFlow(true),
|
val nestedEnabled:StateFlow<Boolean> =MutableStateFlow(true),
|
||||||
val nestedVisible:StateFlow<Boolean> =MutableStateFlow(true),
|
val nestedVisible:StateFlow<Boolean> =MutableStateFlow(true),
|
||||||
|
@ -32,7 +32,7 @@ fun RenderEnumConfig(cfg: EnumConfigurable<Any?>, modifier: Modifier) {
|
|||||||
modifier = Modifier.widthIn(min = 160.dp),
|
modifier = Modifier.widthIn(min = 160.dp),
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
render = {
|
render = {
|
||||||
Text(cfg.describe(it))
|
Text(cfg.describe(it).rememberString())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ fun RenderFolderConfig(cfg: FolderConfigurable, modifier: Modifier) {
|
|||||||
val setValue = cfg::set
|
val setValue = cfg::set
|
||||||
|
|
||||||
val pickFolderLauncher = rememberDirectoryPickerLauncher(
|
val pickFolderLauncher = rememberDirectoryPickerLauncher(
|
||||||
title = cfg.title,
|
title = cfg.title.rememberString(),
|
||||||
initialDirectory = remember(value) {
|
initialDirectory = remember(value) {
|
||||||
runCatching {
|
runCatching {
|
||||||
File(value).canonicalPath
|
File(value).canonicalPath
|
||||||
@ -51,7 +51,7 @@ fun RenderFolderConfig(cfg: FolderConfigurable, modifier: Modifier) {
|
|||||||
},
|
},
|
||||||
shape = RectangleShape,
|
shape = RectangleShape,
|
||||||
textPadding = PaddingValues(4.dp),
|
textPadding = PaddingValues(4.dp),
|
||||||
placeholder = cfg.title,
|
placeholder = cfg.title.rememberString(),
|
||||||
end = {
|
end = {
|
||||||
MyIcon(
|
MyIcon(
|
||||||
icon = MyIcons.folder,
|
icon = MyIcons.folder,
|
||||||
|
@ -32,7 +32,7 @@ fun RenderConfigurableGroup(
|
|||||||
.padding(start = verticalPadding.dp)
|
.padding(start = verticalPadding.dp)
|
||||||
.padding(horizontal = 4.dp)
|
.padding(horizontal = 4.dp)
|
||||||
) {
|
) {
|
||||||
title?.let {
|
title?.rememberString()?.let {
|
||||||
Text(
|
Text(
|
||||||
text = it,
|
text = it,
|
||||||
fontSize = myTextSizes.base,
|
fontSize = myTextSizes.base,
|
||||||
|
@ -18,7 +18,11 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
import kotlinx.datetime.DayOfWeek
|
import kotlinx.datetime.DayOfWeek
|
||||||
|
import java.time.DayOfWeek.*
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RenderDayOfWeekConfigurable(cfg: DayOfWeekConfigurable, modifier: Modifier) {
|
fun RenderDayOfWeekConfigurable(cfg: DayOfWeekConfigurable, modifier: Modifier) {
|
||||||
@ -31,7 +35,7 @@ fun RenderDayOfWeekConfigurable(cfg: DayOfWeekConfigurable, modifier: Modifier)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun selectDay(dayOfWeek: DayOfWeek, select: Boolean) {
|
fun selectDay(dayOfWeek: DayOfWeek, select: Boolean) {
|
||||||
if (!enabled)return
|
if (!enabled) return
|
||||||
if (select) {
|
if (select) {
|
||||||
setValue(
|
setValue(
|
||||||
value.plus(dayOfWeek).sorted().toSet()
|
value.plus(dayOfWeek).sorted().toSet()
|
||||||
@ -52,7 +56,7 @@ fun RenderDayOfWeekConfigurable(cfg: DayOfWeekConfigurable, modifier: Modifier)
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
Modifier.ifThen(!enabled){
|
Modifier.ifThen(!enabled) {
|
||||||
alpha(0.5f)
|
alpha(0.5f)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
@ -61,7 +65,7 @@ fun RenderDayOfWeekConfigurable(cfg: DayOfWeekConfigurable, modifier: Modifier)
|
|||||||
col.forEach { dayOfWeek ->
|
col.forEach { dayOfWeek ->
|
||||||
RenderDayOfWeek(
|
RenderDayOfWeek(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
enabled=enabled,
|
enabled = enabled,
|
||||||
dayOfWeek = dayOfWeek,
|
dayOfWeek = dayOfWeek,
|
||||||
selected = isSelected(dayOfWeek),
|
selected = isSelected(dayOfWeek),
|
||||||
onSelect = { s, isSelected ->
|
onSelect = { s, isSelected ->
|
||||||
@ -83,7 +87,7 @@ fun RenderDayOfWeek(
|
|||||||
dayOfWeek: DayOfWeek,
|
dayOfWeek: DayOfWeek,
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
onSelect: (DayOfWeek, Boolean) -> Unit,
|
onSelect: (DayOfWeek, Boolean) -> Unit,
|
||||||
enabled: Boolean=true,
|
enabled: Boolean = true,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@ -93,7 +97,7 @@ fun RenderDayOfWeek(
|
|||||||
.ifThen(selected) {
|
.ifThen(selected) {
|
||||||
background(myColors.onBackground / 10)
|
background(myColors.onBackground / 10)
|
||||||
}
|
}
|
||||||
.clickable(enabled=enabled) {
|
.clickable(enabled = enabled) {
|
||||||
onSelect(dayOfWeek, !selected)
|
onSelect(dayOfWeek, !selected)
|
||||||
}
|
}
|
||||||
.padding(vertical = 2.dp)
|
.padding(vertical = 2.dp)
|
||||||
@ -104,16 +108,26 @@ fun RenderDayOfWeek(
|
|||||||
MyIcons.check,
|
MyIcons.check,
|
||||||
null,
|
null,
|
||||||
Modifier.size(8.dp)
|
Modifier.size(8.dp)
|
||||||
.alpha(if (selected)1f else 0f ),
|
.alpha(if (selected) 1f else 0f),
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(2.dp))
|
Spacer(Modifier.width(2.dp))
|
||||||
Text(
|
Text(
|
||||||
text = dayOfWeek.toString(),
|
text = dayOfWeek.asStringSource().rememberString(),
|
||||||
modifier = Modifier.alpha(
|
modifier = Modifier.alpha(
|
||||||
if(selected) 1f
|
if (selected) 1f
|
||||||
else 0.5f
|
else 0.5f
|
||||||
),
|
),
|
||||||
fontSize = myTextSizes.xs,
|
fontSize = myTextSizes.xs,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun DayOfWeek.asStringSource() = when (this) {
|
||||||
|
MONDAY -> Res.string.monday
|
||||||
|
TUESDAY -> Res.string.tuesday
|
||||||
|
WEDNESDAY -> Res.string.wednesday
|
||||||
|
THURSDAY -> Res.string.thursday
|
||||||
|
FRIDAY -> Res.string.friday
|
||||||
|
SATURDAY -> Res.string.saturday
|
||||||
|
SUNDAY -> Res.string.sunday
|
||||||
|
}.asStringSource()
|
@ -177,20 +177,21 @@ fun <T> TitleAndDescription(
|
|||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(
|
Text(
|
||||||
cfg.title,
|
cfg.title.rememberString(),
|
||||||
fontSize = myTextSizes.base,
|
fontSize = myTextSizes.base,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
if (cfg.description.isNotBlank()) {
|
if (cfg.description.rememberString().isNotBlank()) {
|
||||||
Spacer(Modifier.size(4.dp))
|
Spacer(Modifier.size(4.dp))
|
||||||
Help(cfg)
|
Help(cfg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (describe) {
|
if (describe) {
|
||||||
val value = cfg.backedBy.collectAsState().value
|
val value = cfg.backedBy.collectAsState().value
|
||||||
val describeContent = remember(value) {
|
val describedStringSource = remember(value) {
|
||||||
cfg.describe(value)
|
cfg.describe(value)
|
||||||
}
|
}
|
||||||
|
val describeContent = describedStringSource.rememberString()
|
||||||
if (describeContent.isNotBlank()) {
|
if (describeContent.isNotBlank()) {
|
||||||
WithContentAlpha(0.75f){
|
WithContentAlpha(0.75f){
|
||||||
Text(describeContent,
|
Text(describeContent,
|
||||||
@ -341,7 +342,7 @@ private fun Help(
|
|||||||
) {
|
) {
|
||||||
WithContentColor(myColors.onSurface) {
|
WithContentColor(myColors.onSurface) {
|
||||||
Text(
|
Text(
|
||||||
cfg.description,
|
cfg.description.rememberString(),
|
||||||
fontSize = myTextSizes.base,
|
fontSize = myTextSizes.base,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ fun RenderThemeConfig(cfg: ThemeConfigurable, modifier: Modifier) {
|
|||||||
.size(16.dp)
|
.size(16.dp)
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(16.dp))
|
Spacer(Modifier.width(16.dp))
|
||||||
Text(cfg.describe(it), fontSize = myTextSizes.lg)
|
Text(cfg.describe(it).rememberString(), fontSize = myTextSizes.lg)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -12,12 +12,14 @@ import androidx.compose.ui.unit.DpSize
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.WindowPosition
|
import androidx.compose.ui.window.WindowPosition
|
||||||
import androidx.compose.ui.window.rememberWindowState
|
import androidx.compose.ui.window.rememberWindowState
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
import ir.amirab.downloader.downloaditem.DownloadJobStatus
|
import ir.amirab.downloader.downloaditem.DownloadJobStatus
|
||||||
import ir.amirab.downloader.monitor.CompletedDownloadItemState
|
import ir.amirab.downloader.monitor.CompletedDownloadItemState
|
||||||
import ir.amirab.downloader.monitor.IDownloadItemState
|
import ir.amirab.downloader.monitor.IDownloadItemState
|
||||||
import ir.amirab.downloader.monitor.ProcessingDownloadItemState
|
import ir.amirab.downloader.monitor.ProcessingDownloadItemState
|
||||||
import ir.amirab.downloader.monitor.statusOrFinished
|
import ir.amirab.downloader.monitor.statusOrFinished
|
||||||
import ir.amirab.downloader.utils.ExceptionUtils
|
import ir.amirab.downloader.utils.ExceptionUtils
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Taskbar
|
import java.awt.Taskbar
|
||||||
import java.awt.Window
|
import java.awt.Window
|
||||||
@ -86,7 +88,7 @@ fun ShowDownloadDialogs(component: DownloadDialogManager) {
|
|||||||
window.minimumSize = Dimension(defaultWidth.toInt(), defaultHeight.toInt())
|
window.minimumSize = Dimension(defaultWidth.toInt(), defaultHeight.toInt())
|
||||||
}
|
}
|
||||||
val singleDownloadPageSizing = remember(showPartInfo) { SingleDownloadPageSizing() }
|
val singleDownloadPageSizing = remember(showPartInfo) { SingleDownloadPageSizing() }
|
||||||
WindowTitle(itemState?.let { getDownloadTitle(it) } ?: "Download")
|
WindowTitle(itemState?.let { getDownloadTitle(it) } ?: myStringResource(Res.string.download))
|
||||||
WindowIcon(MyIcons.appIcon)
|
WindowIcon(MyIcons.appIcon)
|
||||||
var h = defaultHeight
|
var h = defaultHeight
|
||||||
var w = defaultWidth
|
var w = defaultWidth
|
||||||
|
@ -38,17 +38,28 @@ import androidx.compose.ui.unit.DpOffset
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Popup
|
import androidx.compose.ui.window.Popup
|
||||||
import androidx.compose.ui.window.rememberComponentRectPositionProvider
|
import androidx.compose.ui.window.rememberComponentRectPositionProvider
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
import com.abdownloadmanager.utils.compose.useIsInDebugMode
|
import com.abdownloadmanager.utils.compose.useIsInDebugMode
|
||||||
import ir.amirab.downloader.downloaditem.DownloadJobStatus
|
import ir.amirab.downloader.downloaditem.DownloadJobStatus
|
||||||
import ir.amirab.downloader.monitor.*
|
import ir.amirab.downloader.monitor.*
|
||||||
import ir.amirab.downloader.part.PartDownloadStatus
|
import ir.amirab.downloader.part.PartDownloadStatus
|
||||||
import ir.amirab.downloader.utils.ExceptionUtils
|
import ir.amirab.downloader.utils.ExceptionUtils
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
|
|
||||||
enum class SingleDownloadPageSections(
|
enum class SingleDownloadPageSections(
|
||||||
|
val title: StringSource,
|
||||||
val icon: IconSource,
|
val icon: IconSource,
|
||||||
) {
|
) {
|
||||||
Info(MyIcons.info),
|
Info(
|
||||||
Settings(MyIcons.settings),
|
Res.string.info.asStringSource(),
|
||||||
|
MyIcons.info
|
||||||
|
),
|
||||||
|
Settings(
|
||||||
|
Res.string.settings.asStringSource(),
|
||||||
|
MyIcons.settings
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
private val tabs = SingleDownloadPageSections.entries.toList()
|
private val tabs = SingleDownloadPageSections.entries.toList()
|
||||||
@ -77,7 +88,7 @@ fun SingleDownloadPage(singleDownloadComponent: SingleDownloadComponent) {
|
|||||||
selectedTab = tab
|
selectedTab = tab
|
||||||
},
|
},
|
||||||
icon = tab.icon,
|
icon = tab.icon,
|
||||||
title = tab.toString()
|
title = tab.title
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -330,22 +341,18 @@ fun ColumnScope.RenderPartInfo(itemState: ProcessingDownloadItemState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PartInfoCells.Status -> {
|
PartInfoCells.Status -> {
|
||||||
SimpleCellText("${prettifyStatus(it.value.status)}")
|
SimpleCellText(prettifyStatus(it.value.status).rememberString())
|
||||||
}
|
}
|
||||||
|
|
||||||
PartInfoCells.Downloaded -> {
|
PartInfoCells.Downloaded -> {
|
||||||
SimpleCellText("${convertSizeToHumanReadable(it.value.howMuchProceed)}")
|
SimpleCellText(convertSizeToHumanReadable(it.value.howMuchProceed).rememberString())
|
||||||
}
|
}
|
||||||
|
|
||||||
PartInfoCells.Total -> {
|
PartInfoCells.Total -> {
|
||||||
SimpleCellText(
|
SimpleCellText(
|
||||||
"${
|
it.value.length?.let { length ->
|
||||||
it.value.length?.let { length ->
|
convertSizeToHumanReadable(length).rememberString()
|
||||||
convertSizeToHumanReadable(
|
} ?: myStringResource(Res.string.unknown),
|
||||||
length
|
|
||||||
)
|
|
||||||
} ?: "Unknown"
|
|
||||||
}",
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -381,14 +388,14 @@ fun ColumnScope.RenderPartInfo(itemState: ProcessingDownloadItemState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun prettifyStatus(status: PartDownloadStatus): String {
|
fun prettifyStatus(status: PartDownloadStatus): StringSource {
|
||||||
return when (status) {
|
return when (status) {
|
||||||
is PartDownloadStatus.Canceled -> "Disconnected"
|
is PartDownloadStatus.Canceled -> Res.string.disconnected
|
||||||
PartDownloadStatus.IDLE -> "IDLE"
|
PartDownloadStatus.IDLE -> Res.string.idle
|
||||||
PartDownloadStatus.Completed -> "Completed"
|
PartDownloadStatus.Completed -> Res.string.finished
|
||||||
PartDownloadStatus.ReceivingData -> "Receiving Data"
|
PartDownloadStatus.ReceivingData -> Res.string.receiving_data
|
||||||
PartDownloadStatus.SendGet -> "Send Get"
|
PartDownloadStatus.SendGet -> Res.string.send_get
|
||||||
}
|
}.asStringSource()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -398,22 +405,26 @@ private fun SimpleCellText(text: String) {
|
|||||||
|
|
||||||
sealed class PartInfoCells : TableCell<IndexedValue<UiPart>> {
|
sealed class PartInfoCells : TableCell<IndexedValue<UiPart>> {
|
||||||
data object Number : PartInfoCells() {
|
data object Number : PartInfoCells() {
|
||||||
override val name: String = "#"
|
override val id: String = "#"
|
||||||
|
override val name: StringSource = "#".asStringSource()
|
||||||
override val size: CellSize = CellSize.Fixed(26.dp)
|
override val size: CellSize = CellSize.Fixed(26.dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object Status : PartInfoCells() {
|
data object Status : PartInfoCells() {
|
||||||
override val name: String = "Status"
|
override val id: String = "Status"
|
||||||
|
override val name: StringSource = Res.string.status.asStringSource()
|
||||||
override val size: CellSize = CellSize.Resizeable(100.dp..200.dp)
|
override val size: CellSize = CellSize.Resizeable(100.dp..200.dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object Downloaded : PartInfoCells() {
|
data object Downloaded : PartInfoCells() {
|
||||||
override val name: String = "Downloaded"
|
override val id: String = "Downloaded"
|
||||||
|
override val name: StringSource = Res.string.parts_info_downloaded_size.asStringSource()
|
||||||
override val size: CellSize = CellSize.Resizeable(90.dp..200.dp)
|
override val size: CellSize = CellSize.Resizeable(90.dp..200.dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
data object Total : PartInfoCells() {
|
data object Total : PartInfoCells() {
|
||||||
override val name: String = "Total"
|
override val id: String = "Total"
|
||||||
|
override val name: StringSource = Res.string.parts_info_total_size.asStringSource()
|
||||||
override val size: CellSize = CellSize.Resizeable(90.dp..200.dp)
|
override val size: CellSize = CellSize.Resizeable(90.dp..200.dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -440,7 +451,7 @@ fun RenderPropertyItem(propertyItem: SingleDownloadPagePropertyItem) {
|
|||||||
) {
|
) {
|
||||||
WithContentAlpha(0.75f) {
|
WithContentAlpha(0.75f) {
|
||||||
Text(
|
Text(
|
||||||
text = "$title:",
|
text = "${title.rememberString()}:",
|
||||||
modifier = Modifier.weight(0.3f),
|
modifier = Modifier.weight(0.3f),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
fontSize = myTextSizes.base
|
fontSize = myTextSizes.base
|
||||||
@ -448,7 +459,7 @@ fun RenderPropertyItem(propertyItem: SingleDownloadPagePropertyItem) {
|
|||||||
}
|
}
|
||||||
WithContentAlpha(1f) {
|
WithContentAlpha(1f) {
|
||||||
Text(
|
Text(
|
||||||
text = "$value",
|
text = value.rememberString(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.basicMarquee()
|
.basicMarquee()
|
||||||
.weight(0.7f),
|
.weight(0.7f),
|
||||||
@ -532,7 +543,7 @@ private fun PartInfoButton(
|
|||||||
onClick = {
|
onClick = {
|
||||||
onClick(!showing)
|
onClick(!showing)
|
||||||
},
|
},
|
||||||
text = "Part Info",
|
text = myStringResource(Res.string.parts_info),
|
||||||
icon = if (showing) {
|
icon = if (showing) {
|
||||||
MyIcons.up
|
MyIcons.up
|
||||||
} else {
|
} else {
|
||||||
@ -565,7 +576,7 @@ private fun CloseButton(close: () -> Unit) {
|
|||||||
{
|
{
|
||||||
close()
|
close()
|
||||||
},
|
},
|
||||||
text = "Close"
|
text = myStringResource(Res.string.close)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -576,7 +587,7 @@ private fun OpenFileButton(open: () -> Unit) {
|
|||||||
open()
|
open()
|
||||||
},
|
},
|
||||||
icon = MyIcons.fileOpen,
|
icon = MyIcons.fileOpen,
|
||||||
text = "Open"
|
text = myStringResource(Res.string.open_file)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -587,7 +598,7 @@ private fun OpenFolderButton(open: () -> Unit) {
|
|||||||
open()
|
open()
|
||||||
},
|
},
|
||||||
icon = MyIcons.folderOpen,
|
icon = MyIcons.folderOpen,
|
||||||
text = "Folder",
|
text = myStringResource(Res.string.open_folder),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -605,11 +616,11 @@ private fun ToggleButton(
|
|||||||
val isResumeSupported = itemState.supportResume == true
|
val isResumeSupported = itemState.supportResume == true
|
||||||
val (icon, text) = when (itemState.status) {
|
val (icon, text) = when (itemState.status) {
|
||||||
is DownloadJobStatus.CanBeResumed -> {
|
is DownloadJobStatus.CanBeResumed -> {
|
||||||
MyIcons.resume to "Resume"
|
MyIcons.resume to Res.string.resume
|
||||||
}
|
}
|
||||||
|
|
||||||
is DownloadJobStatus.IsActive -> {
|
is DownloadJobStatus.IsActive -> {
|
||||||
MyIcons.pause to "Pause"
|
MyIcons.pause to Res.string.pause
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> return
|
else -> return
|
||||||
@ -629,7 +640,7 @@ private fun ToggleButton(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon = icon,
|
icon = icon,
|
||||||
text = text,
|
text = myStringResource(text),
|
||||||
color = if (isResumeSupported) {
|
color = if (isResumeSupported) {
|
||||||
LocalContentColor.current
|
LocalContentColor.current
|
||||||
} else {
|
} else {
|
||||||
@ -670,13 +681,13 @@ private fun ToggleButton(
|
|||||||
) {
|
) {
|
||||||
Text(buildAnnotatedString {
|
Text(buildAnnotatedString {
|
||||||
withStyle(SpanStyle(color = myColors.warning)) {
|
withStyle(SpanStyle(color = myColors.warning)) {
|
||||||
append("WARNING:\n")
|
append("${myStringResource(Res.string.warning)}:\n")
|
||||||
}
|
}
|
||||||
append("This download doesn't support resuming! You may have to RESTART it later in the Download List")
|
append(myStringResource(Res.string.unsupported_resume_warning))
|
||||||
})
|
})
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
ActionButton(
|
ActionButton(
|
||||||
"Stop Anyway",
|
myStringResource(Res.string.stop_anyway),
|
||||||
onClick = {
|
onClick = {
|
||||||
closePopup()
|
closePopup()
|
||||||
pause()
|
pause()
|
||||||
|
@ -8,12 +8,17 @@ import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects
|
|||||||
import com.abdownloadmanager.desktop.utils.mvi.supportEffects
|
import com.abdownloadmanager.desktop.utils.mvi.supportEffects
|
||||||
import arrow.optics.copy
|
import arrow.optics.copy
|
||||||
import com.abdownloadmanager.desktop.storage.PageStatesStorage
|
import com.abdownloadmanager.desktop.storage.PageStatesStorage
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import com.abdownloadmanager.resources.*
|
||||||
import com.arkivanov.decompose.ComponentContext
|
import com.arkivanov.decompose.ComponentContext
|
||||||
import ir.amirab.downloader.DownloadManagerEvents
|
import ir.amirab.downloader.DownloadManagerEvents
|
||||||
import ir.amirab.downloader.downloaditem.DownloadJobStatus
|
import ir.amirab.downloader.downloaditem.DownloadJobStatus
|
||||||
import ir.amirab.downloader.utils.ExceptionUtils
|
import ir.amirab.downloader.utils.ExceptionUtils
|
||||||
import ir.amirab.downloader.DownloadManager
|
import ir.amirab.downloader.DownloadManager
|
||||||
import ir.amirab.downloader.monitor.*
|
import ir.amirab.downloader.monitor.*
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
|
import ir.amirab.util.compose.asStringSourceWithARgs
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
@ -27,8 +32,8 @@ sealed interface SingleDownloadEffects {
|
|||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class SingleDownloadPagePropertyItem(
|
data class SingleDownloadPagePropertyItem(
|
||||||
val name: String,
|
val name: StringSource,
|
||||||
val value: String,
|
val value: StringSource,
|
||||||
val valueState: ValueType = ValueType.Normal,
|
val valueState: ValueType = ValueType.Normal,
|
||||||
) {
|
) {
|
||||||
enum class ValueType { Normal, Error, Success }
|
enum class ValueType { Normal, Error, Success }
|
||||||
@ -67,9 +72,14 @@ class SingleDownloadComponent(
|
|||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
.map {
|
.map {
|
||||||
buildList {
|
buildList {
|
||||||
add(SingleDownloadPagePropertyItem("Name", it.name))
|
add(SingleDownloadPagePropertyItem(Res.string.name.asStringSource(), it.name.asStringSource()))
|
||||||
add(SingleDownloadPagePropertyItem("Status", createStatusString(it)))
|
add(SingleDownloadPagePropertyItem(Res.string.status.asStringSource(), createStatusString(it)))
|
||||||
add(SingleDownloadPagePropertyItem("Size", convertSizeToHumanReadable(it.contentLength)))
|
add(
|
||||||
|
SingleDownloadPagePropertyItem(
|
||||||
|
Res.string.size.asStringSource(),
|
||||||
|
convertSizeToHumanReadable(it.contentLength)
|
||||||
|
)
|
||||||
|
)
|
||||||
when (it) {
|
when (it) {
|
||||||
is CompletedDownloadItemState -> {
|
is CompletedDownloadItemState -> {
|
||||||
}
|
}
|
||||||
@ -77,21 +87,31 @@ class SingleDownloadComponent(
|
|||||||
is ProcessingDownloadItemState -> {
|
is ProcessingDownloadItemState -> {
|
||||||
add(
|
add(
|
||||||
SingleDownloadPagePropertyItem(
|
SingleDownloadPagePropertyItem(
|
||||||
"Downloaded",
|
Res.string.download_page_downloaded_size.asStringSource(),
|
||||||
convertBytesToHumanReadable(it.progress).orEmpty()
|
convertBytesToHumanReadable(it.progress).orEmpty().asStringSource()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
add(SingleDownloadPagePropertyItem("Speed", convertSpeedToHumanReadable(it.speed)))
|
|
||||||
add(SingleDownloadPagePropertyItem("Remaining Time", (it.remainingTime?.let { remainingTime ->
|
|
||||||
convertTimeRemainingToHumanReadable(remainingTime, TimeNames.ShortNames)
|
|
||||||
}.orEmpty())))
|
|
||||||
add(
|
add(
|
||||||
SingleDownloadPagePropertyItem(
|
SingleDownloadPagePropertyItem(
|
||||||
"Resume Support",
|
Res.string.speed.asStringSource(),
|
||||||
|
convertSpeedToHumanReadable(it.speed).asStringSource()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
add(
|
||||||
|
SingleDownloadPagePropertyItem(
|
||||||
|
Res.string.time_left.asStringSource(),
|
||||||
|
(it.remainingTime?.let { remainingTime ->
|
||||||
|
convertTimeRemainingToHumanReadable(remainingTime, TimeNames.ShortNames)
|
||||||
|
}.orEmpty()).asStringSource()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
add(
|
||||||
|
SingleDownloadPagePropertyItem(
|
||||||
|
Res.string.resume_support.asStringSource(),
|
||||||
when (it.supportResume) {
|
when (it.supportResume) {
|
||||||
true -> "Yes"
|
true -> Res.string.yes.asStringSource()
|
||||||
false -> "No"
|
false -> Res.string.no.asStringSource()
|
||||||
null -> "Unknown"
|
null -> Res.string.unknown.asStringSource()
|
||||||
},
|
},
|
||||||
when (it.supportResume) {
|
when (it.supportResume) {
|
||||||
true -> SingleDownloadPagePropertyItem.ValueType.Success
|
true -> SingleDownloadPagePropertyItem.ValueType.Success
|
||||||
@ -105,23 +125,23 @@ class SingleDownloadComponent(
|
|||||||
}
|
}
|
||||||
}.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
}.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
private fun createStatusString(it: IDownloadItemState): String {
|
private fun createStatusString(it: IDownloadItemState): StringSource {
|
||||||
|
|
||||||
return when (val status = it.statusOrFinished()) {
|
return when (val status = it.statusOrFinished()) {
|
||||||
is DownloadJobStatus.Canceled -> {
|
is DownloadJobStatus.Canceled -> {
|
||||||
if (ExceptionUtils.isNormalCancellation(status.e)) {
|
if (ExceptionUtils.isNormalCancellation(status.e)) {
|
||||||
"Paused"
|
Res.string.paused
|
||||||
} else {
|
} else {
|
||||||
"Error"
|
Res.string.error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DownloadJobStatus.Downloading -> "Downloading"
|
DownloadJobStatus.Downloading -> Res.string.downloading
|
||||||
DownloadJobStatus.Finished -> "Finished"
|
DownloadJobStatus.Finished -> Res.string.finished
|
||||||
DownloadJobStatus.IDLE -> "IDLE"
|
DownloadJobStatus.IDLE -> Res.string.idle
|
||||||
is DownloadJobStatus.PreparingFile -> "PreparingFile"
|
is DownloadJobStatus.PreparingFile -> Res.string.preparing_file
|
||||||
DownloadJobStatus.Resuming -> "Resuming"
|
DownloadJobStatus.Resuming -> Res.string.resuming
|
||||||
}
|
}.asStringSource()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openFolder() {
|
fun openFolder() {
|
||||||
@ -230,28 +250,33 @@ class SingleDownloadComponent(
|
|||||||
val settings by lazy {
|
val settings by lazy {
|
||||||
listOf(
|
listOf(
|
||||||
IntConfigurable(
|
IntConfigurable(
|
||||||
title = "Thread Count",
|
title = Res.string.download_item_settings_thread_count.asStringSource(),
|
||||||
description = "How much thread used to download this item 0 for default",
|
description = Res.string.download_item_settings_thread_count_description.asStringSource(),
|
||||||
backedBy = threadCount,
|
backedBy = threadCount,
|
||||||
describe = {
|
describe = {
|
||||||
if (it == 0) {
|
if (it == 0) {
|
||||||
"uses global setting"
|
Res.string.use_global_settings.asStringSource()
|
||||||
} else {
|
} else {
|
||||||
"$it threads"
|
Res.string.download_item_settings_thread_count_describe
|
||||||
|
.asStringSourceWithARgs(
|
||||||
|
Res.string.download_item_settings_thread_count_describe_createArgs(
|
||||||
|
count = it.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
range = 0..32,
|
range = 0..32,
|
||||||
renderMode = IntConfigurable.RenderMode.TextField,
|
renderMode = IntConfigurable.RenderMode.TextField,
|
||||||
),
|
),
|
||||||
SpeedLimitConfigurable(
|
SpeedLimitConfigurable(
|
||||||
title = "Speed limit",
|
title = Res.string.download_item_settings_speed_limit.asStringSource(),
|
||||||
description = "speed limit for this download",
|
description = Res.string.download_item_settings_speed_limit_description.asStringSource(),
|
||||||
backedBy = speedLimit,
|
backedBy = speedLimit,
|
||||||
describe = {
|
describe = {
|
||||||
if (it == 0L) {
|
if (it == 0L) {
|
||||||
"Unlimited"
|
Res.string.unlimited.asStringSource()
|
||||||
} else {
|
} else {
|
||||||
convertSpeedToHumanReadable(it)
|
convertSpeedToHumanReadable(it).asStringSource()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -9,6 +9,7 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.rememberWindowState
|
import androidx.compose.ui.window.rememberWindowState
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -61,8 +62,8 @@ fun ShowUpdaterDialog(updaterComponent: UpdateComponent) {
|
|||||||
|
|
||||||
message?.let { message ->
|
message?.let { message ->
|
||||||
ShowNotification(
|
ShowNotification(
|
||||||
title = "Updater",
|
title = "Updater".asStringSource(),
|
||||||
description = message,
|
description = message.asStringSource(),
|
||||||
type = notificationType ?: NotificationType.Info,
|
type = notificationType ?: NotificationType.Info,
|
||||||
tag = "Updater"
|
tag = "Updater"
|
||||||
)
|
)
|
||||||
|
@ -4,11 +4,13 @@ import com.abdownloadmanager.desktop.utils.*
|
|||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import arrow.optics.Lens
|
import arrow.optics.Lens
|
||||||
import arrow.optics.optics
|
import arrow.optics.optics
|
||||||
|
import ir.amirab.util.compose.localizationmanager.LanguageStorage
|
||||||
import ir.amirab.util.config.booleanKeyOf
|
import ir.amirab.util.config.booleanKeyOf
|
||||||
import ir.amirab.util.config.intKeyOf
|
import ir.amirab.util.config.intKeyOf
|
||||||
import ir.amirab.util.config.longKeyOf
|
import ir.amirab.util.config.longKeyOf
|
||||||
import ir.amirab.util.config.stringKeyOf
|
import ir.amirab.util.config.stringKeyOf
|
||||||
import ir.amirab.util.config.MapConfig
|
import ir.amirab.util.config.MapConfig
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@ -16,6 +18,7 @@ import java.io.File
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class AppSettingsModel(
|
data class AppSettingsModel(
|
||||||
val theme: String = "dark",
|
val theme: String = "dark",
|
||||||
|
val language: String = "en",
|
||||||
val mergeTopBarWithTitleBar: Boolean = false,
|
val mergeTopBarWithTitleBar: Boolean = false,
|
||||||
val threadCount: Int = 5,
|
val threadCount: Int = 5,
|
||||||
val dynamicPartCreation: Boolean = true,
|
val dynamicPartCreation: Boolean = true,
|
||||||
@ -38,6 +41,7 @@ data class AppSettingsModel(
|
|||||||
object ConfigLens : Lens<MapConfig, AppSettingsModel> {
|
object ConfigLens : Lens<MapConfig, AppSettingsModel> {
|
||||||
object Keys {
|
object Keys {
|
||||||
val theme = stringKeyOf("theme")
|
val theme = stringKeyOf("theme")
|
||||||
|
val language = stringKeyOf("language")
|
||||||
val mergeTopBarWithTitleBar = booleanKeyOf("mergeTopBarWithTitleBar")
|
val mergeTopBarWithTitleBar = booleanKeyOf("mergeTopBarWithTitleBar")
|
||||||
val threadCount = intKeyOf("threadCount")
|
val threadCount = intKeyOf("threadCount")
|
||||||
val dynamicPartCreation = booleanKeyOf("dynamicPartCreation")
|
val dynamicPartCreation = booleanKeyOf("dynamicPartCreation")
|
||||||
@ -58,6 +62,7 @@ data class AppSettingsModel(
|
|||||||
val default by lazy { AppSettingsModel.default }
|
val default by lazy { AppSettingsModel.default }
|
||||||
return AppSettingsModel(
|
return AppSettingsModel(
|
||||||
theme = source.get(Keys.theme) ?: default.theme,
|
theme = source.get(Keys.theme) ?: default.theme,
|
||||||
|
language = source.get(Keys.language) ?: default.language,
|
||||||
mergeTopBarWithTitleBar = source.get(Keys.mergeTopBarWithTitleBar) ?: default.mergeTopBarWithTitleBar,
|
mergeTopBarWithTitleBar = source.get(Keys.mergeTopBarWithTitleBar) ?: default.mergeTopBarWithTitleBar,
|
||||||
threadCount = source.get(Keys.threadCount) ?: default.threadCount,
|
threadCount = source.get(Keys.threadCount) ?: default.threadCount,
|
||||||
dynamicPartCreation = source.get(Keys.dynamicPartCreation) ?: default.dynamicPartCreation,
|
dynamicPartCreation = source.get(Keys.dynamicPartCreation) ?: default.dynamicPartCreation,
|
||||||
@ -77,6 +82,7 @@ data class AppSettingsModel(
|
|||||||
override fun set(source: MapConfig, focus: AppSettingsModel): MapConfig {
|
override fun set(source: MapConfig, focus: AppSettingsModel): MapConfig {
|
||||||
return source.apply {
|
return source.apply {
|
||||||
put(Keys.theme, focus.theme)
|
put(Keys.theme, focus.theme)
|
||||||
|
put(Keys.language, focus.language)
|
||||||
put(Keys.mergeTopBarWithTitleBar, focus.mergeTopBarWithTitleBar)
|
put(Keys.mergeTopBarWithTitleBar, focus.mergeTopBarWithTitleBar)
|
||||||
put(Keys.threadCount, focus.threadCount)
|
put(Keys.threadCount, focus.threadCount)
|
||||||
put(Keys.dynamicPartCreation, focus.dynamicPartCreation)
|
put(Keys.dynamicPartCreation, focus.dynamicPartCreation)
|
||||||
@ -96,8 +102,11 @@ data class AppSettingsModel(
|
|||||||
|
|
||||||
class AppSettingsStorage(
|
class AppSettingsStorage(
|
||||||
settings: DataStore<MapConfig>,
|
settings: DataStore<MapConfig>,
|
||||||
) : ConfigBaseSettingsByMapConfig<AppSettingsModel>(settings, AppSettingsModel.ConfigLens) {
|
) :
|
||||||
|
ConfigBaseSettingsByMapConfig<AppSettingsModel>(settings, AppSettingsModel.ConfigLens),
|
||||||
|
LanguageStorage {
|
||||||
var theme = from(AppSettingsModel.theme)
|
var theme = from(AppSettingsModel.theme)
|
||||||
|
override val selectedLanguage = from(AppSettingsModel.language)
|
||||||
var mergeTopBarWithTitleBar = from(AppSettingsModel.mergeTopBarWithTitleBar)
|
var mergeTopBarWithTitleBar = from(AppSettingsModel.mergeTopBarWithTitleBar)
|
||||||
val threadCount = from(AppSettingsModel.threadCount)
|
val threadCount = from(AppSettingsModel.threadCount)
|
||||||
val dynamicPartCreation = from(AppSettingsModel.dynamicPartCreation)
|
val dynamicPartCreation = from(AppSettingsModel.dynamicPartCreation)
|
||||||
|
@ -24,12 +24,16 @@ import ir.amirab.util.compose.action.buildMenu
|
|||||||
import com.abdownloadmanager.desktop.utils.isInDebugMode
|
import com.abdownloadmanager.desktop.utils.isInDebugMode
|
||||||
import com.abdownloadmanager.desktop.utils.mvi.HandleEffects
|
import com.abdownloadmanager.desktop.utils.mvi.HandleEffects
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.window.*
|
import androidx.compose.ui.window.*
|
||||||
import com.abdownloadmanager.desktop.pages.batchdownload.BatchDownloadWindow
|
import com.abdownloadmanager.desktop.pages.batchdownload.BatchDownloadWindow
|
||||||
import com.abdownloadmanager.desktop.pages.category.ShowCategoryDialogs
|
import com.abdownloadmanager.desktop.pages.category.ShowCategoryDialogs
|
||||||
import com.abdownloadmanager.desktop.pages.home.HomeWindow
|
import com.abdownloadmanager.desktop.pages.home.HomeWindow
|
||||||
import com.abdownloadmanager.desktop.pages.settings.ThemeManager
|
import com.abdownloadmanager.desktop.pages.settings.ThemeManager
|
||||||
|
import com.abdownloadmanager.desktop.ui.widget.ProvideLanguageManager
|
||||||
import com.abdownloadmanager.utils.compose.ProvideDebugInfo
|
import com.abdownloadmanager.utils.compose.ProvideDebugInfo
|
||||||
|
import ir.amirab.util.compose.localizationmanager.LanguageManager
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
@ -45,47 +49,51 @@ object Ui : KoinComponent {
|
|||||||
) {
|
) {
|
||||||
val appComponent: AppComponent = get()
|
val appComponent: AppComponent = get()
|
||||||
val themeManager: ThemeManager = get()
|
val themeManager: ThemeManager = get()
|
||||||
|
val languageManager: LanguageManager = get()
|
||||||
themeManager.boot()
|
themeManager.boot()
|
||||||
|
languageManager.boot()
|
||||||
if (!appArguments.startSilent) {
|
if (!appArguments.startSilent) {
|
||||||
appComponent.openHome()
|
appComponent.openHome()
|
||||||
}
|
}
|
||||||
application {
|
application {
|
||||||
val theme by themeManager.currentThemeColor.collectAsState()
|
val theme by themeManager.currentThemeColor.collectAsState()
|
||||||
ProvideDebugInfo(AppInfo.isInDebugMode()) {
|
ProvideDebugInfo(AppInfo.isInDebugMode()) {
|
||||||
ProvideNotificationManager {
|
ProvideLanguageManager(languageManager) {
|
||||||
ABDownloaderTheme(
|
ProvideNotificationManager {
|
||||||
myColors = theme,
|
ABDownloaderTheme(
|
||||||
|
myColors = theme,
|
||||||
// uiScale = appComponent.uiScale.collectAsState().value
|
// uiScale = appComponent.uiScale.collectAsState().value
|
||||||
) {
|
) {
|
||||||
ProvideGlobalExceptionHandler(globalAppExceptionHandler) {
|
ProvideGlobalExceptionHandler(globalAppExceptionHandler) {
|
||||||
val trayState = rememberTrayState()
|
val trayState = rememberTrayState()
|
||||||
HandleEffectsForApp(appComponent)
|
HandleEffectsForApp(appComponent)
|
||||||
SystemTray(appComponent, trayState)
|
SystemTray(appComponent, trayState)
|
||||||
val showHomeSlot = appComponent.showHomeSlot.collectAsState().value
|
val showHomeSlot = appComponent.showHomeSlot.collectAsState().value
|
||||||
showHomeSlot.child?.instance?.let {
|
showHomeSlot.child?.instance?.let {
|
||||||
HomeWindow(it,appComponent::closeHome)
|
HomeWindow(it, appComponent::closeHome)
|
||||||
|
}
|
||||||
|
val showSettingSlot = appComponent.showSettingSlot.collectAsState().value
|
||||||
|
showSettingSlot.child?.instance?.let {
|
||||||
|
SettingWindow(it, appComponent::closeSettings)
|
||||||
|
}
|
||||||
|
val showQueuesSlot = appComponent.showQueuesSlot.collectAsState().value
|
||||||
|
showQueuesSlot.child?.instance?.let {
|
||||||
|
QueuesWindow(it)
|
||||||
|
}
|
||||||
|
val batchDownloadSlot = appComponent.batchDownloadSlot.collectAsState().value
|
||||||
|
batchDownloadSlot.child?.instance?.let {
|
||||||
|
BatchDownloadWindow(it)
|
||||||
|
}
|
||||||
|
ShowAddDownloadDialogs(appComponent)
|
||||||
|
ShowDownloadDialogs(appComponent)
|
||||||
|
ShowCategoryDialogs(appComponent)
|
||||||
|
//TODO Enable Updater
|
||||||
|
//ShowUpdaterDialog(appComponent.updater)
|
||||||
|
ShowAboutDialog(appComponent)
|
||||||
|
NewQueueDialog(appComponent)
|
||||||
|
ShowMessageDialogs(appComponent)
|
||||||
|
ShowOpenSourceLibraries(appComponent)
|
||||||
}
|
}
|
||||||
val showSettingSlot = appComponent.showSettingSlot.collectAsState().value
|
|
||||||
showSettingSlot.child?.instance?.let {
|
|
||||||
SettingWindow(it, appComponent::closeSettings)
|
|
||||||
}
|
|
||||||
val showQueuesSlot = appComponent.showQueuesSlot.collectAsState().value
|
|
||||||
showQueuesSlot.child?.instance?.let {
|
|
||||||
QueuesWindow(it)
|
|
||||||
}
|
|
||||||
val batchDownloadSlot = appComponent.batchDownloadSlot.collectAsState().value
|
|
||||||
batchDownloadSlot.child?.instance?.let {
|
|
||||||
BatchDownloadWindow(it)
|
|
||||||
}
|
|
||||||
ShowAddDownloadDialogs(appComponent)
|
|
||||||
ShowDownloadDialogs(appComponent)
|
|
||||||
ShowCategoryDialogs(appComponent)
|
|
||||||
//TODO Enable Updater
|
|
||||||
//ShowUpdaterDialog(appComponent.updater)
|
|
||||||
ShowAboutDialog(appComponent)
|
|
||||||
NewQueueDialog(appComponent)
|
|
||||||
ShowMessageDialogs(appComponent)
|
|
||||||
ShowOpenSourceLibraries(appComponent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import androidx.compose.ui.graphics.RectangleShape
|
|||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.graphics.takeOrElse
|
import androidx.compose.ui.graphics.takeOrElse
|
||||||
import androidx.compose.ui.input.key.KeyEvent
|
import androidx.compose.ui.input.key.KeyEvent
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.platform.LocalWindowInfo
|
import androidx.compose.ui.platform.LocalWindowInfo
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@ -65,16 +66,19 @@ private fun FrameWindowScope.CustomWindowFrame(
|
|||||||
}
|
}
|
||||||
.background(background)
|
.background(background)
|
||||||
) {
|
) {
|
||||||
SnapDraggableToolbar(
|
WithTitleBarDirection {
|
||||||
title = title,
|
SnapDraggableToolbar(
|
||||||
windowIcon = windowIcon,
|
title = title,
|
||||||
titlePosition = titlePosition,
|
windowIcon = windowIcon,
|
||||||
start = start,
|
titlePosition = titlePosition,
|
||||||
end = end,
|
start = start,
|
||||||
onRequestMinimize = onRequestMinimize,
|
end = end,
|
||||||
onRequestClose = onRequestClose,
|
onRequestMinimize = onRequestMinimize,
|
||||||
onRequestToggleMaximize = onRequestToggleMaximize
|
onRequestClose = onRequestClose,
|
||||||
)
|
onRequestToggleMaximize = onRequestToggleMaximize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
package com.abdownloadmanager.desktop.ui.customwindow
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
|
||||||
|
val LocalTitleBarDirection = staticCompositionLocalOf<LayoutDirection> {
|
||||||
|
error("TitleBarDirection not provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WithTitleBarDirection(
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalLayoutDirection provides LocalTitleBarDirection.current
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,7 @@ import androidx.compose.ui.unit.sp
|
|||||||
import androidx.compose.ui.window.Popup
|
import androidx.compose.ui.window.Popup
|
||||||
import androidx.compose.ui.window.PopupProperties
|
import androidx.compose.ui.window.PopupProperties
|
||||||
import androidx.compose.ui.window.rememberPopupPositionProviderAtPosition
|
import androidx.compose.ui.window.rememberPopupPositionProviderAtPosition
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
|
|
||||||
/*
|
/*
|
||||||
fun MyColors.asMaterial2Colors(): Colors {
|
fun MyColors.asMaterial2Colors(): Colors {
|
||||||
@ -135,7 +136,7 @@ private class MyContextMenuRepresentation : ContextMenuRepresentation {
|
|||||||
val menuItems = remember(contextItems) {
|
val menuItems = remember(contextItems) {
|
||||||
buildMenu {
|
buildMenu {
|
||||||
contextItems.map {
|
contextItems.map {
|
||||||
item(title = it.label, onClick = {
|
item(title = it.label.asStringSource(), onClick = {
|
||||||
it.onClick()
|
it.onClick()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AddUrlButton(
|
fun AddUrlButton(
|
||||||
@ -29,21 +31,22 @@ fun AddUrlButton(
|
|||||||
.background(myColors.surface)
|
.background(myColors.surface)
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
.height(32.dp)
|
.height(32.dp)
|
||||||
.width(120.dp)
|
// .width(120.dp)
|
||||||
.padding(horizontal = 8.dp)
|
.padding(horizontal = 8.dp),
|
||||||
,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|
||||||
) {
|
) {
|
||||||
WithContentAlpha(1f) {
|
WithContentAlpha(1f) {
|
||||||
MyIcon(addUrlIcon, null, Modifier.size(16.dp))
|
MyIcon(addUrlIcon, null, Modifier.size(16.dp))
|
||||||
Spacer(Modifier.width(10.dp))
|
Spacer(Modifier.width(10.dp))
|
||||||
Text("Add URL",
|
Text(
|
||||||
Modifier.weight(1f),
|
myStringResource(Res.string.new_download),
|
||||||
|
Modifier,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
fontSize = myTextSizes.sm,
|
fontSize = myTextSizes.sm,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.clip(RoundedCornerShape(6.dp))
|
.clip(RoundedCornerShape(6.dp))
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
package com.abdownloadmanager.desktop.ui.widget
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import com.abdownloadmanager.desktop.ui.customwindow.LocalTitleBarDirection
|
||||||
|
import ir.amirab.util.compose.localizationmanager.LanguageManager
|
||||||
|
import ir.amirab.util.compose.localizationmanager.LocalLanguageManager
|
||||||
|
import ir.amirab.util.compose.localizationmanager.LocaleLanguageDirection
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ProvideLanguageManager(
|
||||||
|
languageManager: LanguageManager,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val isRtl = languageManager.isRtl.collectAsState().value
|
||||||
|
val languageDirection = if (isRtl) {
|
||||||
|
LayoutDirection.Rtl
|
||||||
|
} else {
|
||||||
|
LayoutDirection.Ltr
|
||||||
|
}
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalLanguageManager provides languageManager,
|
||||||
|
LocalLayoutDirection provides languageDirection,
|
||||||
|
LocalTitleBarDirection provides LayoutDirection.Ltr,
|
||||||
|
LocaleLanguageDirection provides languageDirection
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.rememberWindowState
|
import androidx.compose.ui.window.rememberWindowState
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@ -32,8 +33,8 @@ sealed class MessageDialogType {
|
|||||||
|
|
||||||
data class MessageDialogModel(
|
data class MessageDialogModel(
|
||||||
val id: String = UUID.randomUUID().toString(),
|
val id: String = UUID.randomUUID().toString(),
|
||||||
val title: String,
|
val title: StringSource,
|
||||||
val description: String,
|
val description: StringSource,
|
||||||
val type: MessageDialogType = MessageDialogType.Info,
|
val type: MessageDialogType = MessageDialogType.Info,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -94,13 +95,13 @@ fun MessageDialog(
|
|||||||
)
|
)
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
msgContent.title,
|
msgContent.title.rememberString(),
|
||||||
fontSize = myTextSizes.xl,
|
fontSize = myTextSizes.xl,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
msgContent.description,
|
msgContent.description.rememberString(),
|
||||||
fontSize = myTextSizes.base,
|
fontSize = myTextSizes.base,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
|
@ -21,6 +21,8 @@ import androidx.compose.ui.draw.shadow
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
private val LocalNotification = compositionLocalOf<NotificationManager> {
|
private val LocalNotification = compositionLocalOf<NotificationManager> {
|
||||||
@ -43,13 +45,13 @@ sealed interface NotificationType {
|
|||||||
@Stable
|
@Stable
|
||||||
class NotificationModel(
|
class NotificationModel(
|
||||||
val tag: Any,
|
val tag: Any,
|
||||||
initialTitle: String = "",
|
initialTitle: StringSource = "".asStringSource(),
|
||||||
initialDescription: String = "",
|
initialDescription: StringSource = "".asStringSource(),
|
||||||
initialNotificationType: NotificationType = NotificationType.Info
|
initialNotificationType: NotificationType = NotificationType.Info,
|
||||||
) {
|
) {
|
||||||
var notificationType: NotificationType by mutableStateOf(initialNotificationType)
|
var notificationType: NotificationType by mutableStateOf(initialNotificationType)
|
||||||
var title: String by mutableStateOf(initialTitle)
|
var title: StringSource by mutableStateOf(initialTitle)
|
||||||
var description: String by mutableStateOf(initialDescription)
|
var description: StringSource by mutableStateOf(initialDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -66,10 +68,10 @@ fun ProvideNotificationManager(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ShowNotification(
|
fun ShowNotification(
|
||||||
title: String,
|
title: StringSource,
|
||||||
description: String,
|
description: StringSource,
|
||||||
type:NotificationType,
|
type: NotificationType,
|
||||||
tag: Any = currentCompositeKeyHash
|
tag: Any = currentCompositeKeyHash,
|
||||||
) {
|
) {
|
||||||
val notification = remember(tag) {
|
val notification = remember(tag) {
|
||||||
NotificationModel(
|
NotificationModel(
|
||||||
@ -156,7 +158,7 @@ private fun RenderNotification(
|
|||||||
private fun NotificationDescription(notificationModel: NotificationModel) {
|
private fun NotificationDescription(notificationModel: NotificationModel) {
|
||||||
WithContentAlpha(0.75f) {
|
WithContentAlpha(0.75f) {
|
||||||
Text(
|
Text(
|
||||||
text = notificationModel.description,
|
text = notificationModel.description.rememberString(),
|
||||||
fontSize = myTextSizes.base
|
fontSize = myTextSizes.base
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -166,7 +168,7 @@ private fun NotificationDescription(notificationModel: NotificationModel) {
|
|||||||
private fun NotificationTitle(notificationModel: NotificationModel) {
|
private fun NotificationTitle(notificationModel: NotificationModel) {
|
||||||
WithContentAlpha(1f) {
|
WithContentAlpha(1f) {
|
||||||
Text(
|
Text(
|
||||||
text = notificationModel.title,
|
text = notificationModel.title.rememberString(),
|
||||||
fontSize = myTextSizes.base,
|
fontSize = myTextSizes.base,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
@ -247,10 +249,10 @@ class NotificationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun showNotification(
|
suspend fun showNotification(
|
||||||
title: String,
|
title: StringSource,
|
||||||
description: String,
|
description: StringSource,
|
||||||
delay: Long = -1,
|
delay: Long = -1,
|
||||||
tag: Double = Math.random()
|
tag: Double = Math.random(),
|
||||||
) {
|
) {
|
||||||
val notification = NotificationModel(
|
val notification = NotificationModel(
|
||||||
tag = tag,
|
tag = tag,
|
||||||
|
@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -32,7 +33,7 @@ fun MyTab(
|
|||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
icon: IconSource,
|
icon: IconSource,
|
||||||
title: String,
|
title: StringSource,
|
||||||
selectionBackground: Color = myColors.background,
|
selectionBackground: Color = myColors.background,
|
||||||
) {
|
) {
|
||||||
WithContentAlpha(
|
WithContentAlpha(
|
||||||
@ -56,7 +57,7 @@ fun MyTab(
|
|||||||
) {
|
) {
|
||||||
MyIcon(icon, null, Modifier.size(16.dp))
|
MyIcon(icon, null, Modifier.size(16.dp))
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(4.dp))
|
||||||
Text(title, maxLines = 1, fontSize = myTextSizes.base)
|
Text(title.rememberString(), maxLines = 1, fontSize = myTextSizes.base)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,8 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Popup
|
import androidx.compose.ui.window.Popup
|
||||||
import androidx.compose.ui.window.rememberCursorPositionProvider
|
import androidx.compose.ui.window.rememberCursorPositionProvider
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
|
||||||
val LocalCellPadding = compositionLocalOf {
|
val LocalCellPadding = compositionLocalOf {
|
||||||
@ -241,13 +243,13 @@ private fun <T, C : TableCell<T>> ShowColumnConfigMenu(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"Customize Columns",
|
myStringResource(Res.string.customize_columns),
|
||||||
fontSize = myTextSizes.base
|
fontSize = myTextSizes.base
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
IconActionButton(
|
IconActionButton(
|
||||||
MyIcons.undo,
|
MyIcons.undo,
|
||||||
"Reset",
|
myStringResource(Res.string.reset),
|
||||||
onClick = {
|
onClick = {
|
||||||
tableState.reset()
|
tableState.reset()
|
||||||
}
|
}
|
||||||
@ -329,7 +331,7 @@ private fun <T, Cell : TableCell<T>> CellConfigItem(
|
|||||||
)
|
)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
cell.name,
|
cell.name.rememberString(),
|
||||||
Modifier
|
Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.ifThen(!isVisible || isForceVisible) {
|
.ifThen(!isVisible || isForceVisible) {
|
||||||
|
@ -23,6 +23,7 @@ import androidx.compose.ui.input.pointer.pointerHoverIcon
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
import ir.amirab.util.flow.mapStateFlow
|
import ir.amirab.util.flow.mapStateFlow
|
||||||
import ir.amirab.util.swapped
|
import ir.amirab.util.swapped
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@ -54,7 +55,8 @@ sealed interface CellSize {
|
|||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
interface TableCell<Item> {
|
interface TableCell<Item> {
|
||||||
val name: String
|
val id: String
|
||||||
|
val name: StringSource
|
||||||
val size: CellSize
|
val size: CellSize
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,7 +88,7 @@ fun DefaultRenderHeader(cell: TableCell<*>) {
|
|||||||
cell.drawHeader()
|
cell.drawHeader()
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
cell.name,
|
cell.name.rememberString(),
|
||||||
Modifier.fillMaxWidth(),
|
Modifier.fillMaxWidth(),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
@ -318,7 +320,7 @@ class TableState<Item, Cell : TableCell<Item>>(
|
|||||||
|
|
||||||
fun save(): SerializableTableState {
|
fun save(): SerializableTableState {
|
||||||
val sizes = customSizes.value.mapKeys {
|
val sizes = customSizes.value.mapKeys {
|
||||||
it.key.name
|
it.key.id
|
||||||
}.mapValues {
|
}.mapValues {
|
||||||
it.value.value
|
it.value.value
|
||||||
}
|
}
|
||||||
@ -326,37 +328,37 @@ class TableState<Item, Cell : TableCell<Item>>(
|
|||||||
return SerializableTableState(
|
return SerializableTableState(
|
||||||
sizes = sizes,
|
sizes = sizes,
|
||||||
sortBy = sortBy?.let {
|
sortBy = sortBy?.let {
|
||||||
SortBy(sortBy.cell.name, sortBy.isUp())
|
SortBy(sortBy.cell.id, sortBy.isUp())
|
||||||
},
|
},
|
||||||
order = order.value.map { it.name },
|
order = order.value.map { it.id },
|
||||||
visibleCells = visibleCells.value.map { it.name }
|
visibleCells = visibleCells.value.map { it.id }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun load(s: SerializableTableState) {
|
fun load(s: SerializableTableState) {
|
||||||
setCustomSizes {
|
setCustomSizes {
|
||||||
val cellsThatHaveCustomWidth = findCellByName(s.sizes.keys)
|
val cellsThatHaveCustomWidth = findCellById(s.sizes.keys)
|
||||||
cellsThatHaveCustomWidth.associateWith { s.sizes[it.name]!!.dp }
|
cellsThatHaveCustomWidth.associateWith { s.sizes[it.id]!!.dp }
|
||||||
}
|
}
|
||||||
setOrder(findCellByName(s.order))
|
setOrder(findCellById(s.order))
|
||||||
setSortBy(
|
setSortBy(
|
||||||
s.sortBy?.let { sortBy ->
|
s.sortBy?.let { sortBy ->
|
||||||
findCellByName(sortBy.name)?.let {
|
findCellById(sortBy.name)?.let {
|
||||||
Sort(it as SortableCell<Item>, sortBy.descending)
|
Sort(it as SortableCell<Item>, sortBy.descending)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
setVisibleCells(findCellByName(s.visibleCells))
|
setVisibleCells(findCellById(s.visibleCells))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun findCellByName(name: String): Cell? {
|
private fun findCellById(name: String): Cell? {
|
||||||
return cells.find { it.name == name }
|
return cells.find { it.id == name }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findCellByName(list: Iterable<String>): List<Cell> {
|
private fun findCellById(list: Iterable<String>): List<Cell> {
|
||||||
return list.mapNotNull { name ->
|
return list.mapNotNull { name ->
|
||||||
findCellByName(name)
|
findCellById(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,9 +34,11 @@ fun SiblingDropDown(
|
|||||||
offset: DpOffset = DpOffset.Zero,
|
offset: DpOffset = DpOffset.Zero,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
val positionProvider = remember {
|
val positionProvider = rememberComponentRectPositionProvider(
|
||||||
SiblingMenuPositionProvider()
|
anchor = Alignment.TopEnd,
|
||||||
}
|
alignment = Alignment.BottomEnd,
|
||||||
|
offset = offset,
|
||||||
|
)
|
||||||
Popup(
|
Popup(
|
||||||
popupPositionProvider = positionProvider,
|
popupPositionProvider = positionProvider,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
@ -33,6 +33,7 @@ import androidx.compose.ui.text.buildAnnotatedString
|
|||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.text.withStyle
|
import androidx.compose.ui.text.withStyle
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ir.amirab.util.compose.modifiers.autoMirror
|
||||||
import javax.swing.KeyStroke
|
import javax.swing.KeyStroke
|
||||||
|
|
||||||
enum class MenuDisabledItemBehavior {
|
enum class MenuDisabledItemBehavior {
|
||||||
@ -76,7 +77,7 @@ fun MenuBar(
|
|||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
.wrapContentHeight(Alignment.CenterVertically)
|
.wrapContentHeight(Alignment.CenterVertically)
|
||||||
) {
|
) {
|
||||||
val text = subMenu.title.collectAsState().value
|
val text = subMenu.title.collectAsState().value.rememberString()
|
||||||
val (firstChar, leadingText) = remember(text) {
|
val (firstChar, leadingText) = remember(text) {
|
||||||
when (text.length) {
|
when (text.length) {
|
||||||
0 -> "" to ""
|
0 -> "" to ""
|
||||||
@ -248,7 +249,7 @@ private fun ReactableItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
title,
|
title.rememberString(),
|
||||||
Modifier.weight(1f),
|
Modifier.weight(1f),
|
||||||
fontSize = myTextSizes.base,
|
fontSize = myTextSizes.base,
|
||||||
softWrap = false,
|
softWrap = false,
|
||||||
@ -332,7 +333,9 @@ fun RenderSubMenuItem(
|
|||||||
MyIcon(
|
MyIcon(
|
||||||
MyIcons.next,
|
MyIcons.next,
|
||||||
null,
|
null,
|
||||||
Modifier.size(16.dp),
|
Modifier
|
||||||
|
.size(16.dp)
|
||||||
|
.autoMirror(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
if (openedItem == menuItem) {
|
if (openedItem == menuItem) {
|
||||||
|
@ -47,7 +47,7 @@ fun ShowOptions(
|
|||||||
val itemPadding = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)
|
val itemPadding = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||||
val title by menu.title.collectAsState()
|
val title by menu.title.collectAsState()
|
||||||
Text(
|
Text(
|
||||||
title,
|
title.rememberString(),
|
||||||
Modifier
|
Modifier
|
||||||
.then(itemPadding)
|
.then(itemPadding)
|
||||||
.basicMarquee(
|
.basicMarquee(
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package com.abdownloadmanager.desktop.utils
|
package com.abdownloadmanager.desktop.utils
|
||||||
|
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
import ir.amirab.downloader.utils.ByteConverter
|
import ir.amirab.downloader.utils.ByteConverter
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
|
|
||||||
data class HumanReadableSize(
|
data class HumanReadableSize(
|
||||||
val value:Double,
|
val value:Double,
|
||||||
@ -52,8 +55,9 @@ fun convertBytesToHumanReadable(size: Long): String? {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun convertSizeToHumanReadable(size: Long): String {
|
fun convertSizeToHumanReadable(size: Long): StringSource {
|
||||||
return convertBytesToHumanReadable(size) ?: "unknown"
|
return convertBytesToHumanReadable(size)?.asStringSource()
|
||||||
|
?: Res.string.unknown.asStringSource()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun convertSpeedToHumanReadable(size: Long, perUnit: String="s"): String {
|
fun convertSpeedToHumanReadable(size: Long, perUnit: String="s"): String {
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
package com.abdownloadmanager.desktop.utils
|
package com.abdownloadmanager.desktop.utils
|
||||||
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import com.abdownloadmanager.resources.Res
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
|
import ir.amirab.util.compose.asStringSource
|
||||||
|
import ir.amirab.util.compose.asStringSourceWithARgs
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.datetime.DateTimePeriod
|
import kotlinx.datetime.DateTimePeriod
|
||||||
@ -81,10 +85,11 @@ fun prettifyRelativeTime(
|
|||||||
count = count,
|
count = count,
|
||||||
names = names,
|
names = names,
|
||||||
)
|
)
|
||||||
val leftOrAgo =
|
return (if (isLater) {
|
||||||
if (isLater) names.left
|
names.left(relativeTime)
|
||||||
else names.ago
|
} else {
|
||||||
return "$relativeTime $leftOrAgo"
|
names.ago(relativeTime)
|
||||||
|
}).getString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun relativeTime(
|
private fun relativeTime(
|
||||||
@ -102,8 +107,7 @@ private fun relativeTime(
|
|||||||
val relativeTime = buildString {
|
val relativeTime = buildString {
|
||||||
if (years > 0) {
|
if (years > 0) {
|
||||||
used++
|
used++
|
||||||
append(years)
|
append(names.years(years).getString())
|
||||||
append(" ${names.years}")
|
|
||||||
}
|
}
|
||||||
if (used == count) return@buildString
|
if (used == count) return@buildString
|
||||||
if (months > 0) {
|
if (months > 0) {
|
||||||
@ -111,8 +115,7 @@ private fun relativeTime(
|
|||||||
append(" ")
|
append(" ")
|
||||||
}
|
}
|
||||||
used++
|
used++
|
||||||
append(months)
|
append(names.months(months).getString())
|
||||||
append(" ${names.months}")
|
|
||||||
}
|
}
|
||||||
if (used == count) return@buildString
|
if (used == count) return@buildString
|
||||||
if (days > 0) {
|
if (days > 0) {
|
||||||
@ -120,8 +123,7 @@ private fun relativeTime(
|
|||||||
append(" ")
|
append(" ")
|
||||||
}
|
}
|
||||||
used++
|
used++
|
||||||
append(days)
|
append(names.days(days).getString())
|
||||||
append(" ${names.days}")
|
|
||||||
}
|
}
|
||||||
if (used == count) return@buildString
|
if (used == count) return@buildString
|
||||||
if (hours > 0) {
|
if (hours > 0) {
|
||||||
@ -129,8 +131,7 @@ private fun relativeTime(
|
|||||||
append(" ")
|
append(" ")
|
||||||
}
|
}
|
||||||
used++
|
used++
|
||||||
append(hours)
|
append(names.hours(hours).getString())
|
||||||
append(" ${names.hours}")
|
|
||||||
}
|
}
|
||||||
if (used == count) return@buildString
|
if (used == count) return@buildString
|
||||||
if (minutes > 0) {
|
if (minutes > 0) {
|
||||||
@ -138,8 +139,7 @@ private fun relativeTime(
|
|||||||
append(" ")
|
append(" ")
|
||||||
}
|
}
|
||||||
used++
|
used++
|
||||||
append(minutes)
|
append(names.minutes(minutes).getString())
|
||||||
append(" ${names.minutes}")
|
|
||||||
}
|
}
|
||||||
if (used == count) return@buildString
|
if (used == count) return@buildString
|
||||||
if (seconds > 0) {
|
if (seconds > 0) {
|
||||||
@ -147,12 +147,11 @@ private fun relativeTime(
|
|||||||
append(" ")
|
append(" ")
|
||||||
}
|
}
|
||||||
used++
|
used++
|
||||||
append(seconds)
|
append(names.seconds(seconds).getString())
|
||||||
append(" ${names.seconds}")
|
|
||||||
}
|
}
|
||||||
if (used == count) return@buildString
|
if (used == count) return@buildString
|
||||||
if (used == 0) {
|
if (used == 0) {
|
||||||
append("0 ${names.seconds}")
|
append(names.seconds(0).getString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return relativeTime
|
return relativeTime
|
||||||
@ -201,37 +200,82 @@ fun convertTimeRemainingToHumanReadable(
|
|||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
interface TimeNames {
|
interface TimeNames {
|
||||||
val years: String
|
fun years(years: Int): StringSource
|
||||||
val months: String
|
fun months(months: Int): StringSource
|
||||||
val days: String
|
fun days(days: Int): StringSource
|
||||||
val hours: String
|
fun hours(hours: Int): StringSource
|
||||||
val minutes: String
|
fun minutes(minutes: Int): StringSource
|
||||||
val seconds: String
|
fun seconds(seconds: Int): StringSource
|
||||||
val ago: String
|
fun ago(time: String): StringSource
|
||||||
val left: String
|
fun left(time: String): StringSource
|
||||||
|
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
object SimpleNames : TimeNames {
|
object SimpleNames : TimeNames {
|
||||||
override val years: String = "years"
|
override fun years(years: Int): StringSource = Res.string.relative_time_long_years.asStringSourceWithARgs(
|
||||||
override val months: String = "months"
|
Res.string.relative_time_long_years_createArgs(years = years.toString())
|
||||||
override val days: String = "days"
|
)
|
||||||
override val hours: String = "hours"
|
|
||||||
override val minutes: String = "minutes"
|
override fun months(months: Int): StringSource = Res.string.relative_time_long_months.asStringSourceWithARgs(
|
||||||
override val seconds: String = "seconds"
|
Res.string.relative_time_long_months_createArgs(months = months.toString())
|
||||||
override val left: String = "left"
|
)
|
||||||
override val ago: String = "ago"
|
|
||||||
|
override fun days(days: Int): StringSource =
|
||||||
|
Res.string.relative_time_long_days.asStringSourceWithARgs(Res.string.relative_time_long_days_createArgs(days = days.toString()))
|
||||||
|
|
||||||
|
override fun hours(hours: Int): StringSource = Res.string.relative_time_long_hours.asStringSourceWithARgs(
|
||||||
|
Res.string.relative_time_long_hours_createArgs(hours = hours.toString())
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun minutes(minutes: Int): StringSource =
|
||||||
|
Res.string.relative_time_long_minutes.asStringSourceWithARgs(
|
||||||
|
Res.string.relative_time_long_minutes_createArgs(minutes = minutes.toString())
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun seconds(seconds: Int): StringSource =
|
||||||
|
Res.string.relative_time_long_seconds.asStringSourceWithARgs(
|
||||||
|
Res.string.relative_time_long_seconds_createArgs(seconds = seconds.toString())
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun left(time: String): StringSource =
|
||||||
|
Res.string.relative_time_left.asStringSourceWithARgs(Res.string.relative_time_left_createArgs(time = time))
|
||||||
|
|
||||||
|
override fun ago(time: String): StringSource =
|
||||||
|
Res.string.relative_time_ago.asStringSourceWithARgs(Res.string.relative_time_ago_createArgs(time = time))
|
||||||
}
|
}
|
||||||
|
|
||||||
object ShortNames : TimeNames {
|
object ShortNames : TimeNames {
|
||||||
override val years: String = "yr"
|
override fun years(years: Int): StringSource = Res.string.relative_time_short_years.asStringSourceWithARgs(
|
||||||
override val months: String = "mn"
|
Res.string.relative_time_short_years_createArgs(years = years.toString())
|
||||||
override val days: String = "d"
|
)
|
||||||
override val hours: String = "hr"
|
|
||||||
override val minutes: String = "m"
|
override fun months(months: Int): StringSource = Res.string.relative_time_short_months.asStringSourceWithARgs(
|
||||||
override val seconds: String = "s"
|
Res.string.relative_time_short_months_createArgs(months = months.toString())
|
||||||
override val left: String = "left"
|
)
|
||||||
override val ago: String = "ago"
|
|
||||||
|
override fun days(days: Int): StringSource = Res.string.relative_time_short_days.asStringSourceWithARgs(
|
||||||
|
Res.string.relative_time_short_days_createArgs(days = days.toString())
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun hours(hours: Int): StringSource = Res.string.relative_time_short_hours.asStringSourceWithARgs(
|
||||||
|
Res.string.relative_time_short_hours_createArgs(hours = hours.toString())
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun minutes(minutes: Int): StringSource =
|
||||||
|
Res.string.relative_time_short_minutes.asStringSourceWithARgs(
|
||||||
|
Res.string.relative_time_short_minutes_createArgs(minutes = minutes.toString())
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun seconds(seconds: Int): StringSource =
|
||||||
|
Res.string.relative_time_short_seconds.asStringSourceWithARgs(
|
||||||
|
Res.string.relative_time_short_seconds_createArgs(seconds = seconds.toString())
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun left(time: String): StringSource =
|
||||||
|
Res.string.relative_time_left.asStringSourceWithARgs(Res.string.relative_time_left_createArgs(time = time))
|
||||||
|
|
||||||
|
override fun ago(time: String): StringSource =
|
||||||
|
Res.string.relative_time_ago.asStringSourceWithARgs(Res.string.relative_time_ago_createArgs(time = time))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package ir.amirab.downloader.utils
|
package ir.amirab.downloader.utils
|
||||||
|
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
|
import java.text.DecimalFormatSymbols
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
object ByteConverter {
|
object ByteConverter {
|
||||||
const val BYTES = 1L
|
const val BYTES = 1L
|
||||||
@ -8,7 +10,7 @@ object ByteConverter {
|
|||||||
const val M_BYTES = K_BYTES * 1024L
|
const val M_BYTES = K_BYTES * 1024L
|
||||||
const val G_BYTES = M_BYTES * 1024L
|
const val G_BYTES = M_BYTES * 1024L
|
||||||
const val T_BYTES = G_BYTES * 1024L
|
const val T_BYTES = G_BYTES * 1024L
|
||||||
private val format = DecimalFormat("#.##")
|
private val format = DecimalFormat("#.##", DecimalFormatSymbols(Locale.US))
|
||||||
fun byteTo(value: Long, unit: Long): Double {
|
fun byteTo(value: Long, unit: Long): Double {
|
||||||
return (value / unit.toDouble())
|
return (value / unit.toDouble())
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,8 @@ include("integration:server")
|
|||||||
include("shared:utils")
|
include("shared:utils")
|
||||||
include("shared:app-utils")
|
include("shared:app-utils")
|
||||||
include("shared:compose-utils")
|
include("shared:compose-utils")
|
||||||
|
include("shared:resources")
|
||||||
|
include("shared:resources:contracts")
|
||||||
include("shared:config")
|
include("shared:config")
|
||||||
include("shared:updater")
|
include("shared:updater")
|
||||||
include("shared:auto-start")
|
include("shared:auto-start")
|
||||||
|
@ -6,5 +6,7 @@ dependencies {
|
|||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
implementation(compose.ui)
|
implementation(compose.ui)
|
||||||
|
implementation(compose.components.resources)
|
||||||
implementation(project(":shared:utils"))
|
implementation(project(":shared:utils"))
|
||||||
|
api(project(":shared:resources:contracts"))
|
||||||
}
|
}
|
@ -6,12 +6,11 @@ import androidx.compose.ui.graphics.painter.Painter
|
|||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import ir.amirab.util.compose.contants.RESOURCE_PROTOCOL
|
||||||
import okio.FileSystem
|
import okio.FileSystem
|
||||||
import okio.Path.Companion.toPath
|
import okio.Path.Companion.toPath
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
private const val RESOURCE_PROTOCOL = "app-resource"
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface IconSource {
|
sealed interface IconSource {
|
||||||
val value: Any
|
val value: Any
|
||||||
|
@ -0,0 +1,141 @@
|
|||||||
|
package ir.amirab.util.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import arrow.core.combine
|
||||||
|
import ir.amirab.util.compose.localizationmanager.LanguageManager
|
||||||
|
import ir.amirab.util.compose.localizationmanager.withReplacedArgs
|
||||||
|
import ir.amirab.util.compose.resources.MyStringResource
|
||||||
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed interface StringSource {
|
||||||
|
@Composable
|
||||||
|
fun rememberString(): String
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberString(args: Map<String, String>): String
|
||||||
|
fun getString(): String
|
||||||
|
fun getString(args: Map<String, String>): String
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class FromString(
|
||||||
|
val value: String,
|
||||||
|
) : StringSource {
|
||||||
|
@Composable
|
||||||
|
override fun rememberString(): String {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun rememberString(args: Map<String, String>): String {
|
||||||
|
return remember(args) {
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
value.withReplacedArgs(args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getString(): String {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getString(args: Map<String, String>): String {
|
||||||
|
return if (args.isEmpty()) {
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
value.withReplacedArgs(args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class FromStringResource(
|
||||||
|
val value: MyStringResource,
|
||||||
|
val extraArgs: Map<String, String> = emptyMap(),
|
||||||
|
) : StringSource {
|
||||||
|
@Composable
|
||||||
|
override fun rememberString(): String {
|
||||||
|
return myStringResource(value, extraArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun rememberString(args: Map<String, String>): String {
|
||||||
|
val argList = remember(extraArgs, args) {
|
||||||
|
extraArgs.plus(args)
|
||||||
|
}
|
||||||
|
return if (argList.isEmpty()) {
|
||||||
|
myStringResource(value)
|
||||||
|
} else {
|
||||||
|
myStringResource(value, argList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLanguageManager(): LanguageManager {
|
||||||
|
return LanguageManager.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getString(): String {
|
||||||
|
return getLanguageManager()
|
||||||
|
.getMessage(value.id)
|
||||||
|
.withReplacedArgs(extraArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getString(args: Map<String, String>): String {
|
||||||
|
return getLanguageManager()
|
||||||
|
.getMessage(value.id)
|
||||||
|
.withReplacedArgs(extraArgs.plus(args))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class CombinedStringSource(
|
||||||
|
val values: List<StringSource>,
|
||||||
|
val separator: String,
|
||||||
|
) : StringSource {
|
||||||
|
@Composable
|
||||||
|
override fun rememberString(): String {
|
||||||
|
return values.map {
|
||||||
|
it.rememberString()
|
||||||
|
}.joinToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun rememberString(args: Map<String, String>): String {
|
||||||
|
return values.map {
|
||||||
|
it.rememberString(args)
|
||||||
|
}.joinToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getString(): String {
|
||||||
|
return values.map {
|
||||||
|
it.getString()
|
||||||
|
}.joinToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getString(args: Map<String, String>): String {
|
||||||
|
return values.map {
|
||||||
|
it.getString(args)
|
||||||
|
}.joinToString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MyStringResource.asStringSource(): StringSource {
|
||||||
|
return StringSource.FromStringResource(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MyStringResource.asStringSourceWithARgs(args: Map<String, String>): StringSource {
|
||||||
|
return StringSource.FromStringResource(this, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.asStringSource(): StringSource {
|
||||||
|
return StringSource.FromString(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<StringSource>.combineStringSources(separator: String = ""): StringSource {
|
||||||
|
return StringSource.CombinedStringSource(this, separator)
|
||||||
|
}
|
@ -1,9 +1,10 @@
|
|||||||
package ir.amirab.util.compose.action
|
package ir.amirab.util.compose.action
|
||||||
|
|
||||||
import ir.amirab.util.compose.IconSource
|
import ir.amirab.util.compose.IconSource
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
|
|
||||||
abstract class AnAction(
|
abstract class AnAction(
|
||||||
title: String,
|
title: StringSource,
|
||||||
icon: IconSource? = null,
|
icon: IconSource? = null,
|
||||||
) : MenuItem.SingleItem(
|
) : MenuItem.SingleItem(
|
||||||
title=title,
|
title=title,
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
package ir.amirab.util.compose.action
|
package ir.amirab.util.compose.action
|
||||||
|
|
||||||
import ir.amirab.util.compose.IconSource
|
import ir.amirab.util.compose.IconSource
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
|
||||||
inline fun simpleAction(
|
inline fun simpleAction(
|
||||||
title: String,
|
title: StringSource,
|
||||||
icon: IconSource?=null,
|
icon: IconSource? = null,
|
||||||
crossinline onActionPerformed: AnAction.() -> Unit
|
crossinline onActionPerformed: AnAction.() -> Unit,
|
||||||
): AnAction {
|
): AnAction {
|
||||||
return object : AnAction(
|
return object : AnAction(
|
||||||
title = title, icon = icon,
|
title = title, icon = icon,
|
||||||
@ -15,10 +16,10 @@ inline fun simpleAction(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
inline fun simpleAction(
|
inline fun simpleAction(
|
||||||
title: String,
|
title: StringSource,
|
||||||
icon: IconSource?=null,
|
icon: IconSource? = null,
|
||||||
checkEnable:StateFlow<Boolean>,
|
checkEnable: StateFlow<Boolean>,
|
||||||
crossinline onActionPerformed: AnAction.() -> Unit
|
crossinline onActionPerformed: AnAction.() -> Unit,
|
||||||
): AnAction {
|
): AnAction {
|
||||||
return object : AnAction(
|
return object : AnAction(
|
||||||
title = title, icon = icon,
|
title = title, icon = icon,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package ir.amirab.util.compose.action
|
package ir.amirab.util.compose.action
|
||||||
|
|
||||||
import ir.amirab.util.compose.IconSource
|
import ir.amirab.util.compose.IconSource
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
|
|
||||||
@DslMarker
|
@DslMarker
|
||||||
private annotation class MenuDsl
|
private annotation class MenuDsl
|
||||||
@ -9,7 +10,7 @@ private annotation class MenuDsl
|
|||||||
class MenuScope {
|
class MenuScope {
|
||||||
private val list = mutableListOf<MenuItem>()
|
private val list = mutableListOf<MenuItem>()
|
||||||
fun item(
|
fun item(
|
||||||
title: String,
|
title: StringSource,
|
||||||
icon: IconSource? = null,
|
icon: IconSource? = null,
|
||||||
onClick: AnAction.() -> Unit,
|
onClick: AnAction.() -> Unit,
|
||||||
) {
|
) {
|
||||||
@ -18,7 +19,7 @@ class MenuScope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun subMenu(
|
fun subMenu(
|
||||||
title: String,
|
title: StringSource,
|
||||||
icon: IconSource? = null,
|
icon: IconSource? = null,
|
||||||
block: MenuScope.() -> Unit,
|
block: MenuScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
|
@ -3,6 +3,7 @@ package ir.amirab.util.compose.action
|
|||||||
import ir.amirab.util.compose.IconSource
|
import ir.amirab.util.compose.IconSource
|
||||||
import ir.amirab.util.flow.mapStateFlow
|
import ir.amirab.util.flow.mapStateFlow
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import ir.amirab.util.compose.StringSource
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
|
||||||
sealed interface MenuItem {
|
sealed interface MenuItem {
|
||||||
@ -12,12 +13,12 @@ sealed interface MenuItem {
|
|||||||
val icon: StateFlow<IconSource?>
|
val icon: StateFlow<IconSource?>
|
||||||
|
|
||||||
//compose aware property
|
//compose aware property
|
||||||
val title: StateFlow<String>
|
val title: StateFlow<StringSource>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CanBeModified {
|
interface CanBeModified {
|
||||||
fun setIcon(icon: IconSource?)
|
fun setIcon(icon: IconSource?)
|
||||||
fun setTitle(title: String)
|
fun setTitle(title: StringSource)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HasEnable {
|
interface HasEnable {
|
||||||
@ -34,7 +35,7 @@ sealed interface MenuItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract class SingleItem(
|
abstract class SingleItem(
|
||||||
title: String,
|
title: StringSource,
|
||||||
icon: IconSource? = null,
|
icon: IconSource? = null,
|
||||||
) : MenuItem,
|
) : MenuItem,
|
||||||
ClickableItem,
|
ClickableItem,
|
||||||
@ -45,11 +46,11 @@ sealed interface MenuItem {
|
|||||||
var shouldDismissOnClick: Boolean = true
|
var shouldDismissOnClick: Boolean = true
|
||||||
|
|
||||||
|
|
||||||
private val _title: MutableStateFlow<String> = MutableStateFlow(title)
|
private val _title: MutableStateFlow<StringSource> = MutableStateFlow(title)
|
||||||
private val _icon: MutableStateFlow<IconSource?> = MutableStateFlow(icon)
|
private val _icon: MutableStateFlow<IconSource?> = MutableStateFlow(icon)
|
||||||
private val _isEnabled: MutableStateFlow<Boolean> = MutableStateFlow(true)
|
private val _isEnabled: MutableStateFlow<Boolean> = MutableStateFlow(true)
|
||||||
|
|
||||||
override val title: StateFlow<String> = _title.asStateFlow()
|
override val title: StateFlow<StringSource> = _title.asStateFlow()
|
||||||
override val icon: StateFlow<IconSource?> = _icon.asStateFlow()
|
override val icon: StateFlow<IconSource?> = _icon.asStateFlow()
|
||||||
override val isEnabled: StateFlow<Boolean> = _isEnabled.asStateFlow()
|
override val isEnabled: StateFlow<Boolean> = _isEnabled.asStateFlow()
|
||||||
|
|
||||||
@ -61,7 +62,7 @@ sealed interface MenuItem {
|
|||||||
_icon.update { icon }
|
_icon.update { icon }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setTitle(title: String) {
|
override fun setTitle(title: StringSource) {
|
||||||
_title.update { title }
|
_title.update { title }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,17 +77,17 @@ sealed interface MenuItem {
|
|||||||
|
|
||||||
class SubMenu(
|
class SubMenu(
|
||||||
icon: IconSource? = null,
|
icon: IconSource? = null,
|
||||||
title: String,
|
title: StringSource,
|
||||||
items: List<MenuItem>,
|
items: List<MenuItem>,
|
||||||
) : MenuItem,
|
) : MenuItem,
|
||||||
ReadableItem,
|
ReadableItem,
|
||||||
HasEnable {
|
HasEnable {
|
||||||
private var _icon: MutableStateFlow<IconSource?> = MutableStateFlow(icon)
|
private var _icon: MutableStateFlow<IconSource?> = MutableStateFlow(icon)
|
||||||
private var _title: MutableStateFlow<String> = MutableStateFlow(title)
|
private var _title: MutableStateFlow<StringSource> = MutableStateFlow(title)
|
||||||
private val _items: MutableStateFlow<List<MenuItem>> = MutableStateFlow(items)
|
private val _items: MutableStateFlow<List<MenuItem>> = MutableStateFlow(items)
|
||||||
|
|
||||||
override var icon: StateFlow<IconSource?> = _icon.asStateFlow()
|
override var icon: StateFlow<IconSource?> = _icon.asStateFlow()
|
||||||
override var title: StateFlow<String> = _title.asStateFlow()
|
override var title: StateFlow<StringSource> = _title.asStateFlow()
|
||||||
|
|
||||||
val items: StateFlow<List<MenuItem>> = _items.asStateFlow()
|
val items: StateFlow<List<MenuItem>> = _items.asStateFlow()
|
||||||
fun setItems(newItems: List<MenuItem>) {
|
fun setItems(newItems: List<MenuItem>) {
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
package ir.amirab.util.compose.contants
|
||||||
|
|
||||||
|
const val RESOURCE_PROTOCOL = "app-resource"
|
||||||
|
const val FILE_PROTOCOL = "file"
|
@ -0,0 +1,225 @@
|
|||||||
|
package ir.amirab.util.compose.localizationmanager
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import ir.amirab.util.compose.contants.FILE_PROTOCOL
|
||||||
|
import ir.amirab.util.compose.contants.RESOURCE_PROTOCOL
|
||||||
|
import ir.amirab.util.flow.mapStateFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import okio.FileSystem
|
||||||
|
import okio.Path.Companion.toPath
|
||||||
|
import okio.buffer
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.net.URI
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.Properties
|
||||||
|
|
||||||
|
class LanguageManager(
|
||||||
|
private val storage: LanguageStorage,
|
||||||
|
) {
|
||||||
|
private val _languageList: MutableStateFlow<List<LanguageInfo>> = MutableStateFlow(emptyList())
|
||||||
|
val languageList = _languageList.asStateFlow()
|
||||||
|
val selectedLanguage = storage.selectedLanguage
|
||||||
|
val isRtl = selectedLanguage.mapStateFlow {
|
||||||
|
rtlLanguages.contains(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun boot() {
|
||||||
|
_languageList.value = getAvailableLanguages()
|
||||||
|
instance = this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectLanguage(code: String?) {
|
||||||
|
val languageCode = code ?: Locale.getDefault().language
|
||||||
|
val languageInfo = languageList.value.find { it.languageCode == languageCode }
|
||||||
|
selectedLanguage.value = (languageInfo ?: DefaultLanguageInfo).languageCode
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMessage(key: String): String {
|
||||||
|
return getMessageContainer().getMessage(key)
|
||||||
|
?: defaultLanguageData.value.getMessage(key)
|
||||||
|
?: key
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRequestedLanguage(): String {
|
||||||
|
return selectedLanguage.value
|
||||||
|
}
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var loadedLanguage: LoadedLanguage? = null
|
||||||
|
|
||||||
|
private val defaultLanguageData = lazy {
|
||||||
|
createMessageContainer(DefaultLanguageInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMessageContainer(
|
||||||
|
languageInfo: LanguageInfo,
|
||||||
|
): MessageData {
|
||||||
|
return when {
|
||||||
|
languageInfo == DefaultLanguageInfo && defaultLanguageData.isInitialized() -> defaultLanguageData.value
|
||||||
|
else -> PropertiesMessageContainer(
|
||||||
|
Properties().apply {
|
||||||
|
kotlin.runCatching {
|
||||||
|
openStream(languageInfo.path)
|
||||||
|
.reader(Charsets.UTF_8)
|
||||||
|
.use {
|
||||||
|
load(it)
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
println("Error while loading language data!")
|
||||||
|
it.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bestLanguageInfo(code: String): LanguageInfo {
|
||||||
|
return languageList.value.find {
|
||||||
|
it.languageCode == code
|
||||||
|
} ?: DefaultLanguageInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMessageContainer(): MessageData {
|
||||||
|
val requestedLanguage = getRequestedLanguage()
|
||||||
|
this.loadedLanguage.let { loadedLanguage ->
|
||||||
|
if (loadedLanguage != null && loadedLanguage.languageInfo.languageCode == requestedLanguage) {
|
||||||
|
return loadedLanguage.messageData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
synchronized(this) {
|
||||||
|
// make sure not created earlier
|
||||||
|
this.loadedLanguage.let { loadedLanguage ->
|
||||||
|
if (loadedLanguage != null && loadedLanguage.languageInfo.languageCode == requestedLanguage) {
|
||||||
|
return loadedLanguage.messageData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val languageInfo = bestLanguageInfo(requestedLanguage)
|
||||||
|
val created = LoadedLanguage(
|
||||||
|
languageInfo,
|
||||||
|
createMessageContainer(languageInfo)
|
||||||
|
)
|
||||||
|
this.loadedLanguage = created
|
||||||
|
return created.messageData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAvailableLanguages(): List<LanguageInfo> {
|
||||||
|
val fileSystem = FileSystem.RESOURCES
|
||||||
|
return fileSystem
|
||||||
|
.list(LOCALES_PATH.toPath())
|
||||||
|
.mapNotNull {
|
||||||
|
kotlin.runCatching {
|
||||||
|
if (fileSystem.metadataOrNull(it)?.isRegularFile == false) {
|
||||||
|
return@runCatching null
|
||||||
|
}
|
||||||
|
val languageCodeAndCountryCode = extractLanguageCodeAndCountryCodeFromFileName(it.name)
|
||||||
|
?: return@runCatching null
|
||||||
|
val locale = if (languageCodeAndCountryCode.countryCode != null) {
|
||||||
|
Locale(languageCodeAndCountryCode.languageCode, languageCodeAndCountryCode.countryCode)
|
||||||
|
} else {
|
||||||
|
Locale(languageCodeAndCountryCode.languageCode)
|
||||||
|
}
|
||||||
|
locale to it
|
||||||
|
}.getOrNull()
|
||||||
|
}.let {
|
||||||
|
val localesWithPath = it
|
||||||
|
localesWithPath.map { (locale, path) ->
|
||||||
|
locale.toLanguageInfo(
|
||||||
|
path = "$RESOURCE_PROTOCOL://$path",
|
||||||
|
hasMultiRegion = localesWithPath.count {
|
||||||
|
it.first.language == locale.language
|
||||||
|
} > 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
lateinit var instance: LanguageManager
|
||||||
|
private const val LOCALES_PATH = "/com/abdownloadmanager/resources/locales"
|
||||||
|
val DefaultLanguageInfo = LanguageInfo(
|
||||||
|
languageCode = "en",
|
||||||
|
countryCode = "US",
|
||||||
|
nativeName = "English",
|
||||||
|
path = URI("$RESOURCE_PROTOCOL:$LOCALES_PATH/en_US.properties")
|
||||||
|
)
|
||||||
|
|
||||||
|
fun openStream(uri: URI): InputStream {
|
||||||
|
return when (uri.scheme) {
|
||||||
|
RESOURCE_PROTOCOL -> FileSystem.RESOURCES.source(uri.path.toPath())
|
||||||
|
FILE_PROTOCOL -> FileSystem.SYSTEM.source(uri.path.toPath())
|
||||||
|
else -> error("unsupported URI")
|
||||||
|
}.buffer().inputStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Locale.toLanguageInfo(
|
||||||
|
path: String,
|
||||||
|
hasMultiRegion: Boolean,
|
||||||
|
): LanguageInfo {
|
||||||
|
return LanguageInfo(
|
||||||
|
languageCode = language,
|
||||||
|
countryCode = country,
|
||||||
|
nativeName = if (hasMultiRegion) getDisplayName(this) else getDisplayLanguage(this),
|
||||||
|
path = URI(path)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val rtlLanguages = arrayOf("ar", "fa", "he", "iw", "ji", "ur", "yi")
|
||||||
|
|
||||||
|
private fun extractLanguageCodeAndCountryCodeFromFileName(name: String): LanguageCodeAndCountryCode? {
|
||||||
|
return name
|
||||||
|
.split(".")
|
||||||
|
.firstOrNull()
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?.let {
|
||||||
|
it.split("_").run {
|
||||||
|
LanguageCodeAndCountryCode(
|
||||||
|
languageCode = get(0),
|
||||||
|
countryCode = getOrNull(1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class LanguageCodeAndCountryCode(
|
||||||
|
val languageCode: String,
|
||||||
|
val countryCode: String?,
|
||||||
|
) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return buildString {
|
||||||
|
append(languageCode)
|
||||||
|
countryCode?.let {
|
||||||
|
append("_")
|
||||||
|
append(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageData {
|
||||||
|
fun getMessage(key: String): String?
|
||||||
|
}
|
||||||
|
|
||||||
|
class PropertiesMessageContainer(
|
||||||
|
private val properties: Properties,
|
||||||
|
) : MessageData {
|
||||||
|
override fun getMessage(key: String): String? {
|
||||||
|
return properties.getProperty(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class LoadedLanguage(
|
||||||
|
val languageInfo: LanguageInfo,
|
||||||
|
val messageData: MessageData,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class LanguageInfo(
|
||||||
|
val languageCode: String,
|
||||||
|
val countryCode: String?,
|
||||||
|
val nativeName: String,
|
||||||
|
val path: URI,
|
||||||
|
)
|
@ -0,0 +1,7 @@
|
|||||||
|
package ir.amirab.util.compose.localizationmanager
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
|
interface LanguageStorage {
|
||||||
|
val selectedLanguage: MutableStateFlow<String>
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package ir.amirab.util.compose.localizationmanager
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
|
||||||
|
val LocalLanguageManager = staticCompositionLocalOf<LanguageManager> {
|
||||||
|
error("LocalLanguageManager not provided")
|
||||||
|
}
|
||||||
|
val LocaleLanguageDirection = staticCompositionLocalOf<LayoutDirection> {
|
||||||
|
error("LocaleLanguageDirection not provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WithLanguageDirection(
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalLayoutDirection provides LocaleLanguageDirection.current,
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package ir.amirab.util.compose.localizationmanager
|
||||||
|
|
||||||
|
import arrow.core.fold
|
||||||
|
|
||||||
|
private fun String.replaceWithVariable(name: String, value: String): String {
|
||||||
|
return replace("{{$name}}", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun String.withReplacedArgs(args: Map<String, String>): String {
|
||||||
|
return args.fold(this) { acc, entry ->
|
||||||
|
acc.replaceWithVariable(entry.key, entry.value)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package ir.amirab.util.compose.modifiers
|
||||||
|
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.composed
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
|
||||||
|
fun Modifier.autoMirror() = composed {
|
||||||
|
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
|
||||||
|
scale(
|
||||||
|
scaleX = if (isRtl) -1f else 1f,
|
||||||
|
scaleY = 1f
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package ir.amirab.util.compose.resources
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import ir.amirab.util.compose.localizationmanager.LocalLanguageManager
|
||||||
|
import ir.amirab.util.compose.localizationmanager.withReplacedArgs
|
||||||
|
|
||||||
|
typealias MyStringResource = ir.amirab.resources.contracts.MyStringResource
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun myStringResource(key: MyStringResource): String {
|
||||||
|
val languageManager = LocalLanguageManager.current
|
||||||
|
val language by languageManager.selectedLanguage.collectAsState()
|
||||||
|
return remember(language, key) {
|
||||||
|
languageManager.getMessage(key.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun myStringResource(key: MyStringResource, args: Map<String, String>): String {
|
||||||
|
val languageManager = LocalLanguageManager.current
|
||||||
|
val language by languageManager.selectedLanguage.collectAsState()
|
||||||
|
return remember(language, key, args) {
|
||||||
|
languageManager
|
||||||
|
.getMessage(key.id)
|
||||||
|
.withReplacedArgs(args)
|
||||||
|
}
|
||||||
|
}
|
137
shared/resources/build.gradle.kts
Normal file
137
shared/resources/build.gradle.kts
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import java.util.Properties
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id(MyPlugins.kotlin)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":shared:resources:contracts"))
|
||||||
|
}
|
||||||
|
val propertiesToKotlinTask by tasks.registering(PropertiesToKotlinTask::class) {
|
||||||
|
outputDir.set(file("build/tasks/propertiesToKotlinTask"))
|
||||||
|
generatedFileName.set("Res.kt")
|
||||||
|
packageName.set("com.abdownloadmanager.resources")
|
||||||
|
myStringResourceClass.set("ir.amirab.resources.contracts.MyStringResource")
|
||||||
|
propertyFiles.from("src/main/resources/com/abdownloadmanager/resources/locales/en_US.properties")
|
||||||
|
}
|
||||||
|
tasks.compileKotlin {
|
||||||
|
dependsOn(propertiesToKotlinTask)
|
||||||
|
}
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
kotlin {
|
||||||
|
srcDirs(propertiesToKotlinTask.map { it.outputDir })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class PropertiesToKotlinTask @Inject constructor(
|
||||||
|
project: Project,
|
||||||
|
) : DefaultTask() {
|
||||||
|
@get:InputFiles
|
||||||
|
val propertyFiles = project.objects.fileCollection()
|
||||||
|
|
||||||
|
@get:Input
|
||||||
|
val packageName = project.objects.property<String>()
|
||||||
|
|
||||||
|
@get:Input
|
||||||
|
val myStringResourceClass = project.objects.property<String>()
|
||||||
|
|
||||||
|
@get:OutputDirectory
|
||||||
|
val outputDir = project.objects.directoryProperty()
|
||||||
|
|
||||||
|
@get:Input
|
||||||
|
val generatedFileName = project.objects.property<String>()
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
fun run() {
|
||||||
|
val properties = Properties()
|
||||||
|
propertyFiles.forEach { file ->
|
||||||
|
file.inputStream().use { inputStream ->
|
||||||
|
properties.load(inputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val content = createFileString(
|
||||||
|
packageName.get(),
|
||||||
|
myStringResourceClass.get(),
|
||||||
|
properties
|
||||||
|
)
|
||||||
|
outputDir.file(generatedFileName).get().asFile.writer().use {
|
||||||
|
it.write(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createFileString(
|
||||||
|
packageName: String,
|
||||||
|
myStringResourceClass: String,
|
||||||
|
properties: Properties,
|
||||||
|
): String {
|
||||||
|
val myStringResourceClassName = myStringResourceClass
|
||||||
|
.split(".").last()
|
||||||
|
val variableRegex by lazy { "\\{\\{(?<variable>.+)\\}\\}".toRegex() }
|
||||||
|
fun findVariablesOfValue(value: String): List<String> {
|
||||||
|
return variableRegex
|
||||||
|
.findAll(value)
|
||||||
|
.toList()
|
||||||
|
.map {
|
||||||
|
it.groups["variable"]!!.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun propertyToCode(key: String, value: String): String {
|
||||||
|
val args = findVariablesOfValue(value)
|
||||||
|
val defination = "val `$key` = $myStringResourceClassName(\"$key\")"
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
return defination
|
||||||
|
} else {
|
||||||
|
val comment = buildString {
|
||||||
|
append("/**\n")
|
||||||
|
append("accepted args:\n")
|
||||||
|
args.forEach { value ->
|
||||||
|
append("@param [$value]\n")
|
||||||
|
}
|
||||||
|
append("*/")
|
||||||
|
}
|
||||||
|
val argCreatorFunction = buildString {
|
||||||
|
append("fun `${key}_createArgs`(")
|
||||||
|
args.forEachIndexed { index, value ->
|
||||||
|
append("$value: String")
|
||||||
|
if (index != args.lastIndex) {
|
||||||
|
append(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
append(") = ")
|
||||||
|
append("mapOf(")
|
||||||
|
args.forEachIndexed { index, value ->
|
||||||
|
append("\"$value\" to $value")
|
||||||
|
if (index != args.lastIndex) {
|
||||||
|
append(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
append(")")
|
||||||
|
}
|
||||||
|
return "$defination\n$comment\n$argCreatorFunction"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildString {
|
||||||
|
append("@file:Suppress(\"RemoveRedundantBackticks\", \"FunctionName\")")
|
||||||
|
append("package $packageName\n")
|
||||||
|
append("import $myStringResourceClass\n")
|
||||||
|
|
||||||
|
append("object Res {\n")
|
||||||
|
append(" object string {\n")
|
||||||
|
for (property in properties) {
|
||||||
|
val key = property.key.toString()
|
||||||
|
val value = property.value.toString()
|
||||||
|
val codeLines = propertyToCode(key, value).lines()
|
||||||
|
for (line in codeLines) {
|
||||||
|
append(" $line\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
append(" }\n")
|
||||||
|
append("}\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
shared/resources/contracts/build.gradle.kts
Normal file
3
shared/resources/contracts/build.gradle.kts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
plugins {
|
||||||
|
id(MyPlugins.kotlin)
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
package ir.amirab.resources.contracts
|
||||||
|
|
||||||
|
@JvmInline
|
||||||
|
value class MyStringResource(val id: String)
|
@ -0,0 +1,271 @@
|
|||||||
|
app_title=AB Download Manager
|
||||||
|
confirm_auto_categorize_downloads_title=Auto categorize downloads
|
||||||
|
confirm_auto_categorize_downloads_description=Any uncategorized item will be automatically added to its related category.
|
||||||
|
confirm_reset_to_default_categories_title=Reset to Default Categories
|
||||||
|
confirm_reset_to_default_categories_description=this will REMOVE all categories and brings backs default categories
|
||||||
|
confirm_delete_download_items_title=Confirm Delete
|
||||||
|
confirm_delete_download_items_description=Are you sure you want to delete {{count}} items?
|
||||||
|
also_delete_file_from_disk=Also delete file from disk
|
||||||
|
confirm_delete_category_item_title=Removing {{name}} category
|
||||||
|
confirm_delete_category_item_description=Are you sure you want to delete "{{value}}" Category ?
|
||||||
|
your_download_will_not_be_deleted=Your downloads won't be deleted
|
||||||
|
drop_link_or_file_here=Drop link or file here.
|
||||||
|
nothing_will_be_imported=Your downloads won't be deleted
|
||||||
|
n_links_will_be_imported={{count}} links will be imported
|
||||||
|
n_items_selected={{count}} items selected
|
||||||
|
delete=Delete
|
||||||
|
remove=Remove
|
||||||
|
cancel=Cancel
|
||||||
|
close=Close
|
||||||
|
ok=Ok
|
||||||
|
add=Add
|
||||||
|
change=Change
|
||||||
|
download=Download
|
||||||
|
refresh=Refresh
|
||||||
|
settings=Settings
|
||||||
|
unknown=Unknown
|
||||||
|
unknown_error=Unknown Error
|
||||||
|
download_item_not_found=Download item not found
|
||||||
|
name=Name
|
||||||
|
download_link=Download link
|
||||||
|
not_finished=Not finished
|
||||||
|
all=All
|
||||||
|
finished=Finished
|
||||||
|
Unfinished=Unfinished
|
||||||
|
canceled=Canceled
|
||||||
|
error=Error
|
||||||
|
paused=Paused
|
||||||
|
downloading=Downloading
|
||||||
|
added=Added
|
||||||
|
idle=IDLE
|
||||||
|
preparing_file=Preparing File
|
||||||
|
creating_file=Creating File
|
||||||
|
resuming=Resuming
|
||||||
|
list_is_empty=List is empty.
|
||||||
|
search_in_the_list=Search in the List
|
||||||
|
search=Search
|
||||||
|
clear=Clear
|
||||||
|
general=General
|
||||||
|
enabled=Enabled
|
||||||
|
disabled=Disabled
|
||||||
|
file=File
|
||||||
|
tasks=Tasks
|
||||||
|
tools=Tools
|
||||||
|
help=Help
|
||||||
|
all_finished=All Finished
|
||||||
|
all_unfinished=All Unfinished
|
||||||
|
entire_list=Entire List
|
||||||
|
download_browser_integration=Download Browser Integration
|
||||||
|
exit=Exit
|
||||||
|
show_downloads=Show Downloads
|
||||||
|
new_download=New Download
|
||||||
|
stop_all=Stop All
|
||||||
|
import_from_clipboard=New Download
|
||||||
|
batch_download=Batch Download
|
||||||
|
open=Open
|
||||||
|
open_file=Open File
|
||||||
|
open_folder=Open Folder
|
||||||
|
resume=Resume
|
||||||
|
pause=Pause
|
||||||
|
restart_download=Restart Download
|
||||||
|
copy_link=Copy link
|
||||||
|
show_properties=Show Properties
|
||||||
|
move_to_queue=Move To Queue
|
||||||
|
move_to_category=Move To Category
|
||||||
|
add_category=Add Category
|
||||||
|
edit_category=Edit Category
|
||||||
|
delete_category=Delete Category
|
||||||
|
category_name=Category Name
|
||||||
|
category_download_location=Category Download Location
|
||||||
|
category_download_location_description=When this category chosen in "Add Download Page" use this directory as "Download Location"
|
||||||
|
category_file_types=Category file types
|
||||||
|
category_file_types_description=Automatically put these file types to this category. (when you add new download)\nSeparate file extensions with space (ext1 ext2 ...)
|
||||||
|
url_patterns=URL Patterns
|
||||||
|
url_patterns_description=Automatically put download from these URLs to this category. (when you add new download)\nSeparate URLs with space, you can also use * for wildcard
|
||||||
|
auto_categorize_downloads=Auto Categorise Downloads
|
||||||
|
restore_defaults=Restore Defaults
|
||||||
|
about=About
|
||||||
|
version_n=Version {{value}}
|
||||||
|
developed_with_love_for_you=Developed with ?? for you
|
||||||
|
visit_the_project_website=Visit the project website
|
||||||
|
this_is_a_free_and_open_source_software=This is a free & Open Source software
|
||||||
|
view_the_source_code=See the Source Code
|
||||||
|
powered_by_open_source_software=Powered by Open Source Software
|
||||||
|
view_the_open_source_licenses=View the Open-Source licenses
|
||||||
|
support_and_community=Support & Community
|
||||||
|
telegram=Telegram
|
||||||
|
channel=Channel
|
||||||
|
group=Group
|
||||||
|
add_download=Add Download
|
||||||
|
add_multi_download_page_header=Select Items you want to pick up for download
|
||||||
|
save_to=Save To
|
||||||
|
where_should_each_item_saved=Where should each item saved?
|
||||||
|
there_are_multiple_items_please_select_a_way_you_want_to_save_them=There are multiple items! please select a way you want to save them
|
||||||
|
each_item_on_its_own_category=Each item on its own category
|
||||||
|
each_item_on_its_own_category_description=Each item will be placed in a category that have that file type
|
||||||
|
all_items_in_one_category=All items in one Category
|
||||||
|
all_items_in_one_category_description=All files will be saved in the selected category location
|
||||||
|
all_items_in_one_Location=All items in one Location
|
||||||
|
all_items_in_one_Location_description=All items will be saved in the selected directory
|
||||||
|
no_category_selected=No Category Selected
|
||||||
|
download_location=Download Location
|
||||||
|
location=Location
|
||||||
|
select_queue=Select Queue
|
||||||
|
without_queue=Without Queue
|
||||||
|
use_category=Use Category
|
||||||
|
cant_write_to_this_folder=Can't write to this folder
|
||||||
|
file_name_already_exists=File name already exists
|
||||||
|
invalid_file_name=Invalid filename
|
||||||
|
show_solutions=Show solutions...
|
||||||
|
change_solution=Change solution
|
||||||
|
select_a_solution=Select a solution
|
||||||
|
select_download_strategy_description=The link you provided is already in download lists please specify what you want to do
|
||||||
|
download_strategy_add_a_numbered_file=Add a numbered file
|
||||||
|
download_strategy_add_a_numbered_file_description=Add an index after the end of download file name
|
||||||
|
download_strategy_override_existing_file=Override existing file
|
||||||
|
download_strategy_override_existing_file_description=Remove existing download and write to that file
|
||||||
|
download_strategy_show_downloaded_file=Show downloaded file
|
||||||
|
download_strategy_show_downloaded_file_description=Show already existing download item, so you can press on resume or open it
|
||||||
|
batch_download_link_help=Enter a link that contains wildcards (use *)
|
||||||
|
invalid_url=Invalid URL
|
||||||
|
list_is_too_large_maximum_n_items_allowed=List is too large! maximum {{count}} items allowed
|
||||||
|
enter_range=Enter range
|
||||||
|
from=From
|
||||||
|
to=To
|
||||||
|
wildcard_length=Wildcard length
|
||||||
|
first_link=First Link
|
||||||
|
last_link=Last Link
|
||||||
|
open_source_software_used_in_this_app=Open Source Software used in this App
|
||||||
|
links=Links
|
||||||
|
website=Website
|
||||||
|
developers=Developers
|
||||||
|
source_code=Source Code
|
||||||
|
license=License
|
||||||
|
no_license_found=No license found
|
||||||
|
organization=Organization
|
||||||
|
add_new_queue=Add New Queue
|
||||||
|
queue_name=Queue Name
|
||||||
|
queues=Queues
|
||||||
|
stop_queue=Stop Queue
|
||||||
|
start_queue=Start Queue
|
||||||
|
config=Config
|
||||||
|
items=Items
|
||||||
|
move_down=Move down
|
||||||
|
move_up=Move up
|
||||||
|
remove_queue=Remove Queue
|
||||||
|
queue_name_help=Specify A name for this queue
|
||||||
|
queue_name_describe=Queue name is {{value}}
|
||||||
|
queue_max_concurrent_download=Max concurrent download
|
||||||
|
queue_max_concurrent_download_description=Max download for this queue
|
||||||
|
queue_automatic_stop=Automatic stop
|
||||||
|
queue_automatic_stop_description=Automatic stop queue when there is no item in it
|
||||||
|
queue_scheduler=Scheduler
|
||||||
|
queue_enable_scheduler=Enable Scheduler
|
||||||
|
queue_active_days=Active Days
|
||||||
|
queue_active_days_description=Which days schedulers function ?
|
||||||
|
queue_scheduler_auto_start_time=Auto Start Time
|
||||||
|
queue_scheduler_enable_auto_stop_time=Enable Auto Stop Time
|
||||||
|
queue_scheduler_auto_stop_time=Auto Stop Time
|
||||||
|
appearance=Appearance
|
||||||
|
download_engine=Download Engine
|
||||||
|
browser_integration=Browser Integration
|
||||||
|
settings_download_thread_count=Thread Count
|
||||||
|
settings_download_thread_count_description=Maximum download thread per download item
|
||||||
|
settings_download_thread_count_describe=a download can have up to {{count}} threads
|
||||||
|
settings_use_server_last_modified_time=Use Server's Last-Modified Time
|
||||||
|
settings_use_server_last_modified_time_description=When downloading a file, use server's last modified time for the local file
|
||||||
|
settings_use_sparse_file_allocation=Sparse File Allocation
|
||||||
|
settings_use_sparse_file_allocation_description=When downloading a file, use server's last modified time for the local file
|
||||||
|
settings_global_speed_limiter=Global Speed Limiter
|
||||||
|
settings_global_speed_limiter_description=Global download speed limit (0 means unlimited)
|
||||||
|
settings_show_average_speed=Show Average Speed
|
||||||
|
settings_show_average_speed_description=Download speed in average or precision
|
||||||
|
settings_default_download_folder=Default Download Folder
|
||||||
|
settings_default_download_folder_description=When you add new download this location is used by default
|
||||||
|
settings_default_download_folder_describe="{{folder}}" will be used
|
||||||
|
settings_use_proxy=Use Proxy
|
||||||
|
settings_use_proxy_description=Use proxy for downloading files
|
||||||
|
settings_use_proxy_describe_no_proxy=No Proxy will be used
|
||||||
|
settings_use_proxy_describe_system_proxy=System Proxy will be used
|
||||||
|
settings_use_proxy_describe_manual_proxy="{{value}}" will be used
|
||||||
|
settings_theme=Theme
|
||||||
|
settings_theme_description=Select a theme for the App
|
||||||
|
settings_language=Language
|
||||||
|
settings_compact_top_bar=Compact Top Bar
|
||||||
|
settings_compact_top_bar_description=Merge top bar with title bar when the main window has enough width
|
||||||
|
settings_start_on_boot=Start On Boot
|
||||||
|
settings_start_on_boot_description=Auto start application on user logins
|
||||||
|
settings_notification_sound=Notification Sound
|
||||||
|
settings_notification_sound_description=Play sound on new notification
|
||||||
|
settings_browser_integration=Browser Integration
|
||||||
|
settings_browser_integration_description=Accept downloads from browsers
|
||||||
|
settings_browser_integration_server_port=Server Port
|
||||||
|
settings_browser_integration_server_port_description=Port for browser integration
|
||||||
|
settings_browser_integration_server_port_describe=App will listen to {{port}}
|
||||||
|
settings_dynamic_part_creation=Dynamic part creation
|
||||||
|
settings_dynamic_part_creation_description=When a part is finished create another part by splitting other parts to improve download speed
|
||||||
|
download_item_settings_speed_limit=Speed Limit
|
||||||
|
download_item_settings_speed_limit_description=Limit download speed for this item
|
||||||
|
download_item_settings_thread_count=Thread count
|
||||||
|
download_item_settings_thread_count_description=How much thread used to download this download item (0 for default)
|
||||||
|
download_item_settings_thread_count_describe={{count}} threads for this download
|
||||||
|
download_item_settings_username_description=Provide a username if the link is a protected resource
|
||||||
|
download_item_settings_password_description=Provide a password if the link is a protected resource
|
||||||
|
username=Username
|
||||||
|
password=Password
|
||||||
|
average_speed=Average Speed
|
||||||
|
exact_speed=Exact Speed
|
||||||
|
unlimited=Unlimited
|
||||||
|
use_global_settings=Use Global Settings
|
||||||
|
cant_run_browser_integration=Can't run browser integration
|
||||||
|
cant_open_file=Can't Open File
|
||||||
|
cant_open_folder=Can't Open Folder
|
||||||
|
# times for example 2 seconds ago
|
||||||
|
relative_time_long_years={{years}} years
|
||||||
|
relative_time_long_months={{months}} months
|
||||||
|
relative_time_long_days={{days}} days
|
||||||
|
relative_time_long_hours={{hours}} hours
|
||||||
|
relative_time_long_minutes={{minutes}} minutes
|
||||||
|
relative_time_long_seconds={{seconds}} seconds
|
||||||
|
relative_time_short_years={{years}} y
|
||||||
|
relative_time_short_months={{months}} M
|
||||||
|
relative_time_short_days={{days}} d
|
||||||
|
relative_time_short_hours={{hours}} hr
|
||||||
|
relative_time_short_minutes={{minutes}} min
|
||||||
|
relative_time_short_seconds={{seconds}} sec
|
||||||
|
relative_time_left={{time}} left
|
||||||
|
relative_time_ago={{time}} ago
|
||||||
|
auto=Auto
|
||||||
|
unspecified=Unspecified
|
||||||
|
custom=Custom
|
||||||
|
icon=Icon
|
||||||
|
author=Author
|
||||||
|
link=Link
|
||||||
|
size=Size
|
||||||
|
status=Status
|
||||||
|
parts_info_downloaded_size=Downloaded
|
||||||
|
parts_info_total_size=Total
|
||||||
|
speed=Speed
|
||||||
|
time_left=Time Left
|
||||||
|
date_added=Date Added
|
||||||
|
info=Info
|
||||||
|
download_page_downloaded_size=Downloaded
|
||||||
|
resume_support=Resume Support
|
||||||
|
yes=Yes
|
||||||
|
no=No
|
||||||
|
parts_info=Parts Info
|
||||||
|
disconnected=Disconnected
|
||||||
|
receiving_data=Receiving Data
|
||||||
|
send_get=Send Get
|
||||||
|
warning=Warning
|
||||||
|
unsupported_resume_warning=This download doesn't support resuming! You may have to RESTART it later in the Download List
|
||||||
|
stop_anyway=Stop Anyway
|
||||||
|
customize_columns=Customize Columns
|
||||||
|
reset=Reset
|
||||||
|
monday=Monday
|
||||||
|
tuesday=Tuesday
|
||||||
|
wednesday=Wednesday
|
||||||
|
thursday=Thursday
|
||||||
|
friday=Friday
|
||||||
|
saturday=Saturday
|
||||||
|
sunday=Sunday
|
Loading…
x
Reference in New Issue
Block a user