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