add localization

This commit is contained in:
AmirHossein Abdolmotallebi 2024-10-23 08:01:30 +03:30
parent 62a8bb73c4
commit b14c2bb5ad
84 changed files with 2082 additions and 697 deletions

View File

@ -3,6 +3,7 @@ import ir.amirab.util.platform.Platform
object MyPlugins { object MyPlugins {
private const val namespace = "myPlugins" private const val namespace = "myPlugins"
const val kotlin = "$namespace.kotlin" const val kotlin = "$namespace.kotlin"
const val kotlinMultiplatform = "$namespace.kotlinMultiplatform"
const val composeDesktop = "$namespace.composeDesktop" const val composeDesktop = "$namespace.composeDesktop"
const val composeBase = "$namespace.composeBase" const val composeBase = "$namespace.composeBase"
const val proguardDesktop = "$namespace.proguardDesktop" const val proguardDesktop = "$namespace.proguardDesktop"
@ -22,4 +23,6 @@ object Plugins {
const val changeLog = "org.jetbrains.changelog" const val changeLog = "org.jetbrains.changelog"
const val buildConfig = "com.github.gmazzo.buildconfig" const val buildConfig = "com.github.gmazzo.buildconfig"
const val aboutLibraries = "com.mikepenz.aboutlibraries.plugin" const val aboutLibraries = "com.mikepenz.aboutlibraries.plugin"
const val multiplatformResources = "dev.icerock.mobile.multiplatform-resources"
} }

View File

@ -0,0 +1,9 @@
package myPlugins
plugins {
kotlin("multiplatform")
}
repositories {
mavenCentral()
google()
}

View File

@ -71,6 +71,7 @@ dependencies {
implementation(project(":shared:updater")) implementation(project(":shared:updater"))
implementation(project(":shared:auto-start")) implementation(project(":shared:auto-start"))
implementation(project(":shared:nanohttp4k")) implementation(project(":shared:nanohttp4k"))
implementation(project(":shared:resources"))
} }
aboutLibraries { aboutLibraries {

View File

@ -37,9 +37,13 @@ import ir.amirab.downloader.utils.ExceptionUtils
import ir.amirab.downloader.utils.OnDuplicateStrategy import ir.amirab.downloader.utils.OnDuplicateStrategy
import com.abdownloadmanager.integration.Integration import com.abdownloadmanager.integration.Integration
import com.abdownloadmanager.integration.IntegrationResult import com.abdownloadmanager.integration.IntegrationResult
import com.abdownloadmanager.resources.*
import com.abdownloadmanager.utils.category.CategoryManager import com.abdownloadmanager.utils.category.CategoryManager
import com.abdownloadmanager.utils.category.CategorySelectionMode import com.abdownloadmanager.utils.category.CategorySelectionMode
import ir.amirab.downloader.exception.TooManyErrorException import ir.amirab.downloader.exception.TooManyErrorException
import ir.amirab.util.compose.StringSource
import ir.amirab.util.compose.asStringSource
import ir.amirab.util.compose.combineStringSources
import ir.amirab.util.osfileutil.FileUtils import ir.amirab.util.osfileutil.FileUtils
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -56,8 +60,8 @@ sealed interface AppEffects {
} }
interface NotificationSender { interface NotificationSender {
fun sendDialogNotification(title: String, description: String, type: MessageDialogType) fun sendDialogNotification(title: StringSource, description: StringSource, type: MessageDialogType)
fun sendNotification(tag: Any, title: String, description: String, type: NotificationType) fun sendNotification(tag: Any, title: StringSource, description: StringSource, type: NotificationType)
} }
class AppComponent( class AppComponent(
@ -365,14 +369,14 @@ class AppComponent(
}.launchIn(scope) }.launchIn(scope)
} }
override fun sendNotification(tag: Any, title: String, description: String, type: NotificationType) { override fun sendNotification(tag: Any, title: StringSource, description: StringSource, type: NotificationType) {
beep() beep()
showNotification(tag = tag, title = title, description = description, type = type) showNotification(tag = tag, title = title, description = description, type = type)
} }
override fun sendDialogNotification( override fun sendDialogNotification(
title: String, title: StringSource,
description: String, description: StringSource,
type: MessageDialogType, type: MessageDialogType,
) { ) {
beep() beep()
@ -387,8 +391,8 @@ class AppComponent(
private fun showNotification( private fun showNotification(
tag: Any, tag: Any,
title: String, title: StringSource,
description: String, description: StringSource,
type: NotificationType = NotificationType.Info, type: NotificationType = NotificationType.Info,
) { ) {
sendEffect( sendEffect(
@ -418,9 +422,9 @@ class AppComponent(
is IntegrationResult.Fail -> { is IntegrationResult.Fail -> {
IntegrationPortBroadcaster.setIntegrationPortInFile(null) IntegrationPortBroadcaster.setIntegrationPortInFile(null)
sendDialogNotification( sendDialogNotification(
title = "Can't run browser integration", title = Res.string.cant_run_browser_integration.asStringSource(),
type = MessageDialogType.Error, type = MessageDialogType.Error,
description = it.throwable.localizedMessage description = it.throwable.localizedMessage.asStringSource()
) )
} }
@ -467,20 +471,20 @@ class AppComponent(
"Too Many Error: " "Too Many Error: "
} else { } else {
"Error: " "Error: "
} }.asStringSource()
val reason = actualCause.message ?: "Unknown" val reason = actualCause.message?.asStringSource() ?: Res.string.unknown.asStringSource()
sendNotification( sendNotification(
"downloadId=${it.downloadItem.id}", "downloadId=${it.downloadItem.id}",
title = it.downloadItem.name, title = it.downloadItem.name.asStringSource(),
description = prefix + reason, description = listOf(prefix, reason).combineStringSources(),
type = NotificationType.Error, type = NotificationType.Error,
) )
} }
if (it is DownloadManagerEvents.OnJobCompleted) { if (it is DownloadManagerEvents.OnJobCompleted) {
sendNotification( sendNotification(
tag = "downloadId=${it.downloadItem.id}", tag = "downloadId=${it.downloadItem.id}",
title = it.downloadItem.name, title = it.downloadItem.name.asStringSource(),
description = "Finished", description = Res.string.finished.asStringSource(),
type = NotificationType.Success, type = NotificationType.Success,
) )
} }
@ -490,9 +494,9 @@ class AppComponent(
val item = downloadSystem.getDownloadItemById(id) val item = downloadSystem.getDownloadItemById(id)
if (item == null) { if (item == null) {
sendNotification( sendNotification(
"Open File", Res.string.open_file,
"Can't open file", Res.string.cant_open_file.asStringSource(),
"Download Item not found", Res.string.download_item_not_found.asStringSource(),
NotificationType.Error, NotificationType.Error,
) )
return return
@ -505,9 +509,9 @@ class AppComponent(
FileUtils.openFile(downloadSystem.getDownloadFile(downloadItem)) FileUtils.openFile(downloadSystem.getDownloadFile(downloadItem))
}.onFailure { }.onFailure {
sendNotification( sendNotification(
"Open File", Res.string.open_file,
"Can't open file", Res.string.cant_open_file.asStringSource(),
it.localizedMessage ?: "Unknown Error", it.localizedMessage?.asStringSource() ?: Res.string.unknown_error.asStringSource(),
NotificationType.Error, NotificationType.Error,
) )
println("Can't open file:${it.message}") println("Can't open file:${it.message}")
@ -518,9 +522,9 @@ class AppComponent(
val item = downloadSystem.getDownloadItemById(id) val item = downloadSystem.getDownloadItemById(id)
if (item == null) { if (item == null) {
sendNotification( sendNotification(
"Open Folder", Res.string.open_folder,
"Can't open folder", Res.string.cant_open_folder.asStringSource(),
"Download Item not found", Res.string.download_item_not_found.asStringSource(),
NotificationType.Error, NotificationType.Error,
) )
return return
@ -533,9 +537,9 @@ class AppComponent(
FileUtils.openFolderOfFile(downloadSystem.getDownloadFile(downloadItem)) FileUtils.openFolderOfFile(downloadSystem.getDownloadFile(downloadItem))
}.onFailure { }.onFailure {
sendNotification( sendNotification(
"Open Folder", Res.string.open_folder,
"Can't open folder", Res.string.cant_open_folder.asStringSource(),
it.localizedMessage ?: "Unknown Error", it.localizedMessage?.asStringSource() ?: Res.string.unknown_error.asStringSource(),
NotificationType.Error, NotificationType.Error,
) )
println("Can't open folder:${it.message}") println("Can't open folder:${it.message}")

View File

@ -7,13 +7,14 @@ import com.abdownloadmanager.desktop.ui.widget.MessageDialogType
import ir.amirab.util.compose.action.AnAction import ir.amirab.util.compose.action.AnAction
import ir.amirab.util.compose.action.MenuItem import ir.amirab.util.compose.action.MenuItem
import ir.amirab.util.compose.action.simpleAction import ir.amirab.util.compose.action.simpleAction
import ir.amirab.util.compose.asStringSource
import org.koin.core.component.get import org.koin.core.component.get
private val appComponent = Di.get<AppComponent>() private val appComponent = Di.get<AppComponent>()
val dummyException by lazy { val dummyException by lazy {
simpleAction( simpleAction(
"Dummy Exception", "Dummy Exception".asStringSource(),
MyIcons.info MyIcons.info
) { ) {
error("This is a dummy exception that is thrown by developer") error("This is a dummy exception that is thrown by developer")
@ -21,7 +22,7 @@ val dummyException by lazy {
} }
val dummyMessage by lazy { val dummyMessage by lazy {
MenuItem.SubMenu( MenuItem.SubMenu(
title = "Show Dialog Message", title = "Show Dialog Message".asStringSource(),
icon = MyIcons.info, icon = MyIcons.info,
items = listOf( items = listOf(
MessageDialogType.Info, MessageDialogType.Info,
@ -34,13 +35,13 @@ val dummyMessage by lazy {
private fun createDummyMessage(type: MessageDialogType): AnAction { private fun createDummyMessage(type: MessageDialogType): AnAction {
return simpleAction( return simpleAction(
"$type Message", "$type Message".asStringSource(),
MyIcons.info, MyIcons.info,
) { ) {
appComponent.sendDialogNotification( appComponent.sendDialogNotification(
type = type, type = type,
title = "Dummy Message", title = "Dummy Message".asStringSource(),
description = "This is a test message" description = "This is a test message".asStringSource()
) )
} }
} }

View File

@ -12,6 +12,8 @@ import ir.amirab.util.compose.action.buildMenu
import ir.amirab.util.compose.action.simpleAction import ir.amirab.util.compose.action.simpleAction
import com.abdownloadmanager.desktop.utils.getIcon import com.abdownloadmanager.desktop.utils.getIcon
import com.abdownloadmanager.desktop.utils.getName import com.abdownloadmanager.desktop.utils.getName
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import com.abdownloadmanager.utils.category.Category import com.abdownloadmanager.utils.category.Category
import ir.amirab.downloader.downloaditem.DownloadCredentials import ir.amirab.downloader.downloaditem.DownloadCredentials
import ir.amirab.downloader.queue.DownloadQueue import ir.amirab.downloader.queue.DownloadQueue
@ -19,6 +21,7 @@ import ir.amirab.downloader.queue.activeQueuesFlow
import ir.amirab.downloader.queue.inactiveQueuesFlow import ir.amirab.downloader.queue.inactiveQueuesFlow
import com.abdownloadmanager.utils.extractors.linkextractor.DownloadCredentialFromStringExtractor import com.abdownloadmanager.utils.extractors.linkextractor.DownloadCredentialFromStringExtractor
import ir.amirab.util.UrlUtils import ir.amirab.util.UrlUtils
import ir.amirab.util.compose.asStringSource
import ir.amirab.util.flow.combineStateFlows import ir.amirab.util.flow.combineStateFlows
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@ -39,13 +42,13 @@ private val activeQueuesFlow = downloadSystem
) )
val newDownloadAction = simpleAction( val newDownloadAction = simpleAction(
"New Download", Res.string.new_download.asStringSource(),
MyIcons.add, MyIcons.add,
) { ) {
appComponent.openAddDownloadDialog(listOf(DownloadCredentials.empty())) appComponent.openAddDownloadDialog(listOf(DownloadCredentials.empty()))
} }
val newDownloadFromClipboardAction = simpleAction( val newDownloadFromClipboardAction = simpleAction(
"Import from clipboard", Res.string.import_from_clipboard.asStringSource(),
MyIcons.paste, MyIcons.paste,
) { ) {
val contentsInClipboard = ClipboardUtil.read() val contentsInClipboard = ClipboardUtil.read()
@ -61,14 +64,14 @@ val newDownloadFromClipboardAction = simpleAction(
appComponent.openAddDownloadDialog(items) appComponent.openAddDownloadDialog(items)
} }
val batchDownloadAction = simpleAction( val batchDownloadAction = simpleAction(
title = "Batch Download", title = Res.string.batch_download.asStringSource(),
icon = MyIcons.download icon = MyIcons.download
) { ) {
appComponent.openBatchDownload() appComponent.openBatchDownload()
} }
val stopQueueGroupAction = MenuItem.SubMenu( val stopQueueGroupAction = MenuItem.SubMenu(
icon = MyIcons.stop, icon = MyIcons.stop,
title = "Stop Queue", title = Res.string.stop_queue.asStringSource(),
items = emptyList() items = emptyList()
).apply { ).apply {
activeQueuesFlow activeQueuesFlow
@ -82,7 +85,7 @@ val stopQueueGroupAction = MenuItem.SubMenu(
val startQueueGroupAction = MenuItem.SubMenu( val startQueueGroupAction = MenuItem.SubMenu(
icon = MyIcons.resume, icon = MyIcons.resume,
title = "Start Queue", title = Res.string.start_queue.asStringSource(),
items = emptyList() items = emptyList()
).apply { ).apply {
appComponent.downloadSystem.queueManager appComponent.downloadSystem.queueManager
@ -97,7 +100,7 @@ val startQueueGroupAction = MenuItem.SubMenu(
val stopAllAction = simpleAction( val stopAllAction = simpleAction(
"Stop All", Res.string.stop_all.asStringSource(),
MyIcons.stop, MyIcons.stop,
checkEnable = combineStateFlows( checkEnable = combineStateFlows(
downloadSystem.downloadMonitor.activeDownloadCount, downloadSystem.downloadMonitor.activeDownloadCount,
@ -113,19 +116,19 @@ val stopAllAction = simpleAction(
} }
val exitAction = simpleAction( val exitAction = simpleAction(
"Exit", Res.string.exit.asStringSource(),
MyIcons.exit, MyIcons.exit,
) { ) {
appComponent.requestClose() appComponent.requestClose()
} }
val browserIntegrations = MenuItem.SubMenu( val browserIntegrations = MenuItem.SubMenu(
title = "Download Browser Integration", title = Res.string.download_browser_integration.asStringSource(),
icon = MyIcons.download, icon = MyIcons.download,
items = buildMenu { items = buildMenu {
for (browserExtension in SharedConstants.browserIntegrations) { for (browserExtension in SharedConstants.browserIntegrations) {
item( item(
title = browserExtension.type.getName(), title = browserExtension.type.getName().asStringSource(),
icon = browserExtension.type.getIcon(), icon = browserExtension.type.getIcon(),
onClick = { UrlUtils.openUrl(browserExtension.url) } onClick = { UrlUtils.openUrl(browserExtension.url) }
) )
@ -134,13 +137,13 @@ val browserIntegrations = MenuItem.SubMenu(
) )
val gotoSettingsAction = simpleAction( val gotoSettingsAction = simpleAction(
"Settings", Res.string.settings.asStringSource(),
MyIcons.settings, MyIcons.settings,
) { ) {
appComponent.openSettings() appComponent.openSettings()
} }
val showDownloadList = simpleAction( val showDownloadList = simpleAction(
"Show Downloads", Res.string.show_downloads.asStringSource(),
MyIcons.settings, MyIcons.settings,
) { ) {
appComponent.openHome() appComponent.openHome()
@ -153,33 +156,33 @@ val showDownloadList = simpleAction(
appComponent.updater.requestCheckForUpdate() appComponent.updater.requestCheckForUpdate()
}*/ }*/
val openAboutAction = simpleAction( val openAboutAction = simpleAction(
title = "About", title = Res.string.about.asStringSource(),
icon = MyIcons.info, icon = MyIcons.info,
) { ) {
appComponent.openAbout() appComponent.openAbout()
} }
val openOpenSourceThirdPartyLibraries = simpleAction( val openOpenSourceThirdPartyLibraries = simpleAction(
title = "View OpenSource Libraries", title = Res.string.view_the_open_source_licenses.asStringSource(),
icon = MyIcons.openSource, icon = MyIcons.openSource,
) { ) {
appComponent.openOpenSourceLibraries() appComponent.openOpenSourceLibraries()
} }
val supportActionGroup = MenuItem.SubMenu( val supportActionGroup = MenuItem.SubMenu(
title = "Support & Community", title = Res.string.support_and_community.asStringSource(),
icon = MyIcons.group, icon = MyIcons.group,
items = buildMenu { items = buildMenu {
item("Website", MyIcons.appIcon) { item(Res.string.website.asStringSource(), MyIcons.appIcon) {
UrlUtils.openUrl(AppInfo.website) UrlUtils.openUrl(AppInfo.website)
} }
item("Source Code", MyIcons.openSource) { item(Res.string.source_code.asStringSource(), MyIcons.openSource) {
UrlUtils.openUrl(AppInfo.sourceCode) UrlUtils.openUrl(AppInfo.sourceCode)
} }
subMenu("Telegram", MyIcons.telegram) { subMenu(Res.string.telegram.asStringSource(), MyIcons.telegram) {
item("Channel", MyIcons.speaker) { item(Res.string.channel.asStringSource(), MyIcons.speaker) {
UrlUtils.openUrl(SharedConstants.telegramChannelUrl) UrlUtils.openUrl(SharedConstants.telegramChannelUrl)
} }
item("Group", MyIcons.group) { item(Res.string.group.asStringSource(), MyIcons.group) {
UrlUtils.openUrl(SharedConstants.telegramGroupUrl) UrlUtils.openUrl(SharedConstants.telegramGroupUrl)
} }
} }
@ -187,7 +190,7 @@ val supportActionGroup = MenuItem.SubMenu(
) )
val openQueuesAction = simpleAction( val openQueuesAction = simpleAction(
title = "Open Queues", title = Res.string.queues.asStringSource(),
icon = MyIcons.queue icon = MyIcons.queue
) { ) {
appComponent.openQueues() appComponent.openQueues()
@ -197,7 +200,7 @@ fun moveToQueueAction(
queue: DownloadQueue, queue: DownloadQueue,
itemId: List<Long>, itemId: List<Long>,
): AnAction { ): AnAction {
return simpleAction(queue.getQueueModel().name) { return simpleAction(queue.getQueueModel().name.asStringSource()) {
scope.launch { scope.launch {
downloadSystem downloadSystem
.queueManager .queueManager
@ -212,7 +215,7 @@ fun createMoveToCategoryAction(
category: Category, category: Category,
itemIds: List<Long>, itemIds: List<Long>,
): AnAction { ): AnAction {
return simpleAction(category.name) { return simpleAction(category.name.asStringSource()) {
scope.launch { scope.launch {
downloadSystem downloadSystem
.categoryManager .categoryManager
@ -227,7 +230,7 @@ fun createMoveToCategoryAction(
fun stopQueueAction( fun stopQueueAction(
queue: DownloadQueue, queue: DownloadQueue,
): AnAction { ): AnAction {
return simpleAction(queue.getQueueModel().name) { return simpleAction(queue.getQueueModel().name.asStringSource()) {
scope.launch { scope.launch {
queue.stop() queue.stop()
} }
@ -237,14 +240,14 @@ fun stopQueueAction(
fun startQueueAction( fun startQueueAction(
queue: DownloadQueue, queue: DownloadQueue,
): AnAction { ): AnAction {
return simpleAction(queue.getQueueModel().name) { return simpleAction(queue.getQueueModel().name.asStringSource()) {
scope.launch { scope.launch {
queue.start() queue.start()
} }
} }
} }
val newQueueAction = simpleAction("New Queue") { val newQueueAction = simpleAction(Res.string.add_new_queue.asStringSource()) {
scope.launch { scope.launch {
appComponent.openNewQueueDialog() appComponent.openNewQueueDialog()
} }

View File

@ -45,6 +45,8 @@ import com.abdownloadmanager.utils.proxy.ProxyManager
import ir.amirab.downloader.connection.proxy.ProxyStrategyProvider import ir.amirab.downloader.connection.proxy.ProxyStrategyProvider
import ir.amirab.downloader.monitor.IDownloadMonitor import ir.amirab.downloader.monitor.IDownloadMonitor
import ir.amirab.downloader.utils.EmptyFileCreator import ir.amirab.downloader.utils.EmptyFileCreator
import ir.amirab.util.compose.localizationmanager.LanguageManager
import ir.amirab.util.compose.localizationmanager.LanguageStorage
import ir.amirab.util.config.datastore.kotlinxSerializationDataStore import ir.amirab.util.config.datastore.kotlinxSerializationDataStore
val downloaderModule = module { val downloaderModule = module {
@ -222,6 +224,9 @@ val appModule = module {
single { single {
ThemeManager(get(), get()) ThemeManager(get(), get())
} }
single {
LanguageManager(get())
}
single { single {
MyIcons MyIcons
}.bind<IMyIcons>() }.bind<IMyIcons>()
@ -241,7 +246,7 @@ val appModule = module {
get(), get(),
) )
) )
} }.bind<LanguageStorage>()
single { single {
PageStatesStorage( PageStatesStorage(
createMapConfigDatastore( createMapConfigDatastore(

View File

@ -9,6 +9,9 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.rememberWindowState import androidx.compose.ui.window.rememberWindowState
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import ir.amirab.util.compose.resources.myStringResource
@Composable @Composable
fun ShowAboutDialog(appComponent: AppComponent) { fun ShowAboutDialog(appComponent: AppComponent) {
@ -37,7 +40,7 @@ fun AboutDialog(
), ),
onCloseRequest = onClose onCloseRequest = onClose
) { ) {
WindowTitle("About") WindowTitle(myStringResource(Res.string.about))
AboutPage( AboutPage(
close = onClose, close = onClose,
onRequestShowOpenSourceLibraries = onRequestShowOpenSourceLibraries onRequestShowOpenSourceLibraries = onRequestShowOpenSourceLibraries

View File

@ -29,8 +29,12 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.abdownloadmanager.desktop.App
import com.abdownloadmanager.utils.compose.widget.MyIcon import com.abdownloadmanager.utils.compose.widget.MyIcon
import com.abdownloadmanager.desktop.ui.util.ifThen import com.abdownloadmanager.desktop.ui.util.ifThen
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import ir.amirab.util.compose.resources.myStringResource
@Composable @Composable
fun AboutPage( fun AboutPage(
@ -44,7 +48,7 @@ fun AboutPage(
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
Row(Modifier.fillMaxWidth().wrapContentWidth(Alignment.End)) { Row(Modifier.fillMaxWidth().wrapContentWidth(Alignment.End)) {
ActionButton( ActionButton(
"Close", myStringResource(Res.string.close),
onClick = close onClick = close
) )
} }
@ -57,8 +61,7 @@ fun RenderAppInfo(
) { ) {
Row( Row(
Modifier.fillMaxWidth() Modifier.fillMaxWidth()
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp),
,
) { ) {
ProvideTextStyle( ProvideTextStyle(
TextStyle(fontSize = myTextSizes.base) TextStyle(fontSize = myTextSizes.base)
@ -82,23 +85,30 @@ fun RenderAppInfo(
) )
Spacer(Modifier.height(2.dp)) Spacer(Modifier.height(2.dp))
WithContentAlpha(0.75f) { WithContentAlpha(0.75f) {
Text("version ${AppInfo.version}", fontSize = myTextSizes.base) Text(
myStringResource(
Res.string.version_n,
Res.string.version_n_createArgs(
value = AppInfo.version.toString()
)
), fontSize = myTextSizes.base
)
} }
} }
} }
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
WithContentAlpha(1f) { WithContentAlpha(1f) {
Text("Developed with ❤️ for you") Text(myStringResource(Res.string.developed_with_love_for_you))
LinkText("Visit the project website", AppInfo.website) LinkText(myStringResource(Res.string.visit_the_project_website), AppInfo.website)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Text("This is a free & Open Source software") Text(myStringResource(Res.string.this_is_a_free_and_open_source_software))
LinkText("See the Source Code", AppInfo.sourceCode) LinkText(myStringResource(Res.string.view_the_source_code), AppInfo.sourceCode)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Text("Powered by Open Source Libraries") Text(myStringResource(Res.string.powered_by_open_source_software))
Text( Text(
"See the Open Sources libraries", myStringResource(Res.string.view_the_open_source_licenses),
style = LocalTextStyle.current.merge(LinkStyle), style = LocalTextStyle.current.merge(LinkStyle),
modifier = Modifier.clickable { modifier = Modifier.clickable {
onRequestShowOpenSourceLibraries() onRequestShowOpenSourceLibraries()
@ -135,12 +145,11 @@ fun LinkText(
Text( Text(
text = text, text = text,
style = LocalTextStyle.current style = LocalTextStyle.current
.merge(LinkStyle).ifThen(isHovered){ .merge(LinkStyle).ifThen(isHovered) {
copy( copy(
textDecoration = TextDecoration.Underline textDecoration = TextDecoration.Underline
) )
} },
,
overflow = overflow, overflow = overflow,
maxLines = maxLines, maxLines = maxLines,
) )

View File

@ -19,6 +19,9 @@ import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.rememberWindowState import androidx.compose.ui.window.rememberWindowState
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import ir.amirab.util.compose.resources.myStringResource
import java.awt.Dimension import java.awt.Dimension
@Composable @Composable
@ -53,7 +56,7 @@ fun ShowAddDownloadDialogs(component: AddDownloadDialogManager) {
window.minimumSize = Dimension(w, h) window.minimumSize = Dimension(w, h)
} }
// BringToFront() // BringToFront()
WindowTitle("Add download") WindowTitle(myStringResource(Res.string.add_download))
WindowIcon(MyIcons.appIcon) WindowIcon(MyIcons.appIcon)
AddDownloadPage(addDownloadComponent) AddDownloadPage(addDownloadComponent)
} }
@ -76,7 +79,7 @@ fun ShowAddDownloadDialogs(component: AddDownloadDialogManager) {
window.minimumSize = Dimension(w, h) window.minimumSize = Dimension(w, h)
} }
// BringToFront() // BringToFront()
WindowTitle("Add download") WindowTitle(myStringResource(Res.string.add_download))
WindowIcon(MyIcons.appIcon) WindowIcon(MyIcons.appIcon)
AddMultiItemPage(addDownloadComponent) AddMultiItemPage(addDownloadComponent)
} }

View File

@ -30,12 +30,15 @@ import com.abdownloadmanager.desktop.ui.icon.MyIcons
import com.abdownloadmanager.desktop.ui.theme.myColors import com.abdownloadmanager.desktop.ui.theme.myColors
import com.abdownloadmanager.desktop.ui.util.ifThen import com.abdownloadmanager.desktop.ui.util.ifThen
import com.abdownloadmanager.desktop.ui.widget.CheckBox import com.abdownloadmanager.desktop.ui.widget.CheckBox
import com.abdownloadmanager.desktop.ui.widget.Help
import com.abdownloadmanager.desktop.utils.div import com.abdownloadmanager.desktop.utils.div
import com.abdownloadmanager.desktop.utils.windowUtil.moveSafe import com.abdownloadmanager.desktop.utils.windowUtil.moveSafe
import com.abdownloadmanager.utils.category.CategorySelectionMode import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import com.abdownloadmanager.utils.compose.WithContentColor import com.abdownloadmanager.utils.compose.WithContentColor
import com.abdownloadmanager.utils.compose.widget.MyIcon import com.abdownloadmanager.utils.compose.widget.MyIcon
import ir.amirab.util.compose.resources.myStringResource
import ir.amirab.util.compose.StringSource
import ir.amirab.util.compose.asStringSource
import java.awt.MouseInfo import java.awt.MouseInfo
@Composable @Composable
@ -51,7 +54,7 @@ fun AddMultiItemPage(
) { ) {
WithContentAlpha(1f) { WithContentAlpha(1f) {
Text( Text(
"Select Items you want to pick up for download", myStringResource(Res.string.add_multi_download_page_header),
fontSize = myTextSizes.base fontSize = myTextSizes.base
) )
} }
@ -109,7 +112,7 @@ fun Footer(
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Row(Modifier.align(Alignment.Bottom)) { Row(Modifier.align(Alignment.Bottom)) {
ActionButton( ActionButton(
text = "Add", text = myStringResource(Res.string.add),
onClick = { onClick = {
component.openAddToQueueDialog() component.openAddToQueueDialog()
}, },
@ -118,7 +121,7 @@ fun Footer(
) )
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
ActionButton( ActionButton(
text = "Cancel", text = myStringResource(Res.string.cancel),
onClick = { onClick = {
component.requestClose() component.requestClose()
}, },
@ -139,7 +142,7 @@ private fun SaveSettings(
) { ) {
var dropdownOpen by remember { mutableStateOf(false) } var dropdownOpen by remember { mutableStateOf(false) }
val saveMode by component.saveMode.collectAsState() val saveMode by component.saveMode.collectAsState()
Text("Save to:") Text("${myStringResource(Res.string.save_to)}:")
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
SaveSolution( SaveSolution(
saveMode = saveMode, saveMode = saveMode,
@ -275,14 +278,14 @@ private fun SaveSolutionPopup(
Modifier.padding(16.dp) Modifier.padding(16.dp)
) { ) {
Text( Text(
"Where should each item saved?", myStringResource(Res.string.where_should_each_item_saved),
Modifier, Modifier,
fontSize = myTextSizes.base fontSize = myTextSizes.base
) )
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
WithContentAlpha(0.75f) { WithContentAlpha(0.75f) {
Text( Text(
"There are multiple items! please select a way you want to save them", myStringResource(Res.string.there_are_multiple_items_please_select_a_way_you_want_to_save_them),
Modifier, Modifier,
fontSize = myTextSizes.sm, fontSize = myTextSizes.sm,
) )
@ -304,8 +307,8 @@ private fun SaveSolutionPopup(
Column { Column {
for (item in AddMultiItemSaveMode.entries) { for (item in AddMultiItemSaveMode.entries) {
SaveSolutionItem( SaveSolutionItem(
title = item.title, title = item.title.rememberString(),
description = item.description, description = item.description.rememberString(),
isSelected = selectedItem == item, isSelected = selectedItem == item,
onClick = { onClick = {
onIteSelected(item) onIteSelected(item)
@ -348,7 +351,7 @@ private fun SaveSolutionHeader(
.padding(vertical = 8.dp) .padding(vertical = 8.dp)
.weight(1f) .weight(1f)
Text( Text(
saveMode.title, saveMode.title.rememberString(),
contentModifier, contentModifier,
) )
Spacer( Spacer(
@ -434,24 +437,24 @@ private fun AllFilesInSameDirectory(
onValueChange = setAlsoCategorize onValueChange = setAlsoCategorize
) )
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
Text("Auto categorize") Text(myStringResource(Res.string.auto_categorize_downloads))
} }
} }
enum class AddMultiItemSaveMode( enum class AddMultiItemSaveMode(
val title: String, val title: StringSource,
val description: String, val description: StringSource,
) { ) {
EachFileInTheirOwnCategory( EachFileInTheirOwnCategory(
title = "Each item on its own category", title = Res.string.each_item_on_its_own_category.asStringSource(),
description = "Each item will be placed in a category that have that file type", description = Res.string.each_item_on_its_own_category_description.asStringSource(),
), ),
AllInOneCategory( AllInOneCategory(
title = "All items in one Category", title = Res.string.all_items_in_one_category.asStringSource(),
description = "All files will be saved in the selected category location", description = Res.string.all_items_in_one_category_description.asStringSource(),
), ),
InSameLocation( InSameLocation(
title = "All items in one Location", title = Res.string.all_items_in_one_Location.asStringSource(),
description = "All items will be saved in the selected directory", description = Res.string.all_items_in_one_Location_description.asStringSource(),
); );
} }

View File

@ -25,8 +25,11 @@ import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.pointer.isShiftPressed import androidx.compose.ui.input.pointer.isShiftPressed
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.utils.FileIconProvider import com.abdownloadmanager.utils.FileIconProvider
import com.abdownloadmanager.utils.compose.widget.MyIcon import com.abdownloadmanager.utils.compose.widget.MyIcon
import ir.amirab.util.compose.StringSource
import ir.amirab.util.compose.asStringSource
@Composable @Composable
fun AddMultiDownloadTable( fun AddMultiDownloadTable(
@ -210,7 +213,8 @@ sealed class AddMultiItemTableCells : TableCell<DownloadUiChecker> {
data object Check : AddMultiItemTableCells(), data object Check : AddMultiItemTableCells(),
CustomCellRenderer { CustomCellRenderer {
override val name: String = "#" override val id: String = "#"
override val name: StringSource = "#".asStringSource()
override val size: CellSize = CellSize.Fixed(26.dp) override val size: CellSize = CellSize.Fixed(26.dp)
@Composable @Composable
@ -225,17 +229,20 @@ sealed class AddMultiItemTableCells : TableCell<DownloadUiChecker> {
} }
data object Name : AddMultiItemTableCells() { data object Name : AddMultiItemTableCells() {
override val name: String = "Name" override val id: String = "Name"
override val name: StringSource = Res.string.name.asStringSource()
override val size: CellSize = CellSize.Resizeable(120.dp..1000.dp, 350.dp) override val size: CellSize = CellSize.Resizeable(120.dp..1000.dp, 350.dp)
} }
data object Link : AddMultiItemTableCells() { data object Link : AddMultiItemTableCells() {
override val name: String = "Link" override val id: String = "Link"
override val name: StringSource = Res.string.link.asStringSource()
override val size: CellSize = CellSize.Resizeable(120.dp..2000.dp, 240.dp) override val size: CellSize = CellSize.Resizeable(120.dp..2000.dp, 240.dp)
} }
data object SizeCell : AddMultiItemTableCells() { data object SizeCell : AddMultiItemTableCells() {
override val name: String = "Size" override val id: String = "Size"
override val name: StringSource = Res.string.size.asStringSource()
override val size: CellSize = CellSize.Resizeable(100.dp..180.dp, 100.dp) override val size: CellSize = CellSize.Resizeable(100.dp..180.dp, 100.dp)
} }
} }
@ -289,7 +296,7 @@ private fun SizeCell(
val length by downloadChecker.length.collectAsState() val length by downloadChecker.length.collectAsState()
CellText( CellText(
length?.let { length?.let {
convertSizeToHumanReadable(it) convertSizeToHumanReadable(it).rememberString()
} ?: "" } ?: ""
) )
} }

View File

@ -29,8 +29,11 @@ import com.abdownloadmanager.desktop.ui.util.ifThen
import com.abdownloadmanager.desktop.ui.widget.Text import com.abdownloadmanager.desktop.ui.widget.Text
import com.abdownloadmanager.desktop.utils.div import com.abdownloadmanager.desktop.utils.div
import com.abdownloadmanager.desktop.utils.windowUtil.moveSafe import com.abdownloadmanager.desktop.utils.windowUtil.moveSafe
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import com.abdownloadmanager.utils.compose.WithContentAlpha import com.abdownloadmanager.utils.compose.WithContentAlpha
import com.abdownloadmanager.utils.compose.widget.MyIcon import com.abdownloadmanager.utils.compose.widget.MyIcon
import ir.amirab.util.compose.resources.myStringResource
import java.awt.MouseInfo import java.awt.MouseInfo
@Composable @Composable
@ -208,7 +211,7 @@ private fun <T> DropDownHeader(
} }
} else { } else {
Text( Text(
"No Category Selected", myStringResource(Res.string.no_category_selected),
contentModifier contentModifier
) )
} }

View File

@ -20,6 +20,9 @@ import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import ir.amirab.util.compose.resources.myStringResource
import io.github.vinceglb.filekit.compose.rememberDirectoryPickerLauncher import io.github.vinceglb.filekit.compose.rememberDirectoryPickerLauncher
import io.github.vinceglb.filekit.core.FileKitPlatformSettings import io.github.vinceglb.filekit.core.FileKitPlatformSettings
import ir.amirab.util.desktop.LocalWindow import ir.amirab.util.desktop.LocalWindow
@ -36,7 +39,7 @@ fun LocationTextField(
var showLastUsedLocations by remember { mutableStateOf(false) } var showLastUsedLocations by remember { mutableStateOf(false) }
val downloadLauncherFolderPickerLauncher = rememberDirectoryPickerLauncher( val downloadLauncherFolderPickerLauncher = rememberDirectoryPickerLauncher(
title = "Download Location", title = myStringResource(Res.string.download_location),
initialDirectory = remember(text) { initialDirectory = remember(text) {
runCatching { runCatching {
File(text).canonicalPath File(text).canonicalPath
@ -57,7 +60,7 @@ fun LocationTextField(
AddDownloadPageTextField( AddDownloadPageTextField(
text, text,
setText, setText,
"Location", myStringResource(Res.string.location),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.onGloballyPositioned { .onGloballyPositioned {

View File

@ -24,6 +24,9 @@ import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.rememberDialogState import androidx.compose.ui.window.rememberDialogState
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import ir.amirab.util.compose.resources.myStringResource
import ir.amirab.downloader.queue.DownloadQueue import ir.amirab.downloader.queue.DownloadQueue
import java.awt.MouseInfo import java.awt.MouseInfo
@ -82,7 +85,7 @@ fun ShowAddToQueueDialog(
) { ) {
WindowDraggableArea(Modifier.fillMaxWidth()) { WindowDraggableArea(Modifier.fillMaxWidth()) {
Text( Text(
"Choose Queue to add", myStringResource(Res.string.select_queue),
modifier = Modifier modifier = Modifier
.padding(vertical = 8.dp) .padding(vertical = 8.dp)
.fillMaxWidth() .fillMaxWidth()
@ -138,11 +141,11 @@ fun ShowAddToQueueDialog(
){ ){
IconActionButton( IconActionButton(
MyIcons.add, MyIcons.add,
contentDescription = "Add new queue", contentDescription = myStringResource(Res.string.add_new_queue),
onClick = newQueueAction onClick = newQueueAction
) )
ActionButton( ActionButton(
text = "Without Queue", text = myStringResource(Res.string.without_queue),
modifier = Modifier, modifier = Modifier,
onClick = { onClick = {
onQueueSelected(null) onQueueSelected(null)

View File

@ -32,8 +32,12 @@ import androidx.compose.ui.unit.*
import androidx.compose.ui.window.* import androidx.compose.ui.window.*
import com.abdownloadmanager.desktop.pages.addDownload.shared.* import com.abdownloadmanager.desktop.pages.addDownload.shared.*
import com.abdownloadmanager.desktop.utils.mvi.HandleEffects import com.abdownloadmanager.desktop.utils.mvi.HandleEffects
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import com.abdownloadmanager.utils.category.rememberIconPainter import com.abdownloadmanager.utils.category.rememberIconPainter
import ir.amirab.util.compose.resources.myStringResource
import ir.amirab.downloader.utils.OnDuplicateStrategy import ir.amirab.downloader.utils.OnDuplicateStrategy
import ir.amirab.util.compose.asStringSource
import java.awt.MouseInfo import java.awt.MouseInfo
@Composable @Composable
@ -96,7 +100,7 @@ fun AddDownloadPage(
onValueChange = { component.setUseCategory(it) } onValueChange = { component.setUseCategory(it) }
) )
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
Text("Use Category") Text(myStringResource(Res.string.use_category))
} }
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
CategorySelect( CategorySelect(
@ -125,7 +129,7 @@ fun AddDownloadPage(
component.setFolder(it) component.setFolder(it)
}, },
errorText = when (canAddResult) { errorText = when (canAddResult) {
CanAddResult.CantWriteInThisFolder -> "Can't write to this folder" CanAddResult.CantWriteInThisFolder -> myStringResource(Res.string.cant_write_to_this_folder)
else -> null else -> null
}, },
lastUsedLocations = component.lastUsedLocations.collectAsState().value lastUsedLocations = component.lastUsedLocations.collectAsState().value
@ -140,13 +144,13 @@ fun AddDownloadPage(
errorText = when (canAddResult) { errorText = when (canAddResult) {
is CanAddResult.DownloadAlreadyExists -> { is CanAddResult.DownloadAlreadyExists -> {
if (onDuplicateStrategy == null) { if (onDuplicateStrategy == null) {
"File name already exists" myStringResource(Res.string.file_name_already_exists)
} else { } else {
null null
} }
} }
CanAddResult.InvalidFileName -> "Invalid filename" CanAddResult.InvalidFileName -> myStringResource(Res.string.invalid_file_name)
else -> null else -> null
}.takeIf { name.isNotEmpty() } }.takeIf { name.isNotEmpty() }
) )
@ -237,14 +241,14 @@ private fun ShowSolutionsOnDuplicateDownload(component: AddSingleDownloadCompone
Modifier.padding(16.dp) Modifier.padding(16.dp)
) { ) {
Text( Text(
"Select a solution", myStringResource(Res.string.select_a_solution),
Modifier, Modifier,
fontSize = myTextSizes.base fontSize = myTextSizes.base
) )
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
WithContentAlpha(0.75f) { WithContentAlpha(0.75f) {
Text( Text(
"The link you provided is already in download lists please specify what you want to do", myStringResource(Res.string.select_download_strategy_description),
Modifier, Modifier,
fontSize = myTextSizes.sm, fontSize = myTextSizes.sm,
) )
@ -262,24 +266,24 @@ private fun ShowSolutionsOnDuplicateDownload(component: AddSingleDownloadCompone
Column { Column {
OnDuplicateStrategySolutionItem( OnDuplicateStrategySolutionItem(
isSelected = onDuplicateStrategy == OnDuplicateStrategy.AddNumbered, isSelected = onDuplicateStrategy == OnDuplicateStrategy.AddNumbered,
title = "Add a numbered file", title = myStringResource(Res.string.download_strategy_add_a_numbered_file),
description = "Add an index after the end of download file name", description = myStringResource(Res.string.download_strategy_add_a_numbered_file_description),
) { ) {
component.setOnDuplicateStrategy(OnDuplicateStrategy.AddNumbered) component.setOnDuplicateStrategy(OnDuplicateStrategy.AddNumbered)
close() close()
} }
OnDuplicateStrategySolutionItem( OnDuplicateStrategySolutionItem(
isSelected = onDuplicateStrategy == OnDuplicateStrategy.OverrideDownload, isSelected = onDuplicateStrategy == OnDuplicateStrategy.OverrideDownload,
title = "Override existing file", title = myStringResource(Res.string.download_strategy_override_existing_file),
description = "Remove existing download and write to that file", description = myStringResource(Res.string.download_strategy_override_existing_file_description),
) { ) {
component.setOnDuplicateStrategy(OnDuplicateStrategy.OverrideDownload) component.setOnDuplicateStrategy(OnDuplicateStrategy.OverrideDownload)
close() close()
} }
OnDuplicateStrategySolutionItem( OnDuplicateStrategySolutionItem(
isSelected = null, isSelected = null,
title = "Show downloaded file", title = myStringResource(Res.string.download_strategy_show_downloaded_file),
description = "Show already existing download item , so you can press on resume or open it", description = myStringResource(Res.string.download_strategy_show_downloaded_file_description),
) { ) {
component.openDownloadFileForCurrentLink() component.openDownloadFileForCurrentLink()
close() close()
@ -432,13 +436,13 @@ private fun PrimaryMainConfigActionButton(
fun ConfigActionsButtons(component: AddSingleDownloadComponent) { fun ConfigActionsButtons(component: AddSingleDownloadComponent) {
val responseInfo by component.linkResponseInfo.collectAsState() val responseInfo by component.linkResponseInfo.collectAsState()
Row { Row {
IconActionButton(MyIcons.refresh, "Refresh") { IconActionButton(MyIcons.refresh, myStringResource(Res.string.refresh)) {
component.refresh() component.refresh()
} }
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
IconActionButton( IconActionButton(
MyIcons.settings, MyIcons.settings,
"Settings", myStringResource(Res.string.settings),
indicateActive = component.showMoreSettings, indicateActive = component.showMoreSettings,
requiresAttention = responseInfo?.requireBasicAuth ?: false requiresAttention = responseInfo?.requireBasicAuth ?: false
) { ) {
@ -454,14 +458,14 @@ private fun MainActionButtons(component: AddSingleDownloadComponent) {
val canAddResult by component.canAddResult.collectAsState() val canAddResult by component.canAddResult.collectAsState()
if (canAddResult is CanAddResult.DownloadAlreadyExists && onDuplicateStrategy == null) { if (canAddResult is CanAddResult.DownloadAlreadyExists && onDuplicateStrategy == null) {
MainConfigActionButton( MainConfigActionButton(
text = "Show solutions...", text = myStringResource(Res.string.show_solutions),
modifier = Modifier, modifier = Modifier,
onClick = { component.showSolutionsOnDuplicateDownloadUi = true }, onClick = { component.showSolutionsOnDuplicateDownloadUi = true },
) )
if (component.shouldShowOpenFile.collectAsState().value) { if (component.shouldShowOpenFile.collectAsState().value) {
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
MainConfigActionButton( MainConfigActionButton(
text = "Open File", text = myStringResource(Res.string.open_file),
modifier = Modifier, modifier = Modifier,
onClick = { component.openExistingFile() }, onClick = { component.openExistingFile() },
) )
@ -469,7 +473,7 @@ private fun MainActionButtons(component: AddSingleDownloadComponent) {
} else { } else {
val canAddToDownloads by component.canAddToDownloads.collectAsState() val canAddToDownloads by component.canAddToDownloads.collectAsState()
MainConfigActionButton( MainConfigActionButton(
text = "Add", text = myStringResource(Res.string.add),
modifier = Modifier, modifier = Modifier,
enabled = canAddToDownloads, enabled = canAddToDownloads,
onClick = { onClick = {
@ -478,7 +482,7 @@ private fun MainActionButtons(component: AddSingleDownloadComponent) {
) )
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
PrimaryMainConfigActionButton( PrimaryMainConfigActionButton(
text = "Download", text = myStringResource(Res.string.download),
modifier = Modifier, modifier = Modifier,
enabled = canAddToDownloads, enabled = canAddToDownloads,
onClick = { onClick = {
@ -488,7 +492,7 @@ private fun MainActionButtons(component: AddSingleDownloadComponent) {
if (onDuplicateStrategy != null) { if (onDuplicateStrategy != null) {
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
MainConfigActionButton( MainConfigActionButton(
text = "Change solution", text = myStringResource(Res.string.change_solution),
modifier = Modifier, modifier = Modifier,
onClick = { component.showSolutionsOnDuplicateDownloadUi = true }, onClick = { component.showSolutionsOnDuplicateDownloadUi = true },
) )
@ -499,7 +503,7 @@ private fun MainActionButtons(component: AddSingleDownloadComponent) {
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
MainConfigActionButton( MainConfigActionButton(
text = "Cancel", text = myStringResource(Res.string.cancel),
modifier = Modifier, modifier = Modifier,
onClick = { onClick = {
component.onRequestClose() component.onRequestClose()
@ -558,10 +562,10 @@ fun RenderFileTypeAndSize(
}.takeIf { }.takeIf {
// this is a length of a html page (error) // this is a length of a html page (error)
fileInfo.isSuccessFul fileInfo.isSuccessFul
} ?: "unknown" } ?: Res.string.unknown.asStringSource()
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text( Text(
size, size.rememberString(),
fontSize = myTextSizes.sm, fontSize = myTextSizes.sm,
) )
} else { } else {
@ -609,7 +613,7 @@ private fun UrlTextField(
AddDownloadPageTextField( AddDownloadPageTextField(
text, text,
setText, setText,
"Download link", myStringResource(Res.string.download_link),
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
end = { end = {
MyTextFieldIcon(MyIcons.paste) { MyTextFieldIcon(MyIcons.paste) {
@ -632,7 +636,7 @@ private fun NameTextField(
AddDownloadPageTextField( AddDownloadPageTextField(
text, text,
setText, setText,
"Name", myStringResource(Res.string.name),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
errorText = errorText, errorText = errorText,
) )

View File

@ -10,6 +10,8 @@ import com.abdownloadmanager.desktop.utils.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects
import com.abdownloadmanager.desktop.utils.mvi.supportEffects import com.abdownloadmanager.desktop.utils.mvi.supportEffects
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import com.abdownloadmanager.utils.extractors.linkextractor.DownloadCredentialFromStringExtractor import com.abdownloadmanager.utils.extractors.linkextractor.DownloadCredentialFromStringExtractor
import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.ComponentContext
import ir.amirab.downloader.connection.DownloaderClient import ir.amirab.downloader.connection.DownloaderClient
@ -31,6 +33,8 @@ import com.abdownloadmanager.utils.FileIconProvider
import com.abdownloadmanager.utils.category.Category import com.abdownloadmanager.utils.category.Category
import com.abdownloadmanager.utils.category.CategoryItem import com.abdownloadmanager.utils.category.CategoryItem
import com.abdownloadmanager.utils.category.CategoryManager import com.abdownloadmanager.utils.category.CategoryManager
import ir.amirab.util.compose.asStringSource
import ir.amirab.util.compose.asStringSourceWithARgs
sealed interface AddSingleDownloadPageEffects { sealed interface AddSingleDownloadPageEffects {
data class SuggestUrl(val link: String) : AddSingleDownloadPageEffects data class SuggestUrl(val link: String) : AddSingleDownloadPageEffects
@ -235,17 +239,17 @@ class AddSingleDownloadComponent(
val configurables = listOf( val configurables = listOf(
SpeedLimitConfigurable( SpeedLimitConfigurable(
"Speed Limit", Res.string.download_item_settings_speed_limit.asStringSource(),
"Limit the speed of download for this file", Res.string.download_item_settings_speed_limit_description.asStringSource(),
backedBy = speedLimit, backedBy = speedLimit,
describe = { describe = {
if (it == 0L) "Unlimited" if (it == 0L) Res.string.unlimited.asStringSource()
else convertSpeedToHumanReadable(it) else convertSpeedToHumanReadable(it).asStringSource()
} }
), ),
IntConfigurable( IntConfigurable(
"Thread count", Res.string.settings_download_thread_count.asStringSource(),
"Limit the threads of download for this file", Res.string.settings_download_thread_count_description.asStringSource(),
backedBy = threadCount.mapTwoWayStateFlow( backedBy = threadCount.mapTwoWayStateFlow(
map = { map = {
it ?: 0 it ?: 0
@ -256,13 +260,18 @@ class AddSingleDownloadComponent(
), ),
range = 0..32, range = 0..32,
describe = { describe = {
if (it == 0) "use Global setting" if (it == 0) Res.string.use_global_settings.asStringSource()
else "$it thread for this download" else Res.string.download_item_settings_thread_count_describe
.asStringSourceWithARgs(
Res.string.download_item_settings_thread_count_describe_createArgs(
count = it.toString()
)
)
} }
), ),
StringConfigurable( StringConfigurable(
"Username", Res.string.username.asStringSource(),
"username if the link is a protected resource", Res.string.download_item_settings_username_description.asStringSource(),
backedBy = createMutableStateFlowFromStateFlow( backedBy = createMutableStateFlowFromStateFlow(
flow = credentials.mapStateFlow { flow = credentials.mapStateFlow {
it.username.orEmpty() it.username.orEmpty()
@ -272,12 +281,12 @@ class AddSingleDownloadComponent(
}, scope }, scope
), ),
describe = { describe = {
"" "".asStringSource()
} }
), ),
StringConfigurable( StringConfigurable(
"Password", Res.string.password.asStringSource(),
"Password if the link is a protected resource", Res.string.download_item_settings_password_description.asStringSource(),
backedBy = createMutableStateFlowFromStateFlow( backedBy = createMutableStateFlowFromStateFlow(
flow = credentials.mapStateFlow { flow = credentials.mapStateFlow {
it.password.orEmpty() it.password.orEmpty()
@ -287,7 +296,7 @@ class AddSingleDownloadComponent(
}, scope }, scope
), ),
describe = { describe = {
"" "".asStringSource()
} }
), ),
) )

View File

@ -27,16 +27,21 @@ import com.abdownloadmanager.desktop.ui.util.ifThen
import com.abdownloadmanager.desktop.ui.widget.* import com.abdownloadmanager.desktop.ui.widget.*
import com.abdownloadmanager.desktop.utils.ClipboardUtil import com.abdownloadmanager.desktop.utils.ClipboardUtil
import com.abdownloadmanager.desktop.utils.div import com.abdownloadmanager.desktop.utils.div
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import com.abdownloadmanager.utils.compose.LocalContentColor import com.abdownloadmanager.utils.compose.LocalContentColor
import com.abdownloadmanager.utils.compose.WithContentAlpha import com.abdownloadmanager.utils.compose.WithContentAlpha
import com.abdownloadmanager.utils.compose.widget.MyIcon import com.abdownloadmanager.utils.compose.widget.MyIcon
import ir.amirab.util.compose.resources.myStringResource
import ir.amirab.util.compose.IconSource import ir.amirab.util.compose.IconSource
import ir.amirab.util.compose.StringSource
import ir.amirab.util.compose.asStringSource
@Composable @Composable
fun BatchDownload( fun BatchDownload(
component: BatchDownloadComponent, component: BatchDownloadComponent,
) { ) {
WindowTitle("Batch Download") WindowTitle(myStringResource(Res.string.batch_download))
val link by component.link.collectAsState() val link by component.link.collectAsState()
val setLink = component::setLink val setLink = component::setLink
val start by component.start.collectAsState() val start by component.start.collectAsState()
@ -59,13 +64,13 @@ fun BatchDownload(
) { ) {
LabeledContent( LabeledContent(
label = { label = {
Text("Enter a link that contains wildcards (use *)") Text(myStringResource(Res.string.batch_download_link_help))
}, },
content = { content = {
BatchDownloadPageTextField( BatchDownloadPageTextField(
text = link, text = link,
onTextChange = setLink, onTextChange = setLink,
placeholder = "Link: https://example.com/photo-*.png", placeholder = "https://example.com/photo-*.png",
modifier = Modifier modifier = Modifier
.focusRequester(linkFocusRequester) .focusRequester(linkFocusRequester)
.fillMaxWidth(), .fillMaxWidth(),
@ -82,10 +87,15 @@ fun BatchDownload(
}, },
errorText = when (val v = validationResult) { errorText = when (val v = validationResult) {
BatchDownloadValidationResult.URLInvalid -> { BatchDownloadValidationResult.URLInvalid -> {
"Invalid URL" myStringResource(Res.string.invalid_url)
} }
is BatchDownloadValidationResult.MaxRangeExceed -> "List is too large! maximum ${v.allowed} items allowed" is BatchDownloadValidationResult.MaxRangeExceed -> myStringResource(
Res.string.list_is_too_large_maximum_n_items_allowed,
Res.string.list_is_too_large_maximum_n_items_allowed_createArgs(
count = v.allowed.toString()
)
)
BatchDownloadValidationResult.Others -> null BatchDownloadValidationResult.Others -> null
BatchDownloadValidationResult.Ok -> null BatchDownloadValidationResult.Ok -> null
} }
@ -95,7 +105,7 @@ fun BatchDownload(
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
LabeledContent( LabeledContent(
label = { label = {
Text("Enter range") Text(myStringResource(Res.string.enter_range))
}, },
content = { content = {
Row( Row(
@ -107,7 +117,7 @@ fun BatchDownload(
placeholder = "", placeholder = "",
modifier = Modifier.width(90.dp), modifier = Modifier.width(90.dp),
start = { start = {
Text("From:", Modifier.padding(horizontal = 8.dp)) Text("${myStringResource(Res.string.from)}:", Modifier.padding(horizontal = 8.dp))
} }
) )
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
@ -120,7 +130,7 @@ fun BatchDownload(
placeholder = "", placeholder = "",
modifier = Modifier.width(90.dp), modifier = Modifier.width(90.dp),
start = { start = {
Text("To:", Modifier.padding(horizontal = 8.dp)) Text("${myStringResource(Res.string.to)}:", Modifier.padding(horizontal = 8.dp))
} }
) )
} }
@ -129,7 +139,7 @@ fun BatchDownload(
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
LabeledContent( LabeledContent(
label = { label = {
Text("Wildcard length") Text(myStringResource(Res.string.wildcard_length))
}, },
content = { content = {
WildcardLengthUi( WildcardLengthUi(
@ -152,7 +162,7 @@ fun BatchDownload(
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
LabeledContent( LabeledContent(
label = { label = {
Text("First Link") Text(myStringResource(Res.string.first_link))
}, },
content = { content = {
LinkPreview(component.startLinkResult.collectAsState().value) LinkPreview(component.startLinkResult.collectAsState().value)
@ -161,7 +171,7 @@ fun BatchDownload(
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
LabeledContent( LabeledContent(
label = { label = {
Text("Last Link") Text(myStringResource(Res.string.last_link))
}, },
content = { content = {
LinkPreview(component.endLinkResult.collectAsState().value) LinkPreview(component.endLinkResult.collectAsState().value)
@ -175,12 +185,12 @@ fun BatchDownload(
modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End) modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End)
) { ) {
ActionButton( ActionButton(
text = "OK", text = myStringResource(Res.string.ok),
enabled = component.canConfirm.collectAsState().value, enabled = component.canConfirm.collectAsState().value,
onClick = component::confirm onClick = component::confirm
) )
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
ActionButton("Cancel", onClick = component.onClose) ActionButton(myStringResource(Res.string.ok), onClick = component.onClose)
} }
} }
} }
@ -197,8 +207,12 @@ fun LinkPreview(link: String) {
) )
} }
enum class WildcardSelect { enum class WildcardSelect(
Auto, Unspecified, Custom; val text: StringSource,
) {
Auto(Res.string.auto.asStringSource()),
Unspecified(Res.string.unspecified.asStringSource()),
Custom(Res.string.custom.asStringSource());
companion object { companion object {
fun fromWildcardLength(wildcardLength: WildcardLength): WildcardSelect { fun fromWildcardLength(wildcardLength: WildcardLength): WildcardSelect {
@ -235,7 +249,7 @@ private fun WildcardLengthUi(
) )
}, },
render = { render = {
Text(it.toString()) Text(it.text.rememberString())
} }
) )
AnimatedVisibility(wildcardLength is WildcardLength.Custom) { AnimatedVisibility(wildcardLength is WildcardLength.Custom) {

View File

@ -20,11 +20,14 @@ import com.abdownloadmanager.desktop.ui.theme.myTextSizes
import com.abdownloadmanager.desktop.ui.util.ifThen import com.abdownloadmanager.desktop.ui.util.ifThen
import com.abdownloadmanager.desktop.ui.widget.* import com.abdownloadmanager.desktop.ui.widget.*
import com.abdownloadmanager.desktop.utils.div import com.abdownloadmanager.desktop.utils.div
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.utils.compose.WithContentAlpha import com.abdownloadmanager.utils.compose.WithContentAlpha
import com.abdownloadmanager.utils.compose.widget.MyIcon import com.abdownloadmanager.utils.compose.widget.MyIcon
import io.github.vinceglb.filekit.compose.rememberDirectoryPickerLauncher import io.github.vinceglb.filekit.compose.rememberDirectoryPickerLauncher
import io.github.vinceglb.filekit.core.FileKitPlatformSettings import io.github.vinceglb.filekit.core.FileKitPlatformSettings
import ir.amirab.util.compose.IconSource import ir.amirab.util.compose.IconSource
import ir.amirab.util.compose.asStringSource
import ir.amirab.util.compose.resources.myStringResource
import ir.amirab.util.desktop.LocalWindow import ir.amirab.util.desktop.LocalWindow
import java.io.File import java.io.File
@ -33,8 +36,13 @@ fun NewCategory(
categoryComponent: CategoryComponent, categoryComponent: CategoryComponent,
) { ) {
WindowTitle( WindowTitle(
if (categoryComponent.isEditMode) "Edit Category" myStringResource(
else "Add Category" if (categoryComponent.isEditMode) {
Res.string.edit_category
} else {
Res.string.add_category
}
)
) )
Column( Column(
modifier = Modifier modifier = Modifier
@ -80,10 +88,12 @@ fun NewCategory(
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(12.dp))
Row(Modifier.fillMaxWidth().wrapContentWidth(Alignment.End)) { Row(Modifier.fillMaxWidth().wrapContentWidth(Alignment.End)) {
ActionButton( ActionButton(
when (categoryComponent.isEditMode) { myStringResource(
true -> "Change" when (categoryComponent.isEditMode) {
false -> "Add" true -> Res.string.change
}, false -> Res.string.add
}
),
enabled = categoryComponent.canSubmit.collectAsState().value, enabled = categoryComponent.canSubmit.collectAsState().value,
onClick = { onClick = {
categoryComponent.submit() categoryComponent.submit()
@ -91,7 +101,7 @@ fun NewCategory(
) )
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
ActionButton( ActionButton(
"Cancel", myStringResource(Res.string.cancel),
onClick = { onClick = {
categoryComponent.close() categoryComponent.close()
} }
@ -116,7 +126,7 @@ fun CategoryDefaultPath(
} ?: defaultDownloadLocation } ?: defaultDownloadLocation
} }
val downloadFolderPickerLauncher = rememberDirectoryPickerLauncher( val downloadFolderPickerLauncher = rememberDirectoryPickerLauncher(
title = "Category Download Location", title = myStringResource(Res.string.category_download_location),
initialDirectory = initialDirectory, initialDirectory = initialDirectory,
platformSettings = FileKitPlatformSettings( platformSettings = FileKitPlatformSettings(
parentWindow = LocalWindow.current parentWindow = LocalWindow.current
@ -126,8 +136,8 @@ fun CategoryDefaultPath(
} }
WithLabel( WithLabel(
"Category Download Location", label = myStringResource(Res.string.category_download_location),
helpText = """When this category chosen in "Add Download Page" use this directory as "Download Location""" helpText = myStringResource(Res.string.category_download_location_description)
) { ) {
CategoryPageTextField( CategoryPageTextField(
text = path, text = path,
@ -150,14 +160,14 @@ fun CategoryAutoTypes(
onTypesChanged: (String) -> Unit, onTypesChanged: (String) -> Unit,
) { ) {
WithLabel( WithLabel(
label = "Category file types", label = myStringResource(Res.string.category_file_types),
helpText = "Automatically put these file types to this category. (when you add new download)\nSeparate file extensions with space (ext1 ext2 ...) " helpText = myStringResource(Res.string.category_file_types_description)
) { ) {
CategoryPageTextField( CategoryPageTextField(
text = types, text = types,
onTextChange = onTypesChanged, onTextChange = onTypesChanged,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
placeholder = "ext1 ext2 ext3 (separate with space)", placeholder = "ext1 ext2 ext3",
singleLine = false, singleLine = false,
) )
} }
@ -171,8 +181,8 @@ fun CategoryAutoUrls(
onUrlPatternChanged: (String) -> Unit, onUrlPatternChanged: (String) -> Unit,
) { ) {
OptionalWithLabel( OptionalWithLabel(
label = "URL patterns", label = myStringResource(Res.string.url_patterns),
helpText = "Automatically put download from these URLs to this category. (when you add new download)\nSeparate URLs with space, you can also use * for wildcard", helpText = myStringResource(Res.string.url_patterns_description),
enabled = enabled, enabled = enabled,
setEnabled = setEnabled setEnabled = setEnabled
) { ) {
@ -194,7 +204,7 @@ fun CategoryName(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
WithLabel( WithLabel(
"Category Name", myStringResource(Res.string.category_name),
modifier, modifier,
) { ) {
CategoryPageTextField( CategoryPageTextField(
@ -266,7 +276,7 @@ private fun CategoryIcon(
mutableStateOf(false) mutableStateOf(false)
} }
WithLabel( WithLabel(
"Icon" myStringResource(Res.string.icon)
) { ) {
RenderIcon( RenderIcon(
icon = iconSource, icon = iconSource,

View File

@ -8,6 +8,9 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.rememberWindowState import androidx.compose.ui.window.rememberWindowState
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import ir.amirab.util.compose.resources.myStringResource
@Composable @Composable
fun ShowOpenSourceLibraries(appComponent: AppComponent){ fun ShowOpenSourceLibraries(appComponent: AppComponent){
@ -31,7 +34,7 @@ fun ShowOpenSourceLibraries(
size = DpSize(650.dp, 400.dp) size = DpSize(650.dp, 400.dp)
) )
) { ) {
WindowTitle("Open Source ThirdParty Libraries") WindowTitle(myStringResource(Res.string.open_source_software_used_in_this_app))
ExternalLibsPage() ExternalLibsPage()
} }
} }

View File

@ -21,10 +21,15 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import com.mikepenz.aboutlibraries.entity.Developer import com.mikepenz.aboutlibraries.entity.Developer
import com.mikepenz.aboutlibraries.entity.Library import com.mikepenz.aboutlibraries.entity.Library
import com.mikepenz.aboutlibraries.entity.License import com.mikepenz.aboutlibraries.entity.License
import com.mikepenz.aboutlibraries.entity.Organization import com.mikepenz.aboutlibraries.entity.Organization
import ir.amirab.util.compose.resources.myStringResource
import ir.amirab.util.compose.StringSource
import ir.amirab.util.compose.asStringSource
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
@Composable @Composable
@ -67,10 +72,10 @@ fun LibraryDialog(
} }
val links = buildList { val links = buildList {
library.scm?.url?.let { library.scm?.url?.let {
add("SourceCode" to it) add(Res.string.source_code.asStringSource() to it)
} }
library.website?.let { library.website?.let {
add("Website" to it) add(Res.string.website.asStringSource() to it)
} }
} }
links.takeIf { it.isNotEmpty() }?.let { links.takeIf { it.isNotEmpty() }?.let {
@ -83,7 +88,7 @@ fun LibraryDialog(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End, horizontalArrangement = Arrangement.End,
) { ) {
ActionButton("Close", onClick = { ActionButton(myStringResource(Res.string.close), onClick = {
onCloseRequest() onCloseRequest()
}) })
} }
@ -93,8 +98,8 @@ fun LibraryDialog(
} }
@Composable @Composable
private fun LibraryLinks(links: List<Pair<String, String>>) { private fun LibraryLinks(links: List<Pair<StringSource, String>>) {
KeyValue("Links") { KeyValue(myStringResource(Res.string.links)) {
ListOfNamesWithLinks(links) ListOfNamesWithLinks(links)
} }
} }
@ -110,12 +115,12 @@ private fun LibraryDescription(description: String) {
@Composable @Composable
private fun LibraryLicenseInfo(licenses: ImmutableSet<License>) { private fun LibraryLicenseInfo(licenses: ImmutableSet<License>) {
KeyValue("License") { KeyValue(myStringResource(Res.string.license)) {
val l = licenses.map { val l = licenses.map {
it.name to it.url it.name.asStringSource() to it.url
} }
if (l.isEmpty()) { if (l.isEmpty()) {
Text("no license found") Text(myStringResource(Res.string.no_license_found))
} else { } else {
ListOfNamesWithLinks(l) ListOfNamesWithLinks(l)
} }
@ -124,23 +129,23 @@ private fun LibraryLicenseInfo(licenses: ImmutableSet<License>) {
@Composable @Composable
private fun LibraryDevelopers(devs: List<Developer>) { private fun LibraryDevelopers(devs: List<Developer>) {
KeyValue("Developers") { KeyValue(myStringResource(Res.string.developers)) {
ListOfNamesWithLinks( ListOfNamesWithLinks(
devs devs
.filter { it.name != null } .filter { it.name != null }
.map { .map {
it.name!! to it.organisationUrl it.name!!.asStringSource() to it.organisationUrl
} }
) )
} }
} }
@Composable @Composable
private fun ListOfNamesWithLinks(map: List<Pair<String, String?>>) { private fun ListOfNamesWithLinks(map: List<Pair<StringSource, String?>>) {
Row { Row {
for ((i, v) in map.withIndex()) { for ((i, v) in map.withIndex()) {
val (name, link) = v val (name, link) = v
MaybeLinkText(name, link) MaybeLinkText(name.rememberString(), link)
if (i < map.lastIndex) { if (i < map.lastIndex) {
Text(", ") Text(", ")
} }
@ -150,7 +155,7 @@ private fun ListOfNamesWithLinks(map: List<Pair<String, String?>>) {
@Composable @Composable
fun LibraryOrganization(organization: Organization) { fun LibraryOrganization(organization: Organization) {
KeyValue("Organization") { KeyValue(myStringResource(Res.string.organization)) {
MaybeLinkText(organization.name, organization.url) MaybeLinkText(organization.name, organization.url)
} }
} }

View File

@ -4,27 +4,33 @@ import com.abdownloadmanager.desktop.ui.widget.customtable.CellSize
import com.abdownloadmanager.desktop.ui.widget.customtable.SortableCell import com.abdownloadmanager.desktop.ui.widget.customtable.SortableCell
import com.abdownloadmanager.desktop.ui.widget.customtable.TableCell import com.abdownloadmanager.desktop.ui.widget.customtable.TableCell
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.abdownloadmanager.resources.Res
import com.mikepenz.aboutlibraries.entity.Library import com.mikepenz.aboutlibraries.entity.Library
import ir.amirab.util.compose.StringSource
import ir.amirab.util.compose.asStringSource
sealed interface LibraryCells : TableCell<Library> { sealed interface LibraryCells : TableCell<Library> {
data object Name : LibraryCells, data object Name : LibraryCells,
SortableCell<Library> { SortableCell<Library> {
override fun sortBy(item: Library): Comparable<*> = item.name override fun sortBy(item: Library): Comparable<*> = item.name
override val name: String = "Name" override val id: String = "Name"
override val name: StringSource = Res.string.name.asStringSource()
override val size: CellSize = CellSize.Resizeable(100.dp..1000.dp,250.dp) override val size: CellSize = CellSize.Resizeable(100.dp..1000.dp,250.dp)
} }
data object Author : LibraryCells, data object Author : LibraryCells,
SortableCell<Library> { SortableCell<Library> {
override fun sortBy(item: Library): Comparable<*> = item.licenses.firstOrNull()?.name.orEmpty() override fun sortBy(item: Library): Comparable<*> = item.licenses.firstOrNull()?.name.orEmpty()
override val name: String = "Author" override val id: String = "Author"
override val name: StringSource = Res.string.author.asStringSource()
override val size: CellSize = CellSize.Resizeable(100.dp..200.dp, 150.dp) override val size: CellSize = CellSize.Resizeable(100.dp..200.dp, 150.dp)
} }
data object License : LibraryCells, data object License : LibraryCells,
SortableCell<Library> { SortableCell<Library> {
override fun sortBy(item: Library): Comparable<*> = item.licenses.firstOrNull()?.name.orEmpty() override fun sortBy(item: Library): Comparable<*> = item.licenses.firstOrNull()?.name.orEmpty()
override val name: String = "License" override val id: String = "License"
override val name: StringSource = Res.string.license.asStringSource()
override val size: CellSize = CellSize.Resizeable(100.dp..200.dp, 150.dp) override val size: CellSize = CellSize.Resizeable(100.dp..200.dp, 150.dp)
} }

View File

@ -87,7 +87,7 @@ private fun ActionButton(
MyIcon(it, null, Modifier.size(16.dp)) MyIcon(it, null, Modifier.size(16.dp))
} }
Spacer(Modifier.size(2.dp)) Spacer(Modifier.size(2.dp))
Text(title, maxLines = 1, fontSize = myTextSizes.sm) Text(title.rememberString(), maxLines = 1, fontSize = myTextSizes.sm)
} }
} }
} }
@ -120,7 +120,7 @@ private fun GroupActionButton(
MyIcon(it, null, Modifier.size(16.dp)) MyIcon(it, null, Modifier.size(16.dp))
} }
Spacer(Modifier.size(2.dp)) Spacer(Modifier.size(2.dp))
Text(title, maxLines = 1, fontSize = myTextSizes.sm) Text(title.rememberString(), maxLines = 1, fontSize = myTextSizes.sm)
} }
} }
} }

View File

@ -22,6 +22,8 @@ import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.abdownloadmanager.desktop.pages.category.CategoryDialogManager import com.abdownloadmanager.desktop.pages.category.CategoryDialogManager
import com.abdownloadmanager.desktop.storage.AppSettingsStorage import com.abdownloadmanager.desktop.storage.AppSettingsStorage
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import com.abdownloadmanager.utils.FileIconProvider import com.abdownloadmanager.utils.FileIconProvider
import com.abdownloadmanager.utils.category.Category import com.abdownloadmanager.utils.category.Category
import com.abdownloadmanager.utils.category.CategoryItemWithId import com.abdownloadmanager.utils.category.CategoryItemWithId
@ -37,6 +39,8 @@ import ir.amirab.util.flow.combineStateFlows
import ir.amirab.util.flow.mapStateFlow import ir.amirab.util.flow.mapStateFlow
import ir.amirab.util.flow.mapTwoWayStateFlow import ir.amirab.util.flow.mapTwoWayStateFlow
import com.abdownloadmanager.utils.extractors.linkextractor.DownloadCredentialFromStringExtractor import com.abdownloadmanager.utils.extractors.linkextractor.DownloadCredentialFromStringExtractor
import ir.amirab.util.compose.asStringSource
import ir.amirab.util.compose.asStringSourceWithARgs
import ir.amirab.util.osfileutil.FileUtils import ir.amirab.util.osfileutil.FileUtils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@ -44,7 +48,6 @@ import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import java.io.File import java.io.File
import java.net.URI
@Stable @Stable
class FilterState { class FilterState {
@ -102,7 +105,7 @@ class DownloadActions(
} }
} }
val openFileAction = simpleAction( val openFileAction = simpleAction(
title = "Open", title = Res.string.open.asStringSource(),
icon = MyIcons.fileOpen, icon = MyIcons.fileOpen,
checkEnable = defaultItem.mapStateFlow { checkEnable = defaultItem.mapStateFlow {
it?.statusOrFinished() is DownloadJobStatus.Finished it?.statusOrFinished() is DownloadJobStatus.Finished
@ -116,7 +119,7 @@ class DownloadActions(
) )
val openFolderAction = simpleAction( val openFolderAction = simpleAction(
title = "Open Folder", title = Res.string.open_folder.asStringSource(),
icon = MyIcons.folderOpen, icon = MyIcons.folderOpen,
checkEnable = defaultItem.mapStateFlow { checkEnable = defaultItem.mapStateFlow {
it?.statusOrFinished() is DownloadJobStatus.Finished it?.statusOrFinished() is DownloadJobStatus.Finished
@ -130,7 +133,7 @@ class DownloadActions(
) )
val deleteAction = simpleAction( val deleteAction = simpleAction(
title = "Delete", title = Res.string.delete.asStringSource(),
icon = MyIcons.remove, icon = MyIcons.remove,
checkEnable = selections.mapStateFlow { it.isNotEmpty() }, checkEnable = selections.mapStateFlow { it.isNotEmpty() },
onActionPerformed = { onActionPerformed = {
@ -141,7 +144,7 @@ class DownloadActions(
) )
val resumeAction = simpleAction( val resumeAction = simpleAction(
title = "Resume", title = Res.string.resume.asStringSource(),
icon = MyIcons.resume, icon = MyIcons.resume,
checkEnable = resumableSelections.mapStateFlow { checkEnable = resumableSelections.mapStateFlow {
it.isNotEmpty() it.isNotEmpty()
@ -158,7 +161,7 @@ class DownloadActions(
) )
val reDownloadAction = simpleAction( val reDownloadAction = simpleAction(
"Restart Download", Res.string.restart_download.asStringSource(),
MyIcons.refresh MyIcons.refresh
) { ) {
scope.launch { scope.launch {
@ -174,7 +177,7 @@ class DownloadActions(
} }
val pauseAction = simpleAction( val pauseAction = simpleAction(
title = "Pause", title = Res.string.pause.asStringSource(),
icon = MyIcons.pause, icon = MyIcons.pause,
checkEnable = pausableSelections.mapStateFlow { checkEnable = pausableSelections.mapStateFlow {
it.isNotEmpty() it.isNotEmpty()
@ -191,7 +194,7 @@ class DownloadActions(
) )
val copyDownloadLinkAction = simpleAction( val copyDownloadLinkAction = simpleAction(
title = "Copy link", title = Res.string.copy_link.asStringSource(),
icon = MyIcons.copy, icon = MyIcons.copy,
checkEnable = checkEnable =
selections.mapStateFlow { it.isNotEmpty() }, selections.mapStateFlow { it.isNotEmpty() },
@ -206,7 +209,7 @@ class DownloadActions(
} }
) )
val openDownloadDialogAction = simpleAction("Show Properties", MyIcons.info) { val openDownloadDialogAction = simpleAction(Res.string.show_properties.asStringSource(), MyIcons.info) {
selections.value.map { it.id } selections.value.map { it.id }
.forEach { id -> .forEach { id ->
downloadDialogManager.openDownloadDialog(id) downloadDialogManager.openDownloadDialog(id)
@ -214,7 +217,7 @@ class DownloadActions(
} }
private val moveToQueueItems = MenuItem.SubMenu( private val moveToQueueItems = MenuItem.SubMenu(
title = "Move To Queue", title = Res.string.move_to_queue.asStringSource(),
items = emptyList() items = emptyList()
).apply { ).apply {
merge( merge(
@ -229,7 +232,7 @@ class DownloadActions(
}.launchIn(scope) }.launchIn(scope)
} }
private val moveToCategoryAction = MenuItem.SubMenu( private val moveToCategoryAction = MenuItem.SubMenu(
title = "Move To Category", title = Res.string.move_to_category.asStringSource(),
items = emptyList() items = emptyList()
).apply { ).apply {
merge( merge(
@ -293,7 +296,7 @@ class CategoryActions(
} }
val openCategoryFolderAction = simpleAction( val openCategoryFolderAction = simpleAction(
title = "Open Folder", title = Res.string.open_folder.asStringSource(),
icon = MyIcons.folderOpen, icon = MyIcons.folderOpen,
checkEnable = mainItemExists, checkEnable = mainItemExists,
onActionPerformed = { onActionPerformed = {
@ -306,7 +309,7 @@ class CategoryActions(
) )
val deleteAction = simpleAction( val deleteAction = simpleAction(
title = "Delete Category", title = Res.string.delete_category.asStringSource(),
icon = MyIcons.remove, icon = MyIcons.remove,
checkEnable = mainItemExists, checkEnable = mainItemExists,
onActionPerformed = { onActionPerformed = {
@ -318,7 +321,7 @@ class CategoryActions(
}, },
) )
val editAction = simpleAction( val editAction = simpleAction(
title = "Edit Category", title = Res.string.edit_category.asStringSource(),
icon = MyIcons.settings, icon = MyIcons.settings,
checkEnable = mainItemExists, checkEnable = mainItemExists,
onActionPerformed = { onActionPerformed = {
@ -331,7 +334,7 @@ class CategoryActions(
) )
val addCategoryAction = simpleAction( val addCategoryAction = simpleAction(
title = "Add Category", title = Res.string.add_category.asStringSource(),
icon = MyIcons.add, icon = MyIcons.add,
onActionPerformed = { onActionPerformed = {
scope.launch { scope.launch {
@ -340,7 +343,7 @@ class CategoryActions(
}, },
) )
val categorizeItemsAction = simpleAction( val categorizeItemsAction = simpleAction(
title = "Auto Categorise Items", title = Res.string.auto_categorize_downloads.asStringSource(),
icon = MyIcons.refresh, icon = MyIcons.refresh,
onActionPerformed = { onActionPerformed = {
scope.launch { scope.launch {
@ -349,7 +352,7 @@ class CategoryActions(
}, },
) )
val resetToDefaultAction = simpleAction( val resetToDefaultAction = simpleAction(
title = "Restore Defaults", title = Res.string.restore_defaults.asStringSource(),
icon = MyIcons.undo, icon = MyIcons.undo,
checkEnable = categoryManager checkEnable = categoryManager
.categoriesFlow .categoriesFlow
@ -496,7 +499,7 @@ class HomeComponent(
val menu: List<MenuItem.SubMenu> = buildMenu { val menu: List<MenuItem.SubMenu> = buildMenu {
subMenu("File") { subMenu(Res.string.file.asStringSource()) {
+newDownloadAction +newDownloadAction
+newDownloadFromClipboardAction +newDownloadFromClipboardAction
+batchDownloadAction +batchDownloadAction
@ -504,7 +507,7 @@ class HomeComponent(
+exitAction +exitAction
} }
subMenu("Tasks") { subMenu(Res.string.tasks.asStringSource()) {
// +toggleQueueAction // +toggleQueueAction
+startQueueGroupAction +startQueueGroupAction
+stopQueueGroupAction +stopQueueGroupAction
@ -512,21 +515,21 @@ class HomeComponent(
+stopAllAction +stopAllAction
separator() separator()
subMenu( subMenu(
title = "Remove", title = Res.string.delete.asStringSource(),
icon = MyIcons.remove icon = MyIcons.remove
) { ) {
item("All Finished") { item(Res.string.all_finished.asStringSource()) {
requestDelete(downloadSystem.getFinishedDownloadIds()) requestDelete(downloadSystem.getFinishedDownloadIds())
} }
item("All Unfinished") { item(Res.string.all_unfinished.asStringSource()) {
requestDelete(downloadSystem.getUnfinishedDownloadIds()) requestDelete(downloadSystem.getUnfinishedDownloadIds())
} }
item("Entire List") { item(Res.string.entire_list.asStringSource()) {
requestDelete(downloadSystem.getAllDownloadIds()) requestDelete(downloadSystem.getAllDownloadIds())
} }
} }
} }
subMenu("Tools") { subMenu(Res.string.tools.asStringSource()) {
if (AppInfo.isInDebugMode()) { if (AppInfo.isInDebugMode()) {
+dummyException +dummyException
+dummyMessage +dummyMessage
@ -536,7 +539,7 @@ class HomeComponent(
separator() separator()
+gotoSettingsAction +gotoSettingsAction
} }
subMenu("Help") { subMenu(Res.string.help.asStringSource()) {
//TODO Enable Updater //TODO Enable Updater
// +checkForUpdateAction // +checkForUpdateAction
+supportActionGroup +supportActionGroup
@ -558,9 +561,15 @@ class HomeComponent(
MenuItem.SubMenu( MenuItem.SubMenu(
icon = null, icon = null,
title = if (selectionList.size == 1) { title = if (selectionList.size == 1) {
downloadActions.defaultItem.value?.name ?: "" (downloadActions.defaultItem.value?.name ?: "")
.asStringSource()
} else { } else {
"${selectionList.size} Selected" Res.string.n_items_selected
.asStringSourceWithARgs(
Res.string.n_items_selected_createArgs(
count = selectionList.size.toString()
)
)
}, },
items = downloadActions.menu items = downloadActions.menu
) )
@ -786,9 +795,9 @@ class HomeComponent(
val dItem = downloadSystem.getDownloadItemById(id) ?: return@launch val dItem = downloadSystem.getDownloadItemById(id) ?: return@launch
if (dItem.status != DownloadStatus.Completed) { if (dItem.status != DownloadStatus.Completed) {
notificationSender.sendNotification( notificationSender.sendNotification(
"Open File", Res.string.open_file,
"Can't open file", Res.string.cant_open_file.asStringSource(),
"Not finished", Res.string.not_finished.asStringSource(),
NotificationType.Error, NotificationType.Error,
) )
return@launch return@launch

View File

@ -44,9 +44,15 @@ import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import com.abdownloadmanager.desktop.ui.customwindow.* import com.abdownloadmanager.desktop.ui.customwindow.*
import com.abdownloadmanager.desktop.ui.widget.menu.ShowOptionsInDropDown import com.abdownloadmanager.desktop.ui.widget.menu.ShowOptionsInDropDown
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import com.abdownloadmanager.utils.category.Category import com.abdownloadmanager.utils.category.Category
import com.abdownloadmanager.utils.category.rememberIconPainter import com.abdownloadmanager.utils.category.rememberIconPainter
import ir.amirab.util.compose.resources.myStringResource
import ir.amirab.util.compose.StringSource
import ir.amirab.util.compose.action.MenuItem import ir.amirab.util.compose.action.MenuItem
import ir.amirab.util.compose.asStringSource
import ir.amirab.util.compose.localizationmanager.WithLanguageDirection
import java.awt.datatransfer.DataFlavor import java.awt.datatransfer.DataFlavor
import java.io.File import java.io.File
@ -54,7 +60,6 @@ import java.io.File
@Composable @Composable
fun HomePage(component: HomeComponent) { fun HomePage(component: HomeComponent) {
val listState by component.downloadList.collectAsState() val listState by component.downloadList.collectAsState()
WindowTitle(AppInfo.name)
var isDragging by remember { mutableStateOf(false) } var isDragging by remember { mutableStateOf(false) }
var showDeletePromptState by remember { var showDeletePromptState by remember {
@ -84,16 +89,16 @@ fun HomePage(component: HomeComponent) {
is HomeEffects.AutoCategorize -> { is HomeEffects.AutoCategorize -> {
showConfirmPrompt = ConfirmPromptState( showConfirmPrompt = ConfirmPromptState(
title = "Auto categorize downloads", title = Res.string.confirm_auto_categorize_downloads_title.asStringSource(),
description = "Any uncategorized item will be automatically added to it's related category.", description = Res.string.confirm_auto_categorize_downloads_description.asStringSource(),
onConfirm = component::onConfirmAutoCategorize onConfirm = component::onConfirmAutoCategorize
) )
} }
is HomeEffects.ResetCategoriesToDefault -> { is HomeEffects.ResetCategoriesToDefault -> {
showConfirmPrompt = ConfirmPromptState( showConfirmPrompt = ConfirmPromptState(
title = "Reset to Default Categories", title = Res.string.confirm_reset_to_default_categories_title.asStringSource(),
description = "this will REMOVE all categories and brings backs default categories", description = Res.string.confirm_reset_to_default_categories_description.asStringSource(),
onConfirm = component::onConfirmResetCategories onConfirm = component::onConfirmResetCategories
) )
} }
@ -207,9 +212,11 @@ fun HomePage(component: HomeComponent) {
) )
) { ) {
if (!mergeTopBar) { if (!mergeTopBar) {
Spacer(Modifier.height(4.dp)) WithTitleBarDirection {
TopBar(component) Spacer(Modifier.height(4.dp))
Spacer(Modifier.height(6.dp)) TopBar(component)
Spacer(Modifier.height(6.dp))
}
} }
Spacer( Spacer(
Modifier.fillMaxWidth() Modifier.fillMaxWidth()
@ -340,14 +347,19 @@ private fun ShowDeletePrompts(
.widthIn(max = 260.dp) .widthIn(max = 260.dp)
) { ) {
Text( Text(
"Confirm Delete", myStringResource(Res.string.confirm_delete_download_items_title),
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = myTextSizes.xl, fontSize = myTextSizes.xl,
color = myColors.onBackground, color = myColors.onBackground,
) )
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(12.dp))
Text( Text(
"Are you sure you want to delete ${deletePromptState.downloadList.size} item ?", myStringResource(
Res.string.confirm_delete_download_items_description,
Res.string.confirm_delete_download_items_description_createArgs(
count = deletePromptState.downloadList.size.toString()
),
),
fontSize = myTextSizes.base, fontSize = myTextSizes.base,
color = myColors.onBackground, color = myColors.onBackground,
) )
@ -366,7 +378,7 @@ private fun ShowDeletePrompts(
}) })
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text( Text(
"Also delete file from disk", myStringResource(Res.string.also_delete_file_from_disk),
fontSize = myTextSizes.base, fontSize = myTextSizes.base,
color = myColors.onBackground, color = myColors.onBackground,
) )
@ -378,13 +390,13 @@ private fun ShowDeletePrompts(
) { ) {
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
ActionButton( ActionButton(
text = "Delete", text = myStringResource(Res.string.delete),
onClick = onConfirm, onClick = onConfirm,
borderColor = SolidColor(myColors.error), borderColor = SolidColor(myColors.error),
contentColor = myColors.error, contentColor = myColors.error,
) )
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
ActionButton(text = "Cancel", onClick = onCancel) ActionButton(text = myStringResource(Res.string.cancel), onClick = onCancel)
} }
} }
} }
@ -415,14 +427,14 @@ private fun ShowConfirmPrompt(
.widthIn(max = 260.dp) .widthIn(max = 260.dp)
) { ) {
Text( Text(
text = promptState.title, text = promptState.title.rememberString(),
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = myTextSizes.xl, fontSize = myTextSizes.xl,
color = myColors.onBackground, color = myColors.onBackground,
) )
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(12.dp))
Text( Text(
text = promptState.description, text = promptState.description.rememberString(),
fontSize = myTextSizes.base, fontSize = myTextSizes.base,
color = myColors.onBackground, color = myColors.onBackground,
) )
@ -433,13 +445,12 @@ private fun ShowConfirmPrompt(
) { ) {
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
ActionButton( ActionButton(
text = "Delete", text = myStringResource(Res.string.ok),
onClick = onConfirm, onClick = onConfirm,
borderColor = SolidColor(myColors.error),
contentColor = myColors.error, contentColor = myColors.error,
) )
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
ActionButton(text = "Cancel", onClick = onCancel) ActionButton(text = myStringResource(Res.string.cancel), onClick = onCancel)
} }
} }
} }
@ -470,20 +481,30 @@ private fun ShowDeleteCategoryPrompt(
.widthIn(max = 260.dp) .widthIn(max = 260.dp)
) { ) {
Text( Text(
"""Removing "${deletePromptState.category.name}" Category""", myStringResource(
Res.string.confirm_delete_category_item_title,
Res.string.confirm_delete_category_item_title_createArgs(
name = deletePromptState.category.name
),
),
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = myTextSizes.xl, fontSize = myTextSizes.xl,
color = myColors.onBackground, color = myColors.onBackground,
) )
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(12.dp))
Text( Text(
"""Are you sure you want to delete "${deletePromptState.category.name}" Category ?""", myStringResource(
Res.string.confirm_delete_category_item_description,
Res.string.confirm_delete_category_item_description_createArgs(
value = deletePromptState.category.name
)
),
fontSize = myTextSizes.base, fontSize = myTextSizes.base,
color = myColors.onBackground, color = myColors.onBackground,
) )
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(12.dp))
Text( Text(
"Your downloads won't be deleted", myStringResource(Res.string.your_download_will_not_be_deleted),
fontSize = myTextSizes.base, fontSize = myTextSizes.base,
color = myColors.onBackground, color = myColors.onBackground,
) )
@ -494,13 +515,13 @@ private fun ShowDeleteCategoryPrompt(
) { ) {
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
ActionButton( ActionButton(
text = "Delete", text = myStringResource(Res.string.delete),
onClick = onConfirm, onClick = onConfirm,
borderColor = SolidColor(myColors.error), borderColor = SolidColor(myColors.error),
contentColor = myColors.error, contentColor = myColors.error,
) )
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
ActionButton(text = "Cancel", onClick = onCancel) ActionButton(text = myStringResource(Res.string.cancel), onClick = onCancel)
} }
} }
} }
@ -520,8 +541,8 @@ data class CategoryDeletePromptState(
@Immutable @Immutable
data class ConfirmPromptState( data class ConfirmPromptState(
val title: String, val title: StringSource,
val description: String, val description: StringSource,
val onConfirm: () -> Unit, val onConfirm: () -> Unit,
) )
@ -553,21 +574,26 @@ fun DragWidget(
Modifier.size(36.dp), Modifier.size(36.dp),
) )
Text( Text(
text = "Drop link or file here.", text = myStringResource(Res.string.drop_link_or_file_here),
fontSize = myTextSizes.xl fontSize = myTextSizes.xl
) )
if (linkCount != null) { if (linkCount != null) {
when { when {
linkCount > 0 -> { linkCount > 0 -> {
Text( Text(
"$linkCount links will be imported", myStringResource(
Res.string.n_links_will_be_imported,
Res.string.n_links_will_be_imported_createArgs(
count = linkCount.toString()
)
),
fontSize = myTextSizes.base, fontSize = myTextSizes.base,
color = myColors.success, color = myColors.success,
) )
} }
linkCount == 0 -> { linkCount == 0 -> {
Text("Nothing will be imported") Text(myStringResource(Res.string.nothing_will_be_imported))
} }
} }
@ -644,7 +670,7 @@ fun CategoryOption(
ShowOptionsInDropDown( ShowOptionsInDropDown(
MenuItem.SubMenu( MenuItem.SubMenu(
icon = categoryOptionMenuState.categoryItem?.rememberIconPainter(), icon = categoryOptionMenuState.categoryItem?.rememberIconPainter(),
title = categoryOptionMenuState.categoryItem?.name.orEmpty(), title = categoryOptionMenuState.categoryItem?.name.orEmpty().asStringSource(),
categoryOptionMenuState.menu, categoryOptionMenuState.menu,
), ),
onDismiss onDismiss
@ -728,20 +754,22 @@ fun HomeSearch(
val searchBoxInteractionSource = remember { MutableInteractionSource() } val searchBoxInteractionSource = remember { MutableInteractionSource() }
val isFocused by searchBoxInteractionSource.collectIsFocusedAsState() val isFocused by searchBoxInteractionSource.collectIsFocusedAsState()
SearchBox( WithLanguageDirection {
text = component.filterState.textToSearch, SearchBox(
onTextChange = { text = component.filterState.textToSearch,
component.filterState.textToSearch = it onTextChange = {
}, component.filterState.textToSearch = it
textPadding = textPadding, },
interactionSource = searchBoxInteractionSource, textPadding = textPadding,
modifier = modifier interactionSource = searchBoxInteractionSource,
.width( modifier = modifier
animateDpAsState( .width(
if (isFocused) 220.dp else 180.dp animateDpAsState(
).value if (isFocused) 220.dp else 180.dp
) ).value
) )
)
}
} }

View File

@ -11,7 +11,11 @@ import com.abdownloadmanager.desktop.actions.handle
import com.abdownloadmanager.desktop.ui.customwindow.CustomWindow import com.abdownloadmanager.desktop.ui.customwindow.CustomWindow
import com.abdownloadmanager.desktop.ui.customwindow.rememberWindowController import com.abdownloadmanager.desktop.ui.customwindow.rememberWindowController
import com.abdownloadmanager.desktop.ui.icon.MyIcons import com.abdownloadmanager.desktop.ui.icon.MyIcons
import com.abdownloadmanager.desktop.utils.AppInfo
import com.abdownloadmanager.desktop.utils.mvi.HandleEffects import com.abdownloadmanager.desktop.utils.mvi.HandleEffects
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import ir.amirab.util.compose.resources.myStringResource
import java.awt.Dimension import java.awt.Dimension
@Composable @Composable
@ -25,10 +29,9 @@ fun HomeWindow(
position = WindowPosition.Aligned(Alignment.Center) position = WindowPosition.Aligned(Alignment.Center)
) )
val onCloseRequest = onCLoseRequest val onCloseRequest = onCLoseRequest
val windowTitle = "AB Download Manager"
val windowIcon = MyIcons.appIcon val windowIcon = MyIcons.appIcon
val windowController = rememberWindowController( val windowController = rememberWindowController(
windowTitle, AppInfo.name,
windowIcon.rememberPainter(), windowIcon.rememberPainter(),
) )

View File

@ -25,13 +25,18 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.* import androidx.compose.ui.input.key.*
import androidx.compose.ui.input.pointer.* import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import com.abdownloadmanager.utils.FileIconProvider import com.abdownloadmanager.utils.FileIconProvider
import com.abdownloadmanager.utils.category.CategoryManager import com.abdownloadmanager.utils.category.CategoryManager
import com.abdownloadmanager.utils.category.rememberCategoryOf import com.abdownloadmanager.utils.category.rememberCategoryOf
import ir.amirab.util.compose.resources.myStringResource
import ir.amirab.downloader.monitor.IDownloadItemState import ir.amirab.downloader.monitor.IDownloadItemState
import ir.amirab.downloader.monitor.remainingOrNull import ir.amirab.downloader.monitor.remainingOrNull
import ir.amirab.downloader.monitor.speedOrNull import ir.amirab.downloader.monitor.speedOrNull
import ir.amirab.downloader.monitor.statusOrFinished import ir.amirab.downloader.monitor.statusOrFinished
import ir.amirab.util.compose.StringSource
import ir.amirab.util.compose.asStringSource
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -143,7 +148,7 @@ fun DownloadList(
), ),
drawOnEmpty = { drawOnEmpty = {
WithContentAlpha(0.75f) { WithContentAlpha(0.75f) {
Text("List is empty.", Modifier.align(Alignment.Center)) Text(myStringResource(Res.string.list_is_empty), Modifier.align(Alignment.Center))
} }
}, },
wrapHeader = { wrapHeader = {
@ -288,7 +293,8 @@ fun DownloadList(
sealed interface DownloadListCells : TableCell<IDownloadItemState> { sealed interface DownloadListCells : TableCell<IDownloadItemState> {
data object Check : DownloadListCells, data object Check : DownloadListCells,
CustomCellRenderer { CustomCellRenderer {
override val name: String = "#" override val id: String = "#"
override val name: StringSource = "#".asStringSource()
override val size: CellSize = CellSize.Fixed(26.dp) override val size: CellSize = CellSize.Fixed(26.dp)
@Composable @Composable
@ -308,7 +314,8 @@ sealed interface DownloadListCells : TableCell<IDownloadItemState> {
SortableCell<IDownloadItemState> { SortableCell<IDownloadItemState> {
override fun sortBy(item: IDownloadItemState): Comparable<*> = item.name override fun sortBy(item: IDownloadItemState): Comparable<*> = item.name
override val name: String = "Name" override val id: String = "Name"
override val name: StringSource = Res.string.name.asStringSource()
override val size: CellSize = CellSize.Resizeable(50.dp..1000.dp, 200.dp) override val size: CellSize = CellSize.Resizeable(50.dp..1000.dp, 200.dp)
} }
@ -316,7 +323,8 @@ sealed interface DownloadListCells : TableCell<IDownloadItemState> {
SortableCell<IDownloadItemState> { SortableCell<IDownloadItemState> {
override fun sortBy(item: IDownloadItemState): Comparable<*> = item.statusOrFinished().order override fun sortBy(item: IDownloadItemState): Comparable<*> = item.statusOrFinished().order
override val name: String = "Status" override val id: String = "Status"
override val name: StringSource = Res.string.status.asStringSource()
override val size: CellSize = CellSize.Resizeable(100.dp..140.dp, 120.dp) override val size: CellSize = CellSize.Resizeable(100.dp..140.dp, 120.dp)
} }
@ -324,7 +332,8 @@ sealed interface DownloadListCells : TableCell<IDownloadItemState> {
SortableCell<IDownloadItemState> { SortableCell<IDownloadItemState> {
override fun sortBy(item: IDownloadItemState): Comparable<*> = item.contentLength override fun sortBy(item: IDownloadItemState): Comparable<*> = item.contentLength
override val name: String = "Size" override val id: String = "Size"
override val name: StringSource = Res.string.size.asStringSource()
override val size: CellSize = CellSize.Resizeable(70.dp..110.dp, 70.dp) override val size: CellSize = CellSize.Resizeable(70.dp..110.dp, 70.dp)
} }
@ -332,7 +341,8 @@ sealed interface DownloadListCells : TableCell<IDownloadItemState> {
SortableCell<IDownloadItemState> { SortableCell<IDownloadItemState> {
override fun sortBy(item: IDownloadItemState): Comparable<*> = item.speedOrNull() ?: 0L override fun sortBy(item: IDownloadItemState): Comparable<*> = item.speedOrNull() ?: 0L
override val name: String = "Speed" override val id: String = "Speed"
override val name: StringSource = Res.string.speed.asStringSource()
override val size: CellSize = CellSize.Resizeable(70.dp..110.dp, 80.dp) override val size: CellSize = CellSize.Resizeable(70.dp..110.dp, 80.dp)
} }
@ -340,7 +350,8 @@ sealed interface DownloadListCells : TableCell<IDownloadItemState> {
SortableCell<IDownloadItemState> { SortableCell<IDownloadItemState> {
override fun sortBy(item: IDownloadItemState): Comparable<*> = item.remainingOrNull() ?: Long.MAX_VALUE override fun sortBy(item: IDownloadItemState): Comparable<*> = item.remainingOrNull() ?: Long.MAX_VALUE
override val name: String = "Time Left" override val id: String = "Time Left"
override val name: StringSource = Res.string.time_left.asStringSource()
override val size: CellSize = CellSize.Resizeable(70.dp..150.dp, 100.dp) override val size: CellSize = CellSize.Resizeable(70.dp..150.dp, 100.dp)
} }
@ -348,7 +359,8 @@ sealed interface DownloadListCells : TableCell<IDownloadItemState> {
SortableCell<IDownloadItemState> { SortableCell<IDownloadItemState> {
override fun sortBy(item: IDownloadItemState): Comparable<*> = item.dateAdded override fun sortBy(item: IDownloadItemState): Comparable<*> = item.dateAdded
override val name: String = "Date Added" override val id: String = "Date Added"
override val name: StringSource = Res.string.date_added.asStringSource()
override val size: CellSize = CellSize.Resizeable(90.dp..150.dp, 100.dp) override val size: CellSize = CellSize.Resizeable(90.dp..150.dp, 100.dp)
} }
} }

View File

@ -20,6 +20,9 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import ir.amirab.util.compose.resources.myStringResource
@Composable @Composable
fun SearchBox( fun SearchBox(
@ -27,7 +30,7 @@ fun SearchBox(
onTextChange: (String) -> Unit, onTextChange: (String) -> Unit,
textPadding: PaddingValues = PaddingValues(horizontal = 8.dp), textPadding: PaddingValues = PaddingValues(horizontal = 8.dp),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
placeholder: String = "Search in the List", placeholder: String = myStringResource(Res.string.search_in_the_list),
modifier: Modifier, modifier: Modifier,
) { ) {
val shape = RoundedCornerShape(12.dp) val shape = RoundedCornerShape(12.dp)
@ -44,7 +47,8 @@ fun SearchBox(
animateFloatAsState(if (text.isBlank()) 0.9f else 1f).value animateFloatAsState(if (text.isBlank()) 0.9f else 1f).value
) { ) {
MyIcon( MyIcon(
MyIcons.search, "Search", MyIcons.search,
myStringResource(Res.string.search),
Modifier Modifier
.padding(start = 8.dp) .padding(start = 8.dp)
.size(16.dp) .size(16.dp)
@ -55,7 +59,7 @@ fun SearchBox(
AnimatedContent(text.isNotBlank()) { AnimatedContent(text.isNotBlank()) {
MyIcon( MyIcon(
MyIcons.clear, MyIcons.clear,
"Clear", myStringResource(Res.string.clear),
Modifier Modifier
.padding(end = 8.dp) .padding(end = 8.dp)
.clip(CircleShape) .clip(CircleShape)

View File

@ -19,13 +19,17 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import com.abdownloadmanager.utils.FileIconProvider import com.abdownloadmanager.utils.FileIconProvider
import com.abdownloadmanager.utils.category.Category import com.abdownloadmanager.utils.category.Category
import ir.amirab.util.compose.resources.myStringResource
import ir.amirab.downloader.downloaditem.DownloadJobStatus import ir.amirab.downloader.downloaditem.DownloadJobStatus
import ir.amirab.downloader.monitor.CompletedDownloadItemState import ir.amirab.downloader.monitor.CompletedDownloadItemState
import ir.amirab.downloader.monitor.IDownloadItemState import ir.amirab.downloader.monitor.IDownloadItemState
import ir.amirab.downloader.monitor.ProcessingDownloadItemState import ir.amirab.downloader.monitor.ProcessingDownloadItemState
import ir.amirab.downloader.utils.ExceptionUtils import ir.amirab.downloader.utils.ExceptionUtils
import ir.amirab.util.compose.resources.MyStringResource
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.datetime.* import kotlinx.datetime.*
@ -115,7 +119,7 @@ fun NameCell(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
Text( Text(
category?.name ?: "General", maxLines = 1, fontSize = myTextSizes.xs, category?.name ?: myStringResource(Res.string.general), maxLines = 1, fontSize = myTextSizes.xs,
color = LocalContentColor.current / 50 color = LocalContentColor.current / 50
) )
} }
@ -186,7 +190,7 @@ fun SizeCell(
) { ) {
item.contentLength.let { item.contentLength.let {
Text( Text(
convertSizeToHumanReadable(it), convertSizeToHumanReadable(it).rememberString(),
maxLines = 1, maxLines = 1,
fontSize = myTextSizes.base, fontSize = myTextSizes.base,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
@ -247,17 +251,69 @@ fun StatusCell(
DownloadJobStatus.Finished, DownloadJobStatus.Finished,
DownloadJobStatus.Resuming, DownloadJobStatus.Resuming,
-> SimpleStatus(itemState.status.toString()) -> SimpleStatus(myStringResource(itemState.status.toStringResource()))
} }
} }
is CompletedDownloadItemState -> { is CompletedDownloadItemState -> {
SimpleStatus("Finished") SimpleStatus(myStringResource(Res.string.finished))
} }
} }
} }
@Composable
private fun DownloadJobStatus.toStringResource(): MyStringResource {
return when (this) {
is DownloadJobStatus.Canceled -> {
Res.string.canceled
}
DownloadJobStatus.Downloading -> {
Res.string.downloading
}
DownloadJobStatus.Finished -> {
Res.string.finished
}
DownloadJobStatus.IDLE -> {
Res.string.idle
}
is DownloadJobStatus.PreparingFile -> {
Res.string.preparing_file
}
DownloadJobStatus.Resuming -> {
Res.string.resuming
}
}
}
private fun DownloadProgressStatus.toStringResource(): MyStringResource {
return when (this) {
DownloadProgressStatus.Added -> {
Res.string.added
}
DownloadProgressStatus.Error -> {
Res.string.error
}
DownloadProgressStatus.Paused -> {
Res.string.paused
}
DownloadProgressStatus.CreatingFile -> {
Res.string.creating_file
}
DownloadProgressStatus.Downloading -> {
Res.string.downloading
}
}
}
@Composable @Composable
private fun SimpleStatus(string: String) { private fun SimpleStatus(string: String) {
@ -285,11 +341,12 @@ private fun ProgressAndPercent(
DownloadProgressStatus.CreatingFile -> myColors.infoGradient DownloadProgressStatus.CreatingFile -> myColors.infoGradient
DownloadProgressStatus.Downloading -> myColors.primaryGradient DownloadProgressStatus.Downloading -> myColors.primaryGradient
} }
val statusString = myStringResource(status.toStringResource())
Column { Column {
val statusText = if (gotAnyProgress) { val statusText = if (gotAnyProgress) {
"${percent ?: "."}% $status" "${percent ?: "."}% $statusString"
} else { } else {
"$status" statusString
} }
SimpleStatus(statusText) SimpleStatus(statusText)
if (status != DownloadProgressStatus.Added) { if (status != DownloadProgressStatus.Added) {

View File

@ -17,7 +17,6 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import com.abdownloadmanager.desktop.ui.widget.Text import com.abdownloadmanager.desktop.ui.widget.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -28,14 +27,18 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.abdownloadmanager.desktop.ui.theme.myColors import com.abdownloadmanager.desktop.ui.theme.myColors
import com.abdownloadmanager.desktop.utils.div import com.abdownloadmanager.desktop.utils.div
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import com.abdownloadmanager.utils.category.Category import com.abdownloadmanager.utils.category.Category
import com.abdownloadmanager.utils.category.rememberIconPainter import com.abdownloadmanager.utils.category.rememberIconPainter
import ir.amirab.downloader.downloaditem.DownloadStatus import ir.amirab.downloader.downloaditem.DownloadStatus
import ir.amirab.downloader.monitor.IDownloadItemState import ir.amirab.downloader.monitor.IDownloadItemState
import ir.amirab.downloader.monitor.statusOrFinished import ir.amirab.downloader.monitor.statusOrFinished
import ir.amirab.util.compose.StringSource
import ir.amirab.util.compose.asStringSource
class DownloadStatusCategoryFilterByList( class DownloadStatusCategoryFilterByList(
name: String, name: StringSource,
icon: IconSource, icon: IconSource,
val acceptedStatus: List<DownloadStatus>, val acceptedStatus: List<DownloadStatus>,
) : DownloadStatusCategoryFilter(name, icon) { ) : DownloadStatusCategoryFilter(name, icon) {
@ -47,7 +50,7 @@ class DownloadStatusCategoryFilterByList(
} }
abstract class DownloadStatusCategoryFilter( abstract class DownloadStatusCategoryFilter(
val name: String, val name: StringSource,
val icon: IconSource, val icon: IconSource,
) { ) {
abstract fun accept(iDownloadStatus: IDownloadItemState): Boolean abstract fun accept(iDownloadStatus: IDownloadItemState): Boolean
@ -58,18 +61,18 @@ object DefinedStatusCategories {
val All = object : DownloadStatusCategoryFilter( val All = object : DownloadStatusCategoryFilter(
"All", Res.string.all.asStringSource(),
MyIcons.folder, MyIcons.folder,
) { ) {
override fun accept(iDownloadStatus: IDownloadItemState): Boolean = true override fun accept(iDownloadStatus: IDownloadItemState): Boolean = true
} }
val Finished = DownloadStatusCategoryFilterByList( val Finished = DownloadStatusCategoryFilterByList(
"Finished", Res.string.finished.asStringSource(),
MyIcons.folder, MyIcons.folder,
listOf(DownloadStatus.Completed) listOf(DownloadStatus.Completed)
) )
val Unfinished = DownloadStatusCategoryFilterByList( val Unfinished = DownloadStatusCategoryFilterByList(
"Unfinished", Res.string.Unfinished.asStringSource(),
MyIcons.folder, MyIcons.folder,
listOf( listOf(
DownloadStatus.Error, DownloadStatus.Error,
@ -203,7 +206,7 @@ fun StatusFilterItem(
) )
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
Text( Text(
statusFilter.name, statusFilter.name.rememberString(),
Modifier.weight(1f), Modifier.weight(1f),
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal, fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
fontSize = myTextSizes.lg, fontSize = myTextSizes.lg,

View File

@ -9,13 +9,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import ir.amirab.util.compose.resources.myStringResource
@Composable @Composable
fun NewQueue( fun NewQueue(
onQueueCreate: (String) -> Unit, onQueueCreate: (String) -> Unit,
onCloseRequest: () -> Unit, onCloseRequest: () -> Unit,
) { ) {
WindowTitle("New Queue") WindowTitle(myStringResource(Res.string.add_new_queue))
var name by remember { var name by remember {
mutableStateOf("") mutableStateOf("")
} }
@ -34,7 +37,7 @@ fun NewQueue(
.focusRequester(focusRequester) .focusRequester(focusRequester)
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)
.widthIn(max = 400.dp), .widthIn(max = 400.dp),
placeholder = "Queue name...", placeholder = myStringResource(Res.string.queue_name),
) )
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
@ -46,14 +49,14 @@ fun NewQueue(
horizontalArrangement = Arrangement.End, horizontalArrangement = Arrangement.End,
) { ) {
ActionButton( ActionButton(
text = "Create", text = myStringResource(Res.string.add),
onClick = { onClick = {
onQueueCreate(name) onQueueCreate(name)
} }
) )
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
ActionButton( ActionButton(
text = "Cancel", text = myStringResource(Res.string.cancel),
onClick = { onClick = {
onCloseRequest() onCloseRequest()
} }

View File

@ -7,11 +7,15 @@ import ir.amirab.util.flow.createMutableStateFlowFromStateFlow
import ir.amirab.util.flow.mapStateFlow import ir.amirab.util.flow.mapStateFlow
import com.abdownloadmanager.desktop.utils.newScopeBasedOn import com.abdownloadmanager.desktop.utils.newScopeBasedOn
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.ComponentContext
import ir.amirab.downloader.monitor.IDownloadItemState import ir.amirab.downloader.monitor.IDownloadItemState
import ir.amirab.downloader.monitor.IDownloadMonitor import ir.amirab.downloader.monitor.IDownloadMonitor
import ir.amirab.downloader.queue.DownloadQueue import ir.amirab.downloader.queue.DownloadQueue
import ir.amirab.downloader.queue.QueueManager import ir.amirab.downloader.queue.QueueManager
import ir.amirab.util.compose.asStringSource
import ir.amirab.util.compose.asStringSourceWithARgs
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@ -69,7 +73,7 @@ class QueueInfoComponent(
val configurations: List<ConfigurableGroup> = val configurations: List<ConfigurableGroup> =
createConfigurableList(downloadQueue, scope) createConfigurableList(downloadQueue, scope)
private fun createConfigurableList( private fun createConfigurableList(
@ -84,11 +88,11 @@ class QueueInfoComponent(
} }
return listOf( return listOf(
ConfigurableGroup( ConfigurableGroup(
groupTitle = MutableStateFlow("General"), groupTitle = MutableStateFlow(Res.string.general.asStringSource()),
nestedConfigurable = listOf( nestedConfigurable = listOf(
StringConfigurable( StringConfigurable(
"Name", Res.string.name.asStringSource(),
"Specify A name for this queue", Res.string.queue_name_help.asStringSource(),
backedBy = createMutableStateFlowFromStateFlow( backedBy = createMutableStateFlowFromStateFlow(
scope = scope, scope = scope,
flow = downloadQueue.queueModel.mapStateFlow() { flow = downloadQueue.queueModel.mapStateFlow() {
@ -101,11 +105,18 @@ class QueueInfoComponent(
validate = { validate = {
it.length in 1..32 it.length in 1..32
}, },
describe = { "Queue name is $it" }, describe = {
Res.string.queue_name_describe
.asStringSourceWithARgs(
Res.string.queue_name_describe_createArgs(
value = it
)
)
},
), ),
IntConfigurable( IntConfigurable(
"Max Concurrent", Res.string.queue_max_concurrent_download.asStringSource(),
"Max download for this queue", Res.string.queue_max_concurrent_download_description.asStringSource(),
backedBy = createMutableStateFlowFromStateFlow( backedBy = createMutableStateFlowFromStateFlow(
scope = scope, scope = scope,
flow = downloadQueue.queueModel.mapStateFlow() { flow = downloadQueue.queueModel.mapStateFlow() {
@ -115,13 +126,13 @@ class QueueInfoComponent(
downloadQueue.setMaxConcurrent(newValue) downloadQueue.setMaxConcurrent(newValue)
}, },
), ),
describe = { "${it}" }, describe = { "$it".asStringSource() },
range = 1..32, range = 1..32,
renderMode = IntConfigurable.RenderMode.TextField, renderMode = IntConfigurable.RenderMode.TextField,
), ),
BooleanConfigurable( BooleanConfigurable(
"Automatic stop", Res.string.queue_automatic_stop.asStringSource(),
"Automatic stop queue when there is no item in it", Res.string.queue_automatic_stop_description.asStringSource(),
backedBy = createMutableStateFlowFromStateFlow( backedBy = createMutableStateFlowFromStateFlow(
scope = scope, scope = scope,
flow = downloadQueue.queueModel.mapStateFlow() { flow = downloadQueue.queueModel.mapStateFlow() {
@ -132,19 +143,19 @@ class QueueInfoComponent(
}, },
), ),
describe = { describe = {
if (it) "Enabled" if (it) Res.string.enabled.asStringSource()
else "Disabled" else Res.string.disabled.asStringSource()
}, },
), ),
), ),
), ),
ConfigurableGroup( ConfigurableGroup(
groupTitle = MutableStateFlow("Scheduler"), groupTitle = MutableStateFlow(Res.string.queue_scheduler.asStringSource()),
nestedVisible = enabledSchedulerFlow, nestedVisible = enabledSchedulerFlow,
mainConfigurable = BooleanConfigurable( mainConfigurable = BooleanConfigurable(
"Enable Scheduler", Res.string.queue_enable_scheduler.asStringSource(),
description = "", description = "".asStringSource(),
describe = { "" }, describe = { "".asStringSource() },
backedBy = createMutableStateFlowFromStateFlow( backedBy = createMutableStateFlowFromStateFlow(
flow = enabledSchedulerFlow, flow = enabledSchedulerFlow,
scope = scope, scope = scope,
@ -157,8 +168,8 @@ class QueueInfoComponent(
), ),
nestedConfigurable = listOf( nestedConfigurable = listOf(
DayOfWeekConfigurable( DayOfWeekConfigurable(
"Active days", Res.string.queue_active_days.asStringSource(),
"which days schedulers function ?", Res.string.queue_active_days_description.asStringSource(),
backedBy = createMutableStateFlowFromStateFlow( backedBy = createMutableStateFlowFromStateFlow(
scope = scope, scope = scope,
flow = downloadQueue.queueModel.mapStateFlow() { flow = downloadQueue.queueModel.mapStateFlow() {
@ -173,11 +184,11 @@ class QueueInfoComponent(
validate = { validate = {
it.isNotEmpty() it.isNotEmpty()
}, },
describe = { "" }, describe = { "".asStringSource() },
), ),
TimeConfigurable( TimeConfigurable(
"Auto Start download", Res.string.queue_scheduler_auto_start_time.asStringSource(),
"", "".asStringSource(),
backedBy = createMutableStateFlowFromStateFlow( backedBy = createMutableStateFlowFromStateFlow(
scope = scope, scope = scope,
flow = downloadQueue.queueModel.mapStateFlow() { flow = downloadQueue.queueModel.mapStateFlow() {
@ -189,12 +200,12 @@ class QueueInfoComponent(
} }
}, },
), ),
describe = { "" }, describe = { "".asStringSource() },
), ),
BooleanConfigurable( BooleanConfigurable(
"Enable Auto Stop", Res.string.queue_scheduler_enable_auto_stop_time.asStringSource(),
description = "", description = "".asStringSource(),
describe = { "" }, describe = { "".asStringSource() },
backedBy = createMutableStateFlowFromStateFlow( backedBy = createMutableStateFlowFromStateFlow(
scope = scope, scope = scope,
flow = enabledEndTimeFlow, flow = enabledEndTimeFlow,
@ -207,8 +218,8 @@ class QueueInfoComponent(
), ),
), ),
TimeConfigurable( TimeConfigurable(
"Auto Stop download", Res.string.queue_scheduler_auto_stop_time.asStringSource(),
"", "".asStringSource(),
backedBy = createMutableStateFlowFromStateFlow( backedBy = createMutableStateFlowFromStateFlow(
scope = scope, scope = scope,
flow = downloadQueue.queueModel.mapStateFlow() { flow = downloadQueue.queueModel.mapStateFlow() {
@ -220,7 +231,7 @@ class QueueInfoComponent(
} }
}, },
), ),
describe = { "" }, describe = { "".asStringSource() },
visible = enabledEndTimeFlow, visible = enabledEndTimeFlow,
), ),
) )

View File

@ -35,10 +35,15 @@ import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import ir.amirab.util.compose.resources.myStringResource
import ir.amirab.downloader.downloaditem.DownloadJobStatus import ir.amirab.downloader.downloaditem.DownloadJobStatus
import ir.amirab.downloader.monitor.IDownloadItemState import ir.amirab.downloader.monitor.IDownloadItemState
import ir.amirab.downloader.monitor.statusOrFinished import ir.amirab.downloader.monitor.statusOrFinished
import ir.amirab.downloader.queue.DownloadQueue import ir.amirab.downloader.queue.DownloadQueue
import ir.amirab.util.compose.StringSource
import ir.amirab.util.compose.asStringSource
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.burnoutcrew.reorderable.* import org.burnoutcrew.reorderable.*
@ -47,7 +52,7 @@ import org.burnoutcrew.reorderable.*
fun QueuePage(component: QueuesComponent) { fun QueuePage(component: QueuesComponent) {
val queues = component.queuesState val queues = component.queuesState
val activeItem: DownloadQueue = component.selectedItem val activeItem: DownloadQueue = component.selectedItem
WindowTitle("Queues") WindowTitle(myStringResource(Res.string.queues))
val borderShape = RoundedCornerShape(6.dp) val borderShape = RoundedCornerShape(6.dp)
val borderColor = myColors.onBackground / 5 val borderColor = myColors.onBackground / 5
Column { Column {
@ -100,11 +105,13 @@ private fun Actions(
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
} }
ActionButton( ActionButton(
text = if (isActive) { text = myStringResource(
"Stop Queue" if (isActive) {
} else { Res.string.stop_queue
"Start Queue" } else {
}, Res.string.start_queue
}
),
modifier = Modifier, modifier = Modifier,
onClick = { onClick = {
scope.launch { scope.launch {
@ -118,7 +125,7 @@ private fun Actions(
) )
space() space()
ActionButton( ActionButton(
text = "Close", text = myStringResource(Res.string.close),
modifier = Modifier, modifier = Modifier,
onClick = { onClick = {
component.close() component.close()
@ -127,9 +134,9 @@ private fun Actions(
} }
} }
enum class QueueInfoPages(val title: String, val icon: IconSource) { enum class QueueInfoPages(val title: StringSource, val icon: IconSource) {
Config("Config", MyIcons.settings), Config(Res.string.config.asStringSource(), MyIcons.settings),
Items("Items", MyIcons.queue), Items(Res.string.items.asStringSource(), MyIcons.queue),
} }
@Composable @Composable
@ -232,7 +239,7 @@ fun RenderQueueItems(
val space = 4.dp val space = 4.dp
IconActionButton( IconActionButton(
icon = MyIcons.remove, icon = MyIcons.remove,
contentDescription = "remove", contentDescription = myStringResource(Res.string.remove),
onClick = { onClick = {
component.deleteItems() component.deleteItems()
}, },
@ -241,7 +248,7 @@ fun RenderQueueItems(
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
IconActionButton( IconActionButton(
icon = MyIcons.down, icon = MyIcons.down,
contentDescription = "Move down", contentDescription = myStringResource(Res.string.move_down),
onClick = { onClick = {
component.moveDownItems() component.moveDownItems()
}, },
@ -250,7 +257,7 @@ fun RenderQueueItems(
Spacer(Modifier.width(space)) Spacer(Modifier.width(space))
IconActionButton( IconActionButton(
icon = MyIcons.up, icon = MyIcons.up,
contentDescription = "Move up", contentDescription = myStringResource(Res.string.move_up),
onClick = { onClick = {
component.moveUpItems() component.moveUpItems()
}, },
@ -413,7 +420,7 @@ private fun QueueListSection(
) { ) {
IconActionButton( IconActionButton(
icon = MyIcons.add, icon = MyIcons.add,
contentDescription = "Add Queue", contentDescription = myStringResource(Res.string.add_new_queue),
onClick = { onClick = {
component.addQueue() component.addQueue()
} }
@ -421,7 +428,7 @@ private fun QueueListSection(
spacer() spacer()
IconActionButton( IconActionButton(
icon = MyIcons.remove, icon = MyIcons.remove,
contentDescription = "Delete Queue", contentDescription = myStringResource(Res.string.remove_queue),
enabled = component.canDeleteThisQueue(selectedItem), enabled = component.canDeleteThisQueue(selectedItem),
onClick = { onClick = {
component.requestDeleteQueue(selectedItem) component.requestDeleteQueue(selectedItem)

View File

@ -11,25 +11,32 @@ import com.abdownloadmanager.desktop.utils.convertSpeedToHumanReadable
import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects
import com.abdownloadmanager.desktop.utils.mvi.supportEffects import com.abdownloadmanager.desktop.utils.mvi.supportEffects
import androidx.compose.runtime.* import androidx.compose.runtime.*
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.utils.proxy.ProxyManager import com.abdownloadmanager.utils.proxy.ProxyManager
import com.abdownloadmanager.utils.proxy.ProxyMode import com.abdownloadmanager.utils.proxy.ProxyMode
import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.ComponentContext
import ir.amirab.util.compose.StringSource
import ir.amirab.util.compose.asStringSource
import ir.amirab.util.compose.asStringSourceWithARgs
import ir.amirab.util.compose.localizationmanager.LanguageInfo
import ir.amirab.util.compose.localizationmanager.LanguageManager
import ir.amirab.util.osfileutil.FileUtils import ir.amirab.util.osfileutil.FileUtils
import ir.amirab.util.flow.createMutableStateFlowFromStateFlow import ir.amirab.util.flow.createMutableStateFlowFromStateFlow
import ir.amirab.util.flow.mapStateFlow
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
sealed class SettingSections( sealed class SettingSections(
val icon: IconSource, val icon: IconSource,
val name: String, val name: StringSource,
) { ) {
data object Appearance : SettingSections(MyIcons.appearance, "Appearance") data object Appearance : SettingSections(MyIcons.appearance, Res.string.appearance.asStringSource())
// TODO ADD Network section (proxy , etc..) // TODO ADD Network section (proxy , etc..)
// data object Network : SettingSections(MyIcons.network, "Network") // data object Network : SettingSections(MyIcons.network, "Network")
data object DownloadEngine : SettingSections(MyIcons.downloadEngine, "Download Engine") data object DownloadEngine : SettingSections(MyIcons.downloadEngine, Res.string.download_engine.asStringSource())
data object BrowserIntegration : SettingSections(MyIcons.network, "Browser Integration") data object BrowserIntegration : SettingSections(MyIcons.network, Res.string.browser_integration.asStringSource())
} }
interface SettingSectionGetter { interface SettingSectionGetter {
@ -38,27 +45,32 @@ interface SettingSectionGetter {
fun threadCountConfig(appRepository: AppRepository): IntConfigurable { fun threadCountConfig(appRepository: AppRepository): IntConfigurable {
return IntConfigurable( return IntConfigurable(
title = "Thread Count", title = Res.string.settings_download_thread_count.asStringSource(),
description = "Maximum download thread per download item", description = Res.string.settings_download_thread_count_description.asStringSource(),
backedBy = appRepository.threadCount, backedBy = appRepository.threadCount,
range = 1..32, range = 1..32,
renderMode = IntConfigurable.RenderMode.TextField, renderMode = IntConfigurable.RenderMode.TextField,
describe = { describe = {
"a download can have up to $it thread" Res.string.settings_download_thread_count_describe
.asStringSourceWithARgs(
Res.string.settings_download_thread_count_describe_createArgs(
count = it.toString()
)
)
}, },
) )
} }
fun dynamicPartDownloadConfig(appRepository: AppRepository): BooleanConfigurable { fun dynamicPartDownloadConfig(appRepository: AppRepository): BooleanConfigurable {
return BooleanConfigurable( return BooleanConfigurable(
title = "Dynamic part creation", title = Res.string.settings_dynamic_part_creation.asStringSource(),
description = "When a part is finished create another part by splitting other parts to improve download speed", description = Res.string.settings_dynamic_part_creation_description.asStringSource(),
backedBy = appRepository.dynamicPartCreation, backedBy = appRepository.dynamicPartCreation,
describe = { describe = {
if (it) { if (it) {
"Enabled" Res.string.enabled.asStringSource()
} else { } else {
"Disabled" Res.string.disabled.asStringSource()
} }
}, },
) )
@ -66,14 +78,14 @@ fun dynamicPartDownloadConfig(appRepository: AppRepository): BooleanConfigurable
fun useServerLastModified(appRepository: AppRepository): BooleanConfigurable { fun useServerLastModified(appRepository: AppRepository): BooleanConfigurable {
return BooleanConfigurable( return BooleanConfigurable(
title = "Server's Last-Modified Time", title = Res.string.settings_use_server_last_modified_time.asStringSource(),
description = "When downloading a file, use server's last modified time for the local file", description = Res.string.settings_use_server_last_modified_time_description.asStringSource(),
backedBy = appRepository.useServerLastModifiedTime, backedBy = appRepository.useServerLastModifiedTime,
describe = { describe = {
if (it) { if (it) {
"Enabled" Res.string.enabled.asStringSource()
} else { } else {
"Disabled" Res.string.disabled.asStringSource()
} }
}, },
) )
@ -81,14 +93,14 @@ fun useServerLastModified(appRepository: AppRepository): BooleanConfigurable {
fun useSparseFileAllocation(appRepository: AppRepository): BooleanConfigurable { fun useSparseFileAllocation(appRepository: AppRepository): BooleanConfigurable {
return BooleanConfigurable( return BooleanConfigurable(
title = "Sparse File Allocation", title = Res.string.settings_use_sparse_file_allocation.asStringSource(),
description = "Create files more efficiently, especially on SSDs, by reducing unnecessary data writing. This can speed up download starts and save disk space. If downloads start slowly, consider disabling this option, as it may not be properly supported on some devices.", description = Res.string.settings_use_sparse_file_allocation_description.asStringSource(),
backedBy = appRepository.useSparseFileAllocation, backedBy = appRepository.useSparseFileAllocation,
describe = { describe = {
if (it) { if (it) {
"Enabled" Res.string.enabled.asStringSource()
} else { } else {
"Disabled" Res.string.disabled.asStringSource()
} }
}, },
) )
@ -96,14 +108,14 @@ fun useSparseFileAllocation(appRepository: AppRepository): BooleanConfigurable {
fun speedLimitConfig(appRepository: AppRepository): SpeedLimitConfigurable { fun speedLimitConfig(appRepository: AppRepository): SpeedLimitConfigurable {
return SpeedLimitConfigurable( return SpeedLimitConfigurable(
title = "Global Speed Limiter", title = Res.string.settings_global_speed_limiter.asStringSource(),
description = "Global download speed limit (0 means unlimited)", description = Res.string.settings_global_speed_limiter_description.asStringSource(),
backedBy = appRepository.speedLimiter, backedBy = appRepository.speedLimiter,
describe = { describe = {
if (it == 0L) { if (it == 0L) {
"Unlimited" Res.string.unlimited.asStringSource()
} else { } else {
convertSpeedToHumanReadable(it) convertSpeedToHumanReadable(it).asStringSource()
} }
} }
) )
@ -111,48 +123,56 @@ fun speedLimitConfig(appRepository: AppRepository): SpeedLimitConfigurable {
fun useAverageSpeedConfig(appRepository: AppRepository): BooleanConfigurable { fun useAverageSpeedConfig(appRepository: AppRepository): BooleanConfigurable {
return BooleanConfigurable( return BooleanConfigurable(
title = "Show Average Speed", title = Res.string.settings_show_average_speed.asStringSource(),
description = "Download speed in average or precision", description = Res.string.settings_show_average_speed_description.asStringSource(),
backedBy = appRepository.useAverageSpeed, backedBy = appRepository.useAverageSpeed,
describe = { describe = {
if (it) "Average Speed" if (it) Res.string.average_speed.asStringSource()
else "Exact Speed" else Res.string.exact_speed.asStringSource()
} }
) )
} }
fun defaultDownloadFolderConfig(appSettings: AppSettingsStorage): FolderConfigurable { fun defaultDownloadFolderConfig(appSettings: AppSettingsStorage): FolderConfigurable {
return FolderConfigurable( return FolderConfigurable(
title = "Default Download Folder", title = Res.string.settings_default_download_folder.asStringSource(),
description = "When you add new download this location is used by default", description = Res.string.settings_default_download_folder_description.asStringSource(),
backedBy = appSettings.defaultDownloadFolder, backedBy = appSettings.defaultDownloadFolder,
validate = { validate = {
FileUtils.canWriteInThisFolder(it) FileUtils.canWriteInThisFolder(it)
}, },
describe = { describe = {
"\"$it\" will be used" Res.string
.settings_default_download_folder_describe
.asStringSourceWithARgs(
Res.string.settings_default_download_folder_describe_createArgs(
folder = it
)
)
} }
) )
} }
fun proxyConfig(proxyManager: ProxyManager, scope: CoroutineScope): ProxyConfigurable { fun proxyConfig(proxyManager: ProxyManager, scope: CoroutineScope): ProxyConfigurable {
return ProxyConfigurable( return ProxyConfigurable(
title = "Use Proxy", title = Res.string.settings_use_proxy.asStringSource(),
description = "Use proxy for downloading files", description = Res.string.settings_use_proxy_description.asStringSource(),
backedBy = proxyManager.proxyData, backedBy = proxyManager.proxyData,
validate = { validate = {
true true
}, },
describe = { describe = {
val str = when (it.proxyMode) { when (it.proxyMode) {
ProxyMode.Direct -> "No proxy" ProxyMode.Direct -> Res.string.settings_use_proxy_describe_no_proxy.asStringSource()
ProxyMode.UseSystem -> "System proxy" ProxyMode.UseSystem -> Res.string.settings_use_proxy_describe_system_proxy.asStringSource()
ProxyMode.Manual -> it.proxyWithRules.proxy.run { ProxyMode.Manual -> Res.string.settings_use_proxy_describe_manual_proxy
"$type $host:$port" .asStringSourceWithARgs(
} Res.string.settings_use_proxy_describe_manual_proxy_createArgs(
value = it.proxyWithRules.proxy.run { "$type $host:$port" }
)
)
} }
"$str will be used"
} }
) )
} }
@ -192,8 +212,8 @@ fun themeConfig(
val currentThemeName = themeManager.currentThemeInfo val currentThemeName = themeManager.currentThemeInfo
val themes = themeManager.possibleThemesToSelect val themes = themeManager.possibleThemesToSelect
return ThemeConfigurable( return ThemeConfigurable(
title = "Theme", title = Res.string.settings_theme.asStringSource(),
description = "Select theme", description = Res.string.settings_theme_description.asStringSource(),
backedBy = createMutableStateFlowFromStateFlow( backedBy = createMutableStateFlowFromStateFlow(
flow = currentThemeName, flow = currentThemeName,
updater = { updater = {
@ -203,21 +223,48 @@ fun themeConfig(
), ),
possibleValues = themes.value, possibleValues = themes.value,
describe = { describe = {
it.name it.name.asStringSource()
},
)
}
fun languageConfig(
languageManager: LanguageManager,
scope: CoroutineScope,
): EnumConfigurable<LanguageInfo> {
val currentLanguageName = languageManager.selectedLanguage
val allLanguages = languageManager.languageList
return EnumConfigurable(
title = Res.string.settings_language.asStringSource(),
description = "".asStringSource(),
backedBy = createMutableStateFlowFromStateFlow(
flow = currentLanguageName.mapStateFlow { l ->
allLanguages.value.find {
it.languageCode == l
} ?: LanguageManager.DefaultLanguageInfo
},
updater = {
languageManager.selectLanguage(it.languageCode)
},
scope = scope,
),
possibleValues = allLanguages.value,
describe = {
it.nativeName.asStringSource()
}, },
) )
} }
fun mergeTopBarWithTitleBarConfig(appSettings: AppSettingsStorage): BooleanConfigurable { fun mergeTopBarWithTitleBarConfig(appSettings: AppSettingsStorage): BooleanConfigurable {
return BooleanConfigurable( return BooleanConfigurable(
title = "Compact Top Bar", title = Res.string.settings_compact_top_bar.asStringSource(),
description = "Merge top bar with title bar when the main window has enough width", description = Res.string.settings_compact_top_bar_description.asStringSource(),
backedBy = appSettings.mergeTopBarWithTitleBar, backedBy = appSettings.mergeTopBarWithTitleBar,
describe = { describe = {
if (it) { if (it) {
"Enabled" Res.string.enabled.asStringSource()
} else { } else {
"Disabled" Res.string.disabled.asStringSource()
} }
}, },
) )
@ -225,15 +272,15 @@ fun mergeTopBarWithTitleBarConfig(appSettings: AppSettingsStorage): BooleanConfi
fun autoStartConfig(appSettings: AppSettingsStorage): BooleanConfigurable { fun autoStartConfig(appSettings: AppSettingsStorage): BooleanConfigurable {
return BooleanConfigurable( return BooleanConfigurable(
title = "Start On Boot", title = Res.string.settings_start_on_boot.asStringSource(),
description = "Auto start application on user logins", description = Res.string.settings_start_on_boot_description.asStringSource(),
backedBy = appSettings.autoStartOnBoot, backedBy = appSettings.autoStartOnBoot,
renderMode = BooleanConfigurable.RenderMode.Switch, renderMode = BooleanConfigurable.RenderMode.Switch,
describe = { describe = {
if (it) { if (it) {
"Auto Start Enabled" Res.string.enabled.asStringSource()
} else { } else {
"Auto Start Disabled" Res.string.disabled.asStringSource()
} }
} }
) )
@ -241,15 +288,15 @@ fun autoStartConfig(appSettings: AppSettingsStorage): BooleanConfigurable {
fun playSoundNotification(appSettings: AppSettingsStorage): BooleanConfigurable { fun playSoundNotification(appSettings: AppSettingsStorage): BooleanConfigurable {
return BooleanConfigurable( return BooleanConfigurable(
title = "Notification Sound", title = Res.string.settings_notification_sound.asStringSource(),
description = "Play sound on new notification", description = Res.string.settings_notification_sound_description.asStringSource(),
backedBy = appSettings.notificationSound, backedBy = appSettings.notificationSound,
renderMode = BooleanConfigurable.RenderMode.Switch, renderMode = BooleanConfigurable.RenderMode.Switch,
describe = { describe = {
if (it) { if (it) {
"Play sounds" Res.string.enabled.asStringSource()
} else { } else {
"Muted" Res.string.disabled.asStringSource()
} }
} }
) )
@ -257,15 +304,15 @@ fun playSoundNotification(appSettings: AppSettingsStorage): BooleanConfigurable
fun browserIntegrationEnabled(appRepository: AppRepository): BooleanConfigurable { fun browserIntegrationEnabled(appRepository: AppRepository): BooleanConfigurable {
return BooleanConfigurable( return BooleanConfigurable(
title = "Browser Integration", title = Res.string.settings_browser_integration.asStringSource(),
description = "Accept downloads from browsers", description = Res.string.settings_browser_integration_description.asStringSource(),
backedBy = appRepository.integrationEnabled, backedBy = appRepository.integrationEnabled,
renderMode = BooleanConfigurable.RenderMode.Switch, renderMode = BooleanConfigurable.RenderMode.Switch,
describe = { describe = {
if (it) { if (it) {
"Enabled" Res.string.enabled.asStringSource()
} else { } else {
"Disabled" Res.string.disabled.asStringSource()
} }
} }
) )
@ -273,11 +320,16 @@ fun browserIntegrationEnabled(appRepository: AppRepository): BooleanConfigurable
fun browserIntegrationPort(appRepository: AppRepository): IntConfigurable { fun browserIntegrationPort(appRepository: AppRepository): IntConfigurable {
return IntConfigurable( return IntConfigurable(
title = "Server Port", title = Res.string.settings_browser_integration_server_port.asStringSource(),
description = "port for browser integration", description = Res.string.settings_browser_integration_server_port_description.asStringSource(),
backedBy = appRepository.integrationPort, backedBy = appRepository.integrationPort,
describe = { describe = {
"listen to $it" Res.string.settings_browser_integration_server_port_describe
.asStringSourceWithARgs(
Res.string.settings_browser_integration_server_port_describe_createArgs(
port = it.toString()
)
)
}, },
range = 0..65000, range = 0..65000,
) )
@ -296,11 +348,13 @@ class SettingsComponent(
val appRepository by inject<AppRepository>() val appRepository by inject<AppRepository>()
val proxyManager by inject<ProxyManager>() val proxyManager by inject<ProxyManager>()
val themeManager by inject<ThemeManager>() val themeManager by inject<ThemeManager>()
val languageManager by inject<LanguageManager>()
val allConfigs = object : SettingSectionGetter { val allConfigs = object : SettingSectionGetter {
override operator fun get(key: SettingSections): List<Configurable<*>> { override operator fun get(key: SettingSections): List<Configurable<*>> {
return when (key) { return when (key) {
Appearance -> listOf( Appearance -> listOf(
themeConfig(themeManager, scope), themeConfig(themeManager, scope),
languageConfig(languageManager, scope),
// uiScaleConfig(appSettings), // uiScaleConfig(appSettings),
autoStartConfig(appSettings), autoStartConfig(appSettings),
mergeTopBarWithTitleBarConfig(appSettings), mergeTopBarWithTitleBarConfig(appSettings),

View File

@ -23,6 +23,9 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import ir.amirab.util.compose.resources.myStringResource
import kotlinx.coroutines.channels.ticker import kotlinx.coroutines.channels.ticker
@Composable @Composable
@ -46,7 +49,7 @@ private fun SideBar(
for (i in settingsComponent.pages) { for (i in settingsComponent.pages) {
SideBarItem( SideBarItem(
icon = i.icon, icon = i.icon,
name = i.name, name = i.name.rememberString(),
isSelected = settingsComponent.currentPage == i, isSelected = settingsComponent.currentPage == i,
onClick = { onClick = {
settingsComponent.currentPage = i settingsComponent.currentPage = i
@ -101,7 +104,7 @@ fun SettingsPage(
settingsComponent: SettingsComponent, settingsComponent: SettingsComponent,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
) { ) {
WindowTitle("Settings") WindowTitle(myStringResource(Res.string.settings))
// WindowIcon(MyIcons.settings) // WindowIcon(MyIcons.settings)
WindowIcon(MyIcons.appIcon) WindowIcon(MyIcons.appIcon)
Row { Row {

View File

@ -6,6 +6,7 @@ import com.abdownloadmanager.desktop.pages.settings.ThemeInfo
import com.abdownloadmanager.desktop.pages.settings.configurable.BooleanConfigurable.RenderMode import com.abdownloadmanager.desktop.pages.settings.configurable.BooleanConfigurable.RenderMode
import com.abdownloadmanager.desktop.ui.theme.MyColors import com.abdownloadmanager.desktop.ui.theme.MyColors
import com.abdownloadmanager.utils.proxy.ProxyData import com.abdownloadmanager.utils.proxy.ProxyData
import ir.amirab.util.compose.StringSource
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -17,11 +18,11 @@ private val DefaultEnabledValue get() = MutableStateFlow(true)
private val DefaultVisibleValue get() = MutableStateFlow(true) private val DefaultVisibleValue get() = MutableStateFlow(true)
sealed class Configurable<T>( sealed class Configurable<T>(
val title: String, val title: StringSource,
val description: String, val description: StringSource,
val backedBy: MutableStateFlow<T>, val backedBy: MutableStateFlow<T>,
val validate: (T) -> Boolean = { true }, val validate: (T) -> Boolean = { true },
val describe: (T) -> String, val describe: (T) -> StringSource,
val enabled: StateFlow<Boolean> = DefaultEnabledValue, val enabled: StateFlow<Boolean> = DefaultEnabledValue,
val visible: StateFlow<Boolean> = DefaultVisibleValue, val visible: StateFlow<Boolean> = DefaultVisibleValue,
) { ) {
@ -39,10 +40,10 @@ sealed class Configurable<T>(
//primitives //primitives
class IntConfigurable( class IntConfigurable(
title: String, title: StringSource,
description: String, description: StringSource,
backedBy: MutableStateFlow<Int>, backedBy: MutableStateFlow<Int>,
describe: ((Int) -> String), describe: ((Int) -> StringSource),
val range: IntRange, val range: IntRange,
val renderMode: RenderMode = RenderMode.TextField, val renderMode: RenderMode = RenderMode.TextField,
enabled: StateFlow<Boolean> = DefaultEnabledValue, enabled: StateFlow<Boolean> = DefaultEnabledValue,
@ -62,10 +63,10 @@ class IntConfigurable(
} }
sealed class BaseLongConfigurable( sealed class BaseLongConfigurable(
title: String, title: StringSource,
description: String, description: StringSource,
backedBy: MutableStateFlow<Long>, backedBy: MutableStateFlow<Long>,
describe: ((Long) -> String), describe: ((Long) -> StringSource),
val range: LongRange, val range: LongRange,
enabled: StateFlow<Boolean> = DefaultEnabledValue, enabled: StateFlow<Boolean> = DefaultEnabledValue,
visible: StateFlow<Boolean> = DefaultVisibleValue, visible: StateFlow<Boolean> = DefaultVisibleValue,
@ -82,10 +83,10 @@ sealed class BaseLongConfigurable(
) )
class LongConfigurable( class LongConfigurable(
title: String, title: StringSource,
description: String, description: StringSource,
backedBy: MutableStateFlow<Long>, backedBy: MutableStateFlow<Long>,
describe: ((Long) -> String), describe: ((Long) -> StringSource),
range: LongRange, range: LongRange,
val renderMode: RenderMode = RenderMode.TextField, val renderMode: RenderMode = RenderMode.TextField,
enabled: StateFlow<Boolean> = DefaultEnabledValue, enabled: StateFlow<Boolean> = DefaultEnabledValue,
@ -105,10 +106,10 @@ class LongConfigurable(
} }
class BooleanConfigurable( class BooleanConfigurable(
title: String, title: StringSource,
description: String, description: StringSource,
backedBy: MutableStateFlow<Boolean>, backedBy: MutableStateFlow<Boolean>,
describe: ((Boolean) -> String), describe: ((Boolean) -> StringSource),
val renderMode: RenderMode = RenderMode.Switch, val renderMode: RenderMode = RenderMode.Switch,
enabled: StateFlow<Boolean> = DefaultEnabledValue, enabled: StateFlow<Boolean> = DefaultEnabledValue,
visible: StateFlow<Boolean> = DefaultVisibleValue, visible: StateFlow<Boolean> = DefaultVisibleValue,
@ -127,14 +128,14 @@ class BooleanConfigurable(
} }
class FloatConfigurable( class FloatConfigurable(
title: String, title: StringSource,
description: String, description: StringSource,
backedBy: MutableStateFlow<Float>, backedBy: MutableStateFlow<Float>,
val range: ClosedFloatingPointRange<Float>, val range: ClosedFloatingPointRange<Float>,
val steps: Int = 0, val steps: Int = 0,
val renderMode: RenderMode = RenderMode.TextField, val renderMode: RenderMode = RenderMode.TextField,
describe: ((Float) -> String), describe: ((Float) -> StringSource),
enabled: StateFlow<Boolean> = DefaultEnabledValue, enabled: StateFlow<Boolean> = DefaultEnabledValue,
visible: StateFlow<Boolean> = DefaultVisibleValue, visible: StateFlow<Boolean> = DefaultVisibleValue,
) : Configurable<Float>( ) : Configurable<Float>(
@ -152,10 +153,10 @@ class FloatConfigurable(
} }
open class StringConfigurable( open class StringConfigurable(
title: String, title: StringSource,
description: String, description: StringSource,
backedBy: MutableStateFlow<String>, backedBy: MutableStateFlow<String>,
describe: ((String) -> String), describe: ((String) -> StringSource),
validate: (String) -> Boolean = { true }, validate: (String) -> Boolean = { true },
enabled: StateFlow<Boolean> = DefaultEnabledValue, enabled: StateFlow<Boolean> = DefaultEnabledValue,
visible: StateFlow<Boolean> = DefaultVisibleValue, visible: StateFlow<Boolean> = DefaultVisibleValue,
@ -170,10 +171,10 @@ open class StringConfigurable(
) )
class FolderConfigurable( class FolderConfigurable(
title: String, title: StringSource,
description: String, description: StringSource,
backedBy: MutableStateFlow<String>, backedBy: MutableStateFlow<String>,
describe: ((String) -> String), describe: ((String) -> StringSource),
validate: (String) -> Boolean, validate: (String) -> Boolean,
enabled: StateFlow<Boolean> = DefaultEnabledValue, enabled: StateFlow<Boolean> = DefaultEnabledValue,
visible: StateFlow<Boolean> = DefaultVisibleValue, visible: StateFlow<Boolean> = DefaultVisibleValue,
@ -188,10 +189,10 @@ class FolderConfigurable(
) )
sealed class BaseEnumConfigurable<T>( sealed class BaseEnumConfigurable<T>(
title: String, title: StringSource,
description: String, description: StringSource,
backedBy: MutableStateFlow<T>, backedBy: MutableStateFlow<T>,
describe: ((T) -> String), describe: ((T) -> StringSource),
val possibleValues: List<T>, val possibleValues: List<T>,
enabled: StateFlow<Boolean> = DefaultEnabledValue, enabled: StateFlow<Boolean> = DefaultEnabledValue,
visible: StateFlow<Boolean> = DefaultVisibleValue, visible: StateFlow<Boolean> = DefaultVisibleValue,
@ -209,10 +210,10 @@ sealed class BaseEnumConfigurable<T>(
//more complex //more complex
open class EnumConfigurable<T>( open class EnumConfigurable<T>(
title: String, title: StringSource,
description: String, description: StringSource,
backedBy: MutableStateFlow<T>, backedBy: MutableStateFlow<T>,
describe: ((T) -> String), describe: ((T) -> StringSource),
possibleValues: List<T>, possibleValues: List<T>,
val renderMode: RenderMode = RenderMode.Spinner, val renderMode: RenderMode = RenderMode.Spinner,
enabled: StateFlow<Boolean> = DefaultEnabledValue, enabled: StateFlow<Boolean> = DefaultEnabledValue,
@ -232,10 +233,10 @@ open class EnumConfigurable<T>(
} }
class ThemeConfigurable( class ThemeConfigurable(
title: String, title: StringSource,
description: String, description: StringSource,
backedBy: MutableStateFlow<ThemeInfo>, backedBy: MutableStateFlow<ThemeInfo>,
describe: (ThemeInfo) -> String, describe: (ThemeInfo) -> StringSource,
possibleValues: List<ThemeInfo>, possibleValues: List<ThemeInfo>,
enabled: StateFlow<Boolean> = DefaultEnabledValue, enabled: StateFlow<Boolean> = DefaultEnabledValue,
visible: StateFlow<Boolean> = DefaultVisibleValue, visible: StateFlow<Boolean> = DefaultVisibleValue,
@ -250,10 +251,10 @@ class ThemeConfigurable(
) )
class SpeedLimitConfigurable( class SpeedLimitConfigurable(
title: String, title: StringSource,
description: String, description: StringSource,
backedBy: MutableStateFlow<Long>, backedBy: MutableStateFlow<Long>,
describe: (Long) -> String, describe: (Long) -> StringSource,
enabled: StateFlow<Boolean> = DefaultEnabledValue, enabled: StateFlow<Boolean> = DefaultEnabledValue,
visible: StateFlow<Boolean> = DefaultVisibleValue, visible: StateFlow<Boolean> = DefaultVisibleValue,
) : BaseLongConfigurable( ) : BaseLongConfigurable(
@ -267,10 +268,10 @@ class SpeedLimitConfigurable(
) )
class TimeConfigurable( class TimeConfigurable(
title: String, title: StringSource,
description: String, description: StringSource,
backedBy: MutableStateFlow<LocalTime>, backedBy: MutableStateFlow<LocalTime>,
describe: (LocalTime) -> String, describe: (LocalTime) -> StringSource,
enabled: StateFlow<Boolean> = DefaultEnabledValue, enabled: StateFlow<Boolean> = DefaultEnabledValue,
visible: StateFlow<Boolean> = DefaultVisibleValue, visible: StateFlow<Boolean> = DefaultVisibleValue,
) : Configurable<LocalTime>( ) : Configurable<LocalTime>(
@ -283,10 +284,10 @@ class TimeConfigurable(
) )
class DayOfWeekConfigurable( class DayOfWeekConfigurable(
title: String, title: StringSource,
description: String, description: StringSource,
backedBy: MutableStateFlow<Set<DayOfWeek>>, backedBy: MutableStateFlow<Set<DayOfWeek>>,
describe: (Set<DayOfWeek>) -> String, describe: (Set<DayOfWeek>) -> StringSource,
validate: (Set<DayOfWeek>) -> Boolean, validate: (Set<DayOfWeek>) -> Boolean,
enabled: StateFlow<Boolean> = DefaultEnabledValue, enabled: StateFlow<Boolean> = DefaultEnabledValue,
visible: StateFlow<Boolean> = DefaultVisibleValue, visible: StateFlow<Boolean> = DefaultVisibleValue,
@ -301,10 +302,10 @@ class DayOfWeekConfigurable(
) )
class ProxyConfigurable( class ProxyConfigurable(
title: String, title: StringSource,
description: String, description: StringSource,
backedBy: MutableStateFlow<ProxyData>, backedBy: MutableStateFlow<ProxyData>,
describe: (ProxyData) -> String, describe: (ProxyData) -> StringSource,
validate: (ProxyData) -> Boolean, validate: (ProxyData) -> Boolean,
enabled: StateFlow<Boolean> = DefaultEnabledValue, enabled: StateFlow<Boolean> = DefaultEnabledValue,
visible: StateFlow<Boolean> = DefaultVisibleValue, visible: StateFlow<Boolean> = DefaultVisibleValue,

View File

@ -2,12 +2,13 @@ package com.abdownloadmanager.desktop.pages.settings.configurable.widgets
import com.abdownloadmanager.desktop.pages.settings.configurable.Configurable import com.abdownloadmanager.desktop.pages.settings.configurable.Configurable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import ir.amirab.util.compose.StringSource
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@Stable @Stable
data class ConfigurableGroup( data class ConfigurableGroup(
val groupTitle:StateFlow<String?> = MutableStateFlow(null), val groupTitle: StateFlow<StringSource?> = MutableStateFlow(null),
val mainConfigurable:Configurable<*>?=null, val mainConfigurable:Configurable<*>?=null,
val nestedEnabled:StateFlow<Boolean> =MutableStateFlow(true), val nestedEnabled:StateFlow<Boolean> =MutableStateFlow(true),
val nestedVisible:StateFlow<Boolean> =MutableStateFlow(true), val nestedVisible:StateFlow<Boolean> =MutableStateFlow(true),

View File

@ -32,7 +32,7 @@ fun RenderEnumConfig(cfg: EnumConfigurable<Any?>, modifier: Modifier) {
modifier = Modifier.widthIn(min = 160.dp), modifier = Modifier.widthIn(min = 160.dp),
enabled = enabled, enabled = enabled,
render = { render = {
Text(cfg.describe(it)) Text(cfg.describe(it).rememberString())
}) })
} }
} }

View File

@ -23,7 +23,7 @@ fun RenderFolderConfig(cfg: FolderConfigurable, modifier: Modifier) {
val setValue = cfg::set val setValue = cfg::set
val pickFolderLauncher = rememberDirectoryPickerLauncher( val pickFolderLauncher = rememberDirectoryPickerLauncher(
title = cfg.title, title = cfg.title.rememberString(),
initialDirectory = remember(value) { initialDirectory = remember(value) {
runCatching { runCatching {
File(value).canonicalPath File(value).canonicalPath
@ -51,7 +51,7 @@ fun RenderFolderConfig(cfg: FolderConfigurable, modifier: Modifier) {
}, },
shape = RectangleShape, shape = RectangleShape,
textPadding = PaddingValues(4.dp), textPadding = PaddingValues(4.dp),
placeholder = cfg.title, placeholder = cfg.title.rememberString(),
end = { end = {
MyIcon( MyIcon(
icon = MyIcons.folder, icon = MyIcons.folder,

View File

@ -32,7 +32,7 @@ fun RenderConfigurableGroup(
.padding(start = verticalPadding.dp) .padding(start = verticalPadding.dp)
.padding(horizontal = 4.dp) .padding(horizontal = 4.dp)
) { ) {
title?.let { title?.rememberString()?.let {
Text( Text(
text = it, text = it,
fontSize = myTextSizes.base, fontSize = myTextSizes.base,

View File

@ -18,7 +18,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.abdownloadmanager.resources.Res
import ir.amirab.util.compose.StringSource
import ir.amirab.util.compose.asStringSource
import kotlinx.datetime.DayOfWeek import kotlinx.datetime.DayOfWeek
import java.time.DayOfWeek.*
@Composable @Composable
fun RenderDayOfWeekConfigurable(cfg: DayOfWeekConfigurable, modifier: Modifier) { fun RenderDayOfWeekConfigurable(cfg: DayOfWeekConfigurable, modifier: Modifier) {
@ -31,7 +35,7 @@ fun RenderDayOfWeekConfigurable(cfg: DayOfWeekConfigurable, modifier: Modifier)
} }
fun selectDay(dayOfWeek: DayOfWeek, select: Boolean) { fun selectDay(dayOfWeek: DayOfWeek, select: Boolean) {
if (!enabled)return if (!enabled) return
if (select) { if (select) {
setValue( setValue(
value.plus(dayOfWeek).sorted().toSet() value.plus(dayOfWeek).sorted().toSet()
@ -52,7 +56,7 @@ fun RenderDayOfWeekConfigurable(cfg: DayOfWeekConfigurable, modifier: Modifier)
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Row( Row(
Modifier.ifThen(!enabled){ Modifier.ifThen(!enabled) {
alpha(0.5f) alpha(0.5f)
} }
) { ) {
@ -61,7 +65,7 @@ fun RenderDayOfWeekConfigurable(cfg: DayOfWeekConfigurable, modifier: Modifier)
col.forEach { dayOfWeek -> col.forEach { dayOfWeek ->
RenderDayOfWeek( RenderDayOfWeek(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
enabled=enabled, enabled = enabled,
dayOfWeek = dayOfWeek, dayOfWeek = dayOfWeek,
selected = isSelected(dayOfWeek), selected = isSelected(dayOfWeek),
onSelect = { s, isSelected -> onSelect = { s, isSelected ->
@ -83,7 +87,7 @@ fun RenderDayOfWeek(
dayOfWeek: DayOfWeek, dayOfWeek: DayOfWeek,
selected: Boolean, selected: Boolean,
onSelect: (DayOfWeek, Boolean) -> Unit, onSelect: (DayOfWeek, Boolean) -> Unit,
enabled: Boolean=true, enabled: Boolean = true,
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -93,7 +97,7 @@ fun RenderDayOfWeek(
.ifThen(selected) { .ifThen(selected) {
background(myColors.onBackground / 10) background(myColors.onBackground / 10)
} }
.clickable(enabled=enabled) { .clickable(enabled = enabled) {
onSelect(dayOfWeek, !selected) onSelect(dayOfWeek, !selected)
} }
.padding(vertical = 2.dp) .padding(vertical = 2.dp)
@ -104,16 +108,26 @@ fun RenderDayOfWeek(
MyIcons.check, MyIcons.check,
null, null,
Modifier.size(8.dp) Modifier.size(8.dp)
.alpha(if (selected)1f else 0f ), .alpha(if (selected) 1f else 0f),
) )
Spacer(Modifier.width(2.dp)) Spacer(Modifier.width(2.dp))
Text( Text(
text = dayOfWeek.toString(), text = dayOfWeek.asStringSource().rememberString(),
modifier = Modifier.alpha( modifier = Modifier.alpha(
if(selected) 1f if (selected) 1f
else 0.5f else 0.5f
), ),
fontSize = myTextSizes.xs, fontSize = myTextSizes.xs,
) )
} }
} }
private fun DayOfWeek.asStringSource() = when (this) {
MONDAY -> Res.string.monday
TUESDAY -> Res.string.tuesday
WEDNESDAY -> Res.string.wednesday
THURSDAY -> Res.string.thursday
FRIDAY -> Res.string.friday
SATURDAY -> Res.string.saturday
SUNDAY -> Res.string.sunday
}.asStringSource()

View File

@ -177,20 +177,21 @@ fun <T> TitleAndDescription(
) { ) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
cfg.title, cfg.title.rememberString(),
fontSize = myTextSizes.base, fontSize = myTextSizes.base,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
) )
if (cfg.description.isNotBlank()) { if (cfg.description.rememberString().isNotBlank()) {
Spacer(Modifier.size(4.dp)) Spacer(Modifier.size(4.dp))
Help(cfg) Help(cfg)
} }
} }
if (describe) { if (describe) {
val value = cfg.backedBy.collectAsState().value val value = cfg.backedBy.collectAsState().value
val describeContent = remember(value) { val describedStringSource = remember(value) {
cfg.describe(value) cfg.describe(value)
} }
val describeContent = describedStringSource.rememberString()
if (describeContent.isNotBlank()) { if (describeContent.isNotBlank()) {
WithContentAlpha(0.75f){ WithContentAlpha(0.75f){
Text(describeContent, Text(describeContent,
@ -341,7 +342,7 @@ private fun Help(
) { ) {
WithContentColor(myColors.onSurface) { WithContentColor(myColors.onSurface) {
Text( Text(
cfg.description, cfg.description.rememberString(),
fontSize = myTextSizes.base, fontSize = myTextSizes.base,
) )
} }

View File

@ -51,7 +51,7 @@ fun RenderThemeConfig(cfg: ThemeConfigurable, modifier: Modifier) {
.size(16.dp) .size(16.dp)
) )
Spacer(Modifier.width(16.dp)) Spacer(Modifier.width(16.dp))
Text(cfg.describe(it), fontSize = myTextSizes.lg) Text(cfg.describe(it).rememberString(), fontSize = myTextSizes.lg)
} }
}) })
} }

View File

@ -12,12 +12,14 @@ import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.rememberWindowState import androidx.compose.ui.window.rememberWindowState
import com.abdownloadmanager.resources.Res
import ir.amirab.downloader.downloaditem.DownloadJobStatus import ir.amirab.downloader.downloaditem.DownloadJobStatus
import ir.amirab.downloader.monitor.CompletedDownloadItemState import ir.amirab.downloader.monitor.CompletedDownloadItemState
import ir.amirab.downloader.monitor.IDownloadItemState import ir.amirab.downloader.monitor.IDownloadItemState
import ir.amirab.downloader.monitor.ProcessingDownloadItemState import ir.amirab.downloader.monitor.ProcessingDownloadItemState
import ir.amirab.downloader.monitor.statusOrFinished import ir.amirab.downloader.monitor.statusOrFinished
import ir.amirab.downloader.utils.ExceptionUtils import ir.amirab.downloader.utils.ExceptionUtils
import ir.amirab.util.compose.resources.myStringResource
import java.awt.Dimension import java.awt.Dimension
import java.awt.Taskbar import java.awt.Taskbar
import java.awt.Window import java.awt.Window
@ -86,7 +88,7 @@ fun ShowDownloadDialogs(component: DownloadDialogManager) {
window.minimumSize = Dimension(defaultWidth.toInt(), defaultHeight.toInt()) window.minimumSize = Dimension(defaultWidth.toInt(), defaultHeight.toInt())
} }
val singleDownloadPageSizing = remember(showPartInfo) { SingleDownloadPageSizing() } val singleDownloadPageSizing = remember(showPartInfo) { SingleDownloadPageSizing() }
WindowTitle(itemState?.let { getDownloadTitle(it) } ?: "Download") WindowTitle(itemState?.let { getDownloadTitle(it) } ?: myStringResource(Res.string.download))
WindowIcon(MyIcons.appIcon) WindowIcon(MyIcons.appIcon)
var h = defaultHeight var h = defaultHeight
var w = defaultWidth var w = defaultWidth

View File

@ -38,17 +38,28 @@ import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.rememberComponentRectPositionProvider import androidx.compose.ui.window.rememberComponentRectPositionProvider
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.utils.compose.useIsInDebugMode import com.abdownloadmanager.utils.compose.useIsInDebugMode
import ir.amirab.downloader.downloaditem.DownloadJobStatus import ir.amirab.downloader.downloaditem.DownloadJobStatus
import ir.amirab.downloader.monitor.* import ir.amirab.downloader.monitor.*
import ir.amirab.downloader.part.PartDownloadStatus import ir.amirab.downloader.part.PartDownloadStatus
import ir.amirab.downloader.utils.ExceptionUtils import ir.amirab.downloader.utils.ExceptionUtils
import ir.amirab.util.compose.StringSource
import ir.amirab.util.compose.asStringSource
import ir.amirab.util.compose.resources.myStringResource
enum class SingleDownloadPageSections( enum class SingleDownloadPageSections(
val title: StringSource,
val icon: IconSource, val icon: IconSource,
) { ) {
Info(MyIcons.info), Info(
Settings(MyIcons.settings), Res.string.info.asStringSource(),
MyIcons.info
),
Settings(
Res.string.settings.asStringSource(),
MyIcons.settings
),
} }
private val tabs = SingleDownloadPageSections.entries.toList() private val tabs = SingleDownloadPageSections.entries.toList()
@ -77,7 +88,7 @@ fun SingleDownloadPage(singleDownloadComponent: SingleDownloadComponent) {
selectedTab = tab selectedTab = tab
}, },
icon = tab.icon, icon = tab.icon,
title = tab.toString() title = tab.title
) )
} }
} }
@ -330,22 +341,18 @@ fun ColumnScope.RenderPartInfo(itemState: ProcessingDownloadItemState) {
} }
PartInfoCells.Status -> { PartInfoCells.Status -> {
SimpleCellText("${prettifyStatus(it.value.status)}") SimpleCellText(prettifyStatus(it.value.status).rememberString())
} }
PartInfoCells.Downloaded -> { PartInfoCells.Downloaded -> {
SimpleCellText("${convertSizeToHumanReadable(it.value.howMuchProceed)}") SimpleCellText(convertSizeToHumanReadable(it.value.howMuchProceed).rememberString())
} }
PartInfoCells.Total -> { PartInfoCells.Total -> {
SimpleCellText( SimpleCellText(
"${ it.value.length?.let { length ->
it.value.length?.let { length -> convertSizeToHumanReadable(length).rememberString()
convertSizeToHumanReadable( } ?: myStringResource(Res.string.unknown),
length
)
} ?: "Unknown"
}",
) )
} }
} }
@ -381,14 +388,14 @@ fun ColumnScope.RenderPartInfo(itemState: ProcessingDownloadItemState) {
} }
} }
fun prettifyStatus(status: PartDownloadStatus): String { fun prettifyStatus(status: PartDownloadStatus): StringSource {
return when (status) { return when (status) {
is PartDownloadStatus.Canceled -> "Disconnected" is PartDownloadStatus.Canceled -> Res.string.disconnected
PartDownloadStatus.IDLE -> "IDLE" PartDownloadStatus.IDLE -> Res.string.idle
PartDownloadStatus.Completed -> "Completed" PartDownloadStatus.Completed -> Res.string.finished
PartDownloadStatus.ReceivingData -> "Receiving Data" PartDownloadStatus.ReceivingData -> Res.string.receiving_data
PartDownloadStatus.SendGet -> "Send Get" PartDownloadStatus.SendGet -> Res.string.send_get
} }.asStringSource()
} }
@Composable @Composable
@ -398,22 +405,26 @@ private fun SimpleCellText(text: String) {
sealed class PartInfoCells : TableCell<IndexedValue<UiPart>> { sealed class PartInfoCells : TableCell<IndexedValue<UiPart>> {
data object Number : PartInfoCells() { data object Number : PartInfoCells() {
override val name: String = "#" override val id: String = "#"
override val name: StringSource = "#".asStringSource()
override val size: CellSize = CellSize.Fixed(26.dp) override val size: CellSize = CellSize.Fixed(26.dp)
} }
data object Status : PartInfoCells() { data object Status : PartInfoCells() {
override val name: String = "Status" override val id: String = "Status"
override val name: StringSource = Res.string.status.asStringSource()
override val size: CellSize = CellSize.Resizeable(100.dp..200.dp) override val size: CellSize = CellSize.Resizeable(100.dp..200.dp)
} }
data object Downloaded : PartInfoCells() { data object Downloaded : PartInfoCells() {
override val name: String = "Downloaded" override val id: String = "Downloaded"
override val name: StringSource = Res.string.parts_info_downloaded_size.asStringSource()
override val size: CellSize = CellSize.Resizeable(90.dp..200.dp) override val size: CellSize = CellSize.Resizeable(90.dp..200.dp)
} }
data object Total : PartInfoCells() { data object Total : PartInfoCells() {
override val name: String = "Total" override val id: String = "Total"
override val name: StringSource = Res.string.parts_info_total_size.asStringSource()
override val size: CellSize = CellSize.Resizeable(90.dp..200.dp) override val size: CellSize = CellSize.Resizeable(90.dp..200.dp)
} }
@ -440,7 +451,7 @@ fun RenderPropertyItem(propertyItem: SingleDownloadPagePropertyItem) {
) { ) {
WithContentAlpha(0.75f) { WithContentAlpha(0.75f) {
Text( Text(
text = "$title:", text = "${title.rememberString()}:",
modifier = Modifier.weight(0.3f), modifier = Modifier.weight(0.3f),
maxLines = 1, maxLines = 1,
fontSize = myTextSizes.base fontSize = myTextSizes.base
@ -448,7 +459,7 @@ fun RenderPropertyItem(propertyItem: SingleDownloadPagePropertyItem) {
} }
WithContentAlpha(1f) { WithContentAlpha(1f) {
Text( Text(
text = "$value", text = value.rememberString(),
modifier = Modifier modifier = Modifier
.basicMarquee() .basicMarquee()
.weight(0.7f), .weight(0.7f),
@ -532,7 +543,7 @@ private fun PartInfoButton(
onClick = { onClick = {
onClick(!showing) onClick(!showing)
}, },
text = "Part Info", text = myStringResource(Res.string.parts_info),
icon = if (showing) { icon = if (showing) {
MyIcons.up MyIcons.up
} else { } else {
@ -565,7 +576,7 @@ private fun CloseButton(close: () -> Unit) {
{ {
close() close()
}, },
text = "Close" text = myStringResource(Res.string.close)
) )
} }
@ -576,7 +587,7 @@ private fun OpenFileButton(open: () -> Unit) {
open() open()
}, },
icon = MyIcons.fileOpen, icon = MyIcons.fileOpen,
text = "Open" text = myStringResource(Res.string.open_file)
) )
} }
@ -587,7 +598,7 @@ private fun OpenFolderButton(open: () -> Unit) {
open() open()
}, },
icon = MyIcons.folderOpen, icon = MyIcons.folderOpen,
text = "Folder", text = myStringResource(Res.string.open_folder),
) )
} }
@ -605,11 +616,11 @@ private fun ToggleButton(
val isResumeSupported = itemState.supportResume == true val isResumeSupported = itemState.supportResume == true
val (icon, text) = when (itemState.status) { val (icon, text) = when (itemState.status) {
is DownloadJobStatus.CanBeResumed -> { is DownloadJobStatus.CanBeResumed -> {
MyIcons.resume to "Resume" MyIcons.resume to Res.string.resume
} }
is DownloadJobStatus.IsActive -> { is DownloadJobStatus.IsActive -> {
MyIcons.pause to "Pause" MyIcons.pause to Res.string.pause
} }
else -> return else -> return
@ -629,7 +640,7 @@ private fun ToggleButton(
} }
}, },
icon = icon, icon = icon,
text = text, text = myStringResource(text),
color = if (isResumeSupported) { color = if (isResumeSupported) {
LocalContentColor.current LocalContentColor.current
} else { } else {
@ -670,13 +681,13 @@ private fun ToggleButton(
) { ) {
Text(buildAnnotatedString { Text(buildAnnotatedString {
withStyle(SpanStyle(color = myColors.warning)) { withStyle(SpanStyle(color = myColors.warning)) {
append("WARNING:\n") append("${myStringResource(Res.string.warning)}:\n")
} }
append("This download doesn't support resuming! You may have to RESTART it later in the Download List") append(myStringResource(Res.string.unsupported_resume_warning))
}) })
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
ActionButton( ActionButton(
"Stop Anyway", myStringResource(Res.string.stop_anyway),
onClick = { onClick = {
closePopup() closePopup()
pause() pause()

View File

@ -8,12 +8,17 @@ import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects
import com.abdownloadmanager.desktop.utils.mvi.supportEffects import com.abdownloadmanager.desktop.utils.mvi.supportEffects
import arrow.optics.copy import arrow.optics.copy
import com.abdownloadmanager.desktop.storage.PageStatesStorage import com.abdownloadmanager.desktop.storage.PageStatesStorage
import com.abdownloadmanager.resources.Res
import com.abdownloadmanager.resources.*
import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.ComponentContext
import ir.amirab.downloader.DownloadManagerEvents import ir.amirab.downloader.DownloadManagerEvents
import ir.amirab.downloader.downloaditem.DownloadJobStatus import ir.amirab.downloader.downloaditem.DownloadJobStatus
import ir.amirab.downloader.utils.ExceptionUtils import ir.amirab.downloader.utils.ExceptionUtils
import ir.amirab.downloader.DownloadManager import ir.amirab.downloader.DownloadManager
import ir.amirab.downloader.monitor.* import ir.amirab.downloader.monitor.*
import ir.amirab.util.compose.StringSource
import ir.amirab.util.compose.asStringSource
import ir.amirab.util.compose.asStringSourceWithARgs
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -27,8 +32,8 @@ sealed interface SingleDownloadEffects {
@Immutable @Immutable
data class SingleDownloadPagePropertyItem( data class SingleDownloadPagePropertyItem(
val name: String, val name: StringSource,
val value: String, val value: StringSource,
val valueState: ValueType = ValueType.Normal, val valueState: ValueType = ValueType.Normal,
) { ) {
enum class ValueType { Normal, Error, Success } enum class ValueType { Normal, Error, Success }
@ -67,9 +72,14 @@ class SingleDownloadComponent(
.filterNotNull() .filterNotNull()
.map { .map {
buildList { buildList {
add(SingleDownloadPagePropertyItem("Name", it.name)) add(SingleDownloadPagePropertyItem(Res.string.name.asStringSource(), it.name.asStringSource()))
add(SingleDownloadPagePropertyItem("Status", createStatusString(it))) add(SingleDownloadPagePropertyItem(Res.string.status.asStringSource(), createStatusString(it)))
add(SingleDownloadPagePropertyItem("Size", convertSizeToHumanReadable(it.contentLength))) add(
SingleDownloadPagePropertyItem(
Res.string.size.asStringSource(),
convertSizeToHumanReadable(it.contentLength)
)
)
when (it) { when (it) {
is CompletedDownloadItemState -> { is CompletedDownloadItemState -> {
} }
@ -77,21 +87,31 @@ class SingleDownloadComponent(
is ProcessingDownloadItemState -> { is ProcessingDownloadItemState -> {
add( add(
SingleDownloadPagePropertyItem( SingleDownloadPagePropertyItem(
"Downloaded", Res.string.download_page_downloaded_size.asStringSource(),
convertBytesToHumanReadable(it.progress).orEmpty() convertBytesToHumanReadable(it.progress).orEmpty().asStringSource()
) )
) )
add(SingleDownloadPagePropertyItem("Speed", convertSpeedToHumanReadable(it.speed)))
add(SingleDownloadPagePropertyItem("Remaining Time", (it.remainingTime?.let { remainingTime ->
convertTimeRemainingToHumanReadable(remainingTime, TimeNames.ShortNames)
}.orEmpty())))
add( add(
SingleDownloadPagePropertyItem( SingleDownloadPagePropertyItem(
"Resume Support", Res.string.speed.asStringSource(),
convertSpeedToHumanReadable(it.speed).asStringSource()
)
)
add(
SingleDownloadPagePropertyItem(
Res.string.time_left.asStringSource(),
(it.remainingTime?.let { remainingTime ->
convertTimeRemainingToHumanReadable(remainingTime, TimeNames.ShortNames)
}.orEmpty()).asStringSource()
)
)
add(
SingleDownloadPagePropertyItem(
Res.string.resume_support.asStringSource(),
when (it.supportResume) { when (it.supportResume) {
true -> "Yes" true -> Res.string.yes.asStringSource()
false -> "No" false -> Res.string.no.asStringSource()
null -> "Unknown" null -> Res.string.unknown.asStringSource()
}, },
when (it.supportResume) { when (it.supportResume) {
true -> SingleDownloadPagePropertyItem.ValueType.Success true -> SingleDownloadPagePropertyItem.ValueType.Success
@ -105,23 +125,23 @@ class SingleDownloadComponent(
} }
}.stateIn(scope, SharingStarted.Eagerly, emptyList()) }.stateIn(scope, SharingStarted.Eagerly, emptyList())
private fun createStatusString(it: IDownloadItemState): String { private fun createStatusString(it: IDownloadItemState): StringSource {
return when (val status = it.statusOrFinished()) { return when (val status = it.statusOrFinished()) {
is DownloadJobStatus.Canceled -> { is DownloadJobStatus.Canceled -> {
if (ExceptionUtils.isNormalCancellation(status.e)) { if (ExceptionUtils.isNormalCancellation(status.e)) {
"Paused" Res.string.paused
} else { } else {
"Error" Res.string.error
} }
} }
DownloadJobStatus.Downloading -> "Downloading" DownloadJobStatus.Downloading -> Res.string.downloading
DownloadJobStatus.Finished -> "Finished" DownloadJobStatus.Finished -> Res.string.finished
DownloadJobStatus.IDLE -> "IDLE" DownloadJobStatus.IDLE -> Res.string.idle
is DownloadJobStatus.PreparingFile -> "PreparingFile" is DownloadJobStatus.PreparingFile -> Res.string.preparing_file
DownloadJobStatus.Resuming -> "Resuming" DownloadJobStatus.Resuming -> Res.string.resuming
} }.asStringSource()
} }
fun openFolder() { fun openFolder() {
@ -230,28 +250,33 @@ class SingleDownloadComponent(
val settings by lazy { val settings by lazy {
listOf( listOf(
IntConfigurable( IntConfigurable(
title = "Thread Count", title = Res.string.download_item_settings_thread_count.asStringSource(),
description = "How much thread used to download this item 0 for default", description = Res.string.download_item_settings_thread_count_description.asStringSource(),
backedBy = threadCount, backedBy = threadCount,
describe = { describe = {
if (it == 0) { if (it == 0) {
"uses global setting" Res.string.use_global_settings.asStringSource()
} else { } else {
"$it threads" Res.string.download_item_settings_thread_count_describe
.asStringSourceWithARgs(
Res.string.download_item_settings_thread_count_describe_createArgs(
count = it.toString()
)
)
} }
}, },
range = 0..32, range = 0..32,
renderMode = IntConfigurable.RenderMode.TextField, renderMode = IntConfigurable.RenderMode.TextField,
), ),
SpeedLimitConfigurable( SpeedLimitConfigurable(
title = "Speed limit", title = Res.string.download_item_settings_speed_limit.asStringSource(),
description = "speed limit for this download", description = Res.string.download_item_settings_speed_limit_description.asStringSource(),
backedBy = speedLimit, backedBy = speedLimit,
describe = { describe = {
if (it == 0L) { if (it == 0L) {
"Unlimited" Res.string.unlimited.asStringSource()
} else { } else {
convertSpeedToHumanReadable(it) convertSpeedToHumanReadable(it).asStringSource()
} }
}, },
), ),

View File

@ -9,6 +9,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.rememberWindowState import androidx.compose.ui.window.rememberWindowState
import ir.amirab.util.compose.asStringSource
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -61,8 +62,8 @@ fun ShowUpdaterDialog(updaterComponent: UpdateComponent) {
message?.let { message -> message?.let { message ->
ShowNotification( ShowNotification(
title = "Updater", title = "Updater".asStringSource(),
description = message, description = message.asStringSource(),
type = notificationType ?: NotificationType.Info, type = notificationType ?: NotificationType.Info,
tag = "Updater" tag = "Updater"
) )

View File

@ -4,11 +4,13 @@ import com.abdownloadmanager.desktop.utils.*
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import arrow.optics.Lens import arrow.optics.Lens
import arrow.optics.optics import arrow.optics.optics
import ir.amirab.util.compose.localizationmanager.LanguageStorage
import ir.amirab.util.config.booleanKeyOf import ir.amirab.util.config.booleanKeyOf
import ir.amirab.util.config.intKeyOf import ir.amirab.util.config.intKeyOf
import ir.amirab.util.config.longKeyOf import ir.amirab.util.config.longKeyOf
import ir.amirab.util.config.stringKeyOf import ir.amirab.util.config.stringKeyOf
import ir.amirab.util.config.MapConfig import ir.amirab.util.config.MapConfig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.io.File import java.io.File
@ -16,6 +18,7 @@ import java.io.File
@Serializable @Serializable
data class AppSettingsModel( data class AppSettingsModel(
val theme: String = "dark", val theme: String = "dark",
val language: String = "en",
val mergeTopBarWithTitleBar: Boolean = false, val mergeTopBarWithTitleBar: Boolean = false,
val threadCount: Int = 5, val threadCount: Int = 5,
val dynamicPartCreation: Boolean = true, val dynamicPartCreation: Boolean = true,
@ -38,6 +41,7 @@ data class AppSettingsModel(
object ConfigLens : Lens<MapConfig, AppSettingsModel> { object ConfigLens : Lens<MapConfig, AppSettingsModel> {
object Keys { object Keys {
val theme = stringKeyOf("theme") val theme = stringKeyOf("theme")
val language = stringKeyOf("language")
val mergeTopBarWithTitleBar = booleanKeyOf("mergeTopBarWithTitleBar") val mergeTopBarWithTitleBar = booleanKeyOf("mergeTopBarWithTitleBar")
val threadCount = intKeyOf("threadCount") val threadCount = intKeyOf("threadCount")
val dynamicPartCreation = booleanKeyOf("dynamicPartCreation") val dynamicPartCreation = booleanKeyOf("dynamicPartCreation")
@ -58,6 +62,7 @@ data class AppSettingsModel(
val default by lazy { AppSettingsModel.default } val default by lazy { AppSettingsModel.default }
return AppSettingsModel( return AppSettingsModel(
theme = source.get(Keys.theme) ?: default.theme, theme = source.get(Keys.theme) ?: default.theme,
language = source.get(Keys.language) ?: default.language,
mergeTopBarWithTitleBar = source.get(Keys.mergeTopBarWithTitleBar) ?: default.mergeTopBarWithTitleBar, mergeTopBarWithTitleBar = source.get(Keys.mergeTopBarWithTitleBar) ?: default.mergeTopBarWithTitleBar,
threadCount = source.get(Keys.threadCount) ?: default.threadCount, threadCount = source.get(Keys.threadCount) ?: default.threadCount,
dynamicPartCreation = source.get(Keys.dynamicPartCreation) ?: default.dynamicPartCreation, dynamicPartCreation = source.get(Keys.dynamicPartCreation) ?: default.dynamicPartCreation,
@ -77,6 +82,7 @@ data class AppSettingsModel(
override fun set(source: MapConfig, focus: AppSettingsModel): MapConfig { override fun set(source: MapConfig, focus: AppSettingsModel): MapConfig {
return source.apply { return source.apply {
put(Keys.theme, focus.theme) put(Keys.theme, focus.theme)
put(Keys.language, focus.language)
put(Keys.mergeTopBarWithTitleBar, focus.mergeTopBarWithTitleBar) put(Keys.mergeTopBarWithTitleBar, focus.mergeTopBarWithTitleBar)
put(Keys.threadCount, focus.threadCount) put(Keys.threadCount, focus.threadCount)
put(Keys.dynamicPartCreation, focus.dynamicPartCreation) put(Keys.dynamicPartCreation, focus.dynamicPartCreation)
@ -96,8 +102,11 @@ data class AppSettingsModel(
class AppSettingsStorage( class AppSettingsStorage(
settings: DataStore<MapConfig>, settings: DataStore<MapConfig>,
) : ConfigBaseSettingsByMapConfig<AppSettingsModel>(settings, AppSettingsModel.ConfigLens) { ) :
ConfigBaseSettingsByMapConfig<AppSettingsModel>(settings, AppSettingsModel.ConfigLens),
LanguageStorage {
var theme = from(AppSettingsModel.theme) var theme = from(AppSettingsModel.theme)
override val selectedLanguage = from(AppSettingsModel.language)
var mergeTopBarWithTitleBar = from(AppSettingsModel.mergeTopBarWithTitleBar) var mergeTopBarWithTitleBar = from(AppSettingsModel.mergeTopBarWithTitleBar)
val threadCount = from(AppSettingsModel.threadCount) val threadCount = from(AppSettingsModel.threadCount)
val dynamicPartCreation = from(AppSettingsModel.dynamicPartCreation) val dynamicPartCreation = from(AppSettingsModel.dynamicPartCreation)

View File

@ -24,12 +24,16 @@ import ir.amirab.util.compose.action.buildMenu
import com.abdownloadmanager.desktop.utils.isInDebugMode import com.abdownloadmanager.desktop.utils.isInDebugMode
import com.abdownloadmanager.desktop.utils.mvi.HandleEffects import com.abdownloadmanager.desktop.utils.mvi.HandleEffects
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.window.* import androidx.compose.ui.window.*
import com.abdownloadmanager.desktop.pages.batchdownload.BatchDownloadWindow import com.abdownloadmanager.desktop.pages.batchdownload.BatchDownloadWindow
import com.abdownloadmanager.desktop.pages.category.ShowCategoryDialogs import com.abdownloadmanager.desktop.pages.category.ShowCategoryDialogs
import com.abdownloadmanager.desktop.pages.home.HomeWindow import com.abdownloadmanager.desktop.pages.home.HomeWindow
import com.abdownloadmanager.desktop.pages.settings.ThemeManager import com.abdownloadmanager.desktop.pages.settings.ThemeManager
import com.abdownloadmanager.desktop.ui.widget.ProvideLanguageManager
import com.abdownloadmanager.utils.compose.ProvideDebugInfo import com.abdownloadmanager.utils.compose.ProvideDebugInfo
import ir.amirab.util.compose.localizationmanager.LanguageManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
@ -45,47 +49,51 @@ object Ui : KoinComponent {
) { ) {
val appComponent: AppComponent = get() val appComponent: AppComponent = get()
val themeManager: ThemeManager = get() val themeManager: ThemeManager = get()
val languageManager: LanguageManager = get()
themeManager.boot() themeManager.boot()
languageManager.boot()
if (!appArguments.startSilent) { if (!appArguments.startSilent) {
appComponent.openHome() appComponent.openHome()
} }
application { application {
val theme by themeManager.currentThemeColor.collectAsState() val theme by themeManager.currentThemeColor.collectAsState()
ProvideDebugInfo(AppInfo.isInDebugMode()) { ProvideDebugInfo(AppInfo.isInDebugMode()) {
ProvideNotificationManager { ProvideLanguageManager(languageManager) {
ABDownloaderTheme( ProvideNotificationManager {
myColors = theme, ABDownloaderTheme(
myColors = theme,
// uiScale = appComponent.uiScale.collectAsState().value // uiScale = appComponent.uiScale.collectAsState().value
) { ) {
ProvideGlobalExceptionHandler(globalAppExceptionHandler) { ProvideGlobalExceptionHandler(globalAppExceptionHandler) {
val trayState = rememberTrayState() val trayState = rememberTrayState()
HandleEffectsForApp(appComponent) HandleEffectsForApp(appComponent)
SystemTray(appComponent, trayState) SystemTray(appComponent, trayState)
val showHomeSlot = appComponent.showHomeSlot.collectAsState().value val showHomeSlot = appComponent.showHomeSlot.collectAsState().value
showHomeSlot.child?.instance?.let { showHomeSlot.child?.instance?.let {
HomeWindow(it,appComponent::closeHome) HomeWindow(it, appComponent::closeHome)
}
val showSettingSlot = appComponent.showSettingSlot.collectAsState().value
showSettingSlot.child?.instance?.let {
SettingWindow(it, appComponent::closeSettings)
}
val showQueuesSlot = appComponent.showQueuesSlot.collectAsState().value
showQueuesSlot.child?.instance?.let {
QueuesWindow(it)
}
val batchDownloadSlot = appComponent.batchDownloadSlot.collectAsState().value
batchDownloadSlot.child?.instance?.let {
BatchDownloadWindow(it)
}
ShowAddDownloadDialogs(appComponent)
ShowDownloadDialogs(appComponent)
ShowCategoryDialogs(appComponent)
//TODO Enable Updater
//ShowUpdaterDialog(appComponent.updater)
ShowAboutDialog(appComponent)
NewQueueDialog(appComponent)
ShowMessageDialogs(appComponent)
ShowOpenSourceLibraries(appComponent)
} }
val showSettingSlot = appComponent.showSettingSlot.collectAsState().value
showSettingSlot.child?.instance?.let {
SettingWindow(it, appComponent::closeSettings)
}
val showQueuesSlot = appComponent.showQueuesSlot.collectAsState().value
showQueuesSlot.child?.instance?.let {
QueuesWindow(it)
}
val batchDownloadSlot = appComponent.batchDownloadSlot.collectAsState().value
batchDownloadSlot.child?.instance?.let {
BatchDownloadWindow(it)
}
ShowAddDownloadDialogs(appComponent)
ShowDownloadDialogs(appComponent)
ShowCategoryDialogs(appComponent)
//TODO Enable Updater
//ShowUpdaterDialog(appComponent.updater)
ShowAboutDialog(appComponent)
NewQueueDialog(appComponent)
ShowMessageDialogs(appComponent)
ShowOpenSourceLibraries(appComponent)
} }
} }
} }

View File

@ -25,6 +25,7 @@ import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -65,16 +66,19 @@ private fun FrameWindowScope.CustomWindowFrame(
} }
.background(background) .background(background)
) { ) {
SnapDraggableToolbar( WithTitleBarDirection {
title = title, SnapDraggableToolbar(
windowIcon = windowIcon, title = title,
titlePosition = titlePosition, windowIcon = windowIcon,
start = start, titlePosition = titlePosition,
end = end, start = start,
onRequestMinimize = onRequestMinimize, end = end,
onRequestClose = onRequestClose, onRequestMinimize = onRequestMinimize,
onRequestToggleMaximize = onRequestToggleMaximize onRequestClose = onRequestClose,
) onRequestToggleMaximize = onRequestToggleMaximize
)
}
content() content()
} }
} }

View File

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

View File

@ -18,6 +18,7 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Popup import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties import androidx.compose.ui.window.PopupProperties
import androidx.compose.ui.window.rememberPopupPositionProviderAtPosition import androidx.compose.ui.window.rememberPopupPositionProviderAtPosition
import ir.amirab.util.compose.asStringSource
/* /*
fun MyColors.asMaterial2Colors(): Colors { fun MyColors.asMaterial2Colors(): Colors {
@ -135,7 +136,7 @@ private class MyContextMenuRepresentation : ContextMenuRepresentation {
val menuItems = remember(contextItems) { val menuItems = remember(contextItems) {
buildMenu { buildMenu {
contextItems.map { contextItems.map {
item(title = it.label, onClick = { item(title = it.label.asStringSource(), onClick = {
it.onClick() it.onClick()
}) })
} }

View File

@ -14,6 +14,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.abdownloadmanager.resources.Res
import ir.amirab.util.compose.resources.myStringResource
@Composable @Composable
fun AddUrlButton( fun AddUrlButton(
@ -29,21 +31,22 @@ fun AddUrlButton(
.background(myColors.surface) .background(myColors.surface)
.clickable(onClick = onClick) .clickable(onClick = onClick)
.height(32.dp) .height(32.dp)
.width(120.dp) // .width(120.dp)
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp),
,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
WithContentAlpha(1f) { WithContentAlpha(1f) {
MyIcon(addUrlIcon, null, Modifier.size(16.dp)) MyIcon(addUrlIcon, null, Modifier.size(16.dp))
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(10.dp))
Text("Add URL", Text(
Modifier.weight(1f), myStringResource(Res.string.new_download),
Modifier,
maxLines = 1, maxLines = 1,
fontSize = myTextSizes.sm, fontSize = myTextSizes.sm,
) )
} }
Spacer(Modifier.width(10.dp))
Box( Box(
Modifier Modifier
.clip(RoundedCornerShape(6.dp)) .clip(RoundedCornerShape(6.dp))

View File

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

View File

@ -19,6 +19,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.rememberWindowState import androidx.compose.ui.window.rememberWindowState
import ir.amirab.util.compose.StringSource
import java.awt.Dimension import java.awt.Dimension
import java.util.UUID import java.util.UUID
@ -32,8 +33,8 @@ sealed class MessageDialogType {
data class MessageDialogModel( data class MessageDialogModel(
val id: String = UUID.randomUUID().toString(), val id: String = UUID.randomUUID().toString(),
val title: String, val title: StringSource,
val description: String, val description: StringSource,
val type: MessageDialogType = MessageDialogType.Info, val type: MessageDialogType = MessageDialogType.Info,
) )
@ -94,13 +95,13 @@ fun MessageDialog(
) )
Column { Column {
Text( Text(
msgContent.title, msgContent.title.rememberString(),
fontSize = myTextSizes.xl, fontSize = myTextSizes.xl,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
) )
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Text( Text(
msgContent.description, msgContent.description.rememberString(),
fontSize = myTextSizes.base, fontSize = myTextSizes.base,
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)

View File

@ -21,6 +21,8 @@ import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ir.amirab.util.compose.StringSource
import ir.amirab.util.compose.asStringSource
import kotlinx.coroutines.* import kotlinx.coroutines.*
private val LocalNotification = compositionLocalOf<NotificationManager> { private val LocalNotification = compositionLocalOf<NotificationManager> {
@ -43,13 +45,13 @@ sealed interface NotificationType {
@Stable @Stable
class NotificationModel( class NotificationModel(
val tag: Any, val tag: Any,
initialTitle: String = "", initialTitle: StringSource = "".asStringSource(),
initialDescription: String = "", initialDescription: StringSource = "".asStringSource(),
initialNotificationType: NotificationType = NotificationType.Info initialNotificationType: NotificationType = NotificationType.Info,
) { ) {
var notificationType: NotificationType by mutableStateOf(initialNotificationType) var notificationType: NotificationType by mutableStateOf(initialNotificationType)
var title: String by mutableStateOf(initialTitle) var title: StringSource by mutableStateOf(initialTitle)
var description: String by mutableStateOf(initialDescription) var description: StringSource by mutableStateOf(initialDescription)
} }
@Composable @Composable
@ -66,10 +68,10 @@ fun ProvideNotificationManager(
@Composable @Composable
fun ShowNotification( fun ShowNotification(
title: String, title: StringSource,
description: String, description: StringSource,
type:NotificationType, type: NotificationType,
tag: Any = currentCompositeKeyHash tag: Any = currentCompositeKeyHash,
) { ) {
val notification = remember(tag) { val notification = remember(tag) {
NotificationModel( NotificationModel(
@ -156,7 +158,7 @@ private fun RenderNotification(
private fun NotificationDescription(notificationModel: NotificationModel) { private fun NotificationDescription(notificationModel: NotificationModel) {
WithContentAlpha(0.75f) { WithContentAlpha(0.75f) {
Text( Text(
text = notificationModel.description, text = notificationModel.description.rememberString(),
fontSize = myTextSizes.base fontSize = myTextSizes.base
) )
} }
@ -166,7 +168,7 @@ private fun NotificationDescription(notificationModel: NotificationModel) {
private fun NotificationTitle(notificationModel: NotificationModel) { private fun NotificationTitle(notificationModel: NotificationModel) {
WithContentAlpha(1f) { WithContentAlpha(1f) {
Text( Text(
text = notificationModel.title, text = notificationModel.title.rememberString(),
fontSize = myTextSizes.base, fontSize = myTextSizes.base,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
) )
@ -247,10 +249,10 @@ class NotificationManager {
} }
suspend fun showNotification( suspend fun showNotification(
title: String, title: StringSource,
description: String, description: StringSource,
delay: Long = -1, delay: Long = -1,
tag: Double = Math.random() tag: Double = Math.random(),
) { ) {
val notification = NotificationModel( val notification = NotificationModel(
tag = tag, tag = tag,

View File

@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ir.amirab.util.compose.StringSource
@Composable @Composable
@ -32,7 +33,7 @@ fun MyTab(
selected: Boolean, selected: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
icon: IconSource, icon: IconSource,
title: String, title: StringSource,
selectionBackground: Color = myColors.background, selectionBackground: Color = myColors.background,
) { ) {
WithContentAlpha( WithContentAlpha(
@ -56,7 +57,7 @@ fun MyTab(
) { ) {
MyIcon(icon, null, Modifier.size(16.dp)) MyIcon(icon, null, Modifier.size(16.dp))
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
Text(title, maxLines = 1, fontSize = myTextSizes.base) Text(title.rememberString(), maxLines = 1, fontSize = myTextSizes.base)
} }
} }
} }

View File

@ -27,6 +27,8 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.rememberCursorPositionProvider import androidx.compose.ui.window.rememberCursorPositionProvider
import com.abdownloadmanager.resources.Res
import ir.amirab.util.compose.resources.myStringResource
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
val LocalCellPadding = compositionLocalOf { val LocalCellPadding = compositionLocalOf {
@ -241,13 +243,13 @@ private fun <T, C : TableCell<T>> ShowColumnConfigMenu(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
"Customize Columns", myStringResource(Res.string.customize_columns),
fontSize = myTextSizes.base fontSize = myTextSizes.base
) )
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
IconActionButton( IconActionButton(
MyIcons.undo, MyIcons.undo,
"Reset", myStringResource(Res.string.reset),
onClick = { onClick = {
tableState.reset() tableState.reset()
} }
@ -329,7 +331,7 @@ private fun <T, Cell : TableCell<T>> CellConfigItem(
) )
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text( Text(
cell.name, cell.name.rememberString(),
Modifier Modifier
.weight(1f) .weight(1f)
.ifThen(!isVisible || isForceVisible) { .ifThen(!isVisible || isForceVisible) {

View File

@ -23,6 +23,7 @@ import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ir.amirab.util.compose.StringSource
import ir.amirab.util.flow.mapStateFlow import ir.amirab.util.flow.mapStateFlow
import ir.amirab.util.swapped import ir.amirab.util.swapped
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -54,7 +55,8 @@ sealed interface CellSize {
@Stable @Stable
interface TableCell<Item> { interface TableCell<Item> {
val name: String val id: String
val name: StringSource
val size: CellSize val size: CellSize
} }
@ -86,7 +88,7 @@ fun DefaultRenderHeader(cell: TableCell<*>) {
cell.drawHeader() cell.drawHeader()
} else { } else {
Text( Text(
cell.name, cell.name.rememberString(),
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
@ -318,7 +320,7 @@ class TableState<Item, Cell : TableCell<Item>>(
fun save(): SerializableTableState { fun save(): SerializableTableState {
val sizes = customSizes.value.mapKeys { val sizes = customSizes.value.mapKeys {
it.key.name it.key.id
}.mapValues { }.mapValues {
it.value.value it.value.value
} }
@ -326,37 +328,37 @@ class TableState<Item, Cell : TableCell<Item>>(
return SerializableTableState( return SerializableTableState(
sizes = sizes, sizes = sizes,
sortBy = sortBy?.let { sortBy = sortBy?.let {
SortBy(sortBy.cell.name, sortBy.isUp()) SortBy(sortBy.cell.id, sortBy.isUp())
}, },
order = order.value.map { it.name }, order = order.value.map { it.id },
visibleCells = visibleCells.value.map { it.name } visibleCells = visibleCells.value.map { it.id }
) )
} }
fun load(s: SerializableTableState) { fun load(s: SerializableTableState) {
setCustomSizes { setCustomSizes {
val cellsThatHaveCustomWidth = findCellByName(s.sizes.keys) val cellsThatHaveCustomWidth = findCellById(s.sizes.keys)
cellsThatHaveCustomWidth.associateWith { s.sizes[it.name]!!.dp } cellsThatHaveCustomWidth.associateWith { s.sizes[it.id]!!.dp }
} }
setOrder(findCellByName(s.order)) setOrder(findCellById(s.order))
setSortBy( setSortBy(
s.sortBy?.let { sortBy -> s.sortBy?.let { sortBy ->
findCellByName(sortBy.name)?.let { findCellById(sortBy.name)?.let {
Sort(it as SortableCell<Item>, sortBy.descending) Sort(it as SortableCell<Item>, sortBy.descending)
} }
} }
) )
setVisibleCells(findCellByName(s.visibleCells)) setVisibleCells(findCellById(s.visibleCells))
} }
private fun findCellByName(name: String): Cell? { private fun findCellById(name: String): Cell? {
return cells.find { it.name == name } return cells.find { it.id == name }
} }
private fun findCellByName(list: Iterable<String>): List<Cell> { private fun findCellById(list: Iterable<String>): List<Cell> {
return list.mapNotNull { name -> return list.mapNotNull { name ->
findCellByName(name) findCellById(name)
} }
} }

View File

@ -34,9 +34,11 @@ fun SiblingDropDown(
offset: DpOffset = DpOffset.Zero, offset: DpOffset = DpOffset.Zero,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val positionProvider = remember { val positionProvider = rememberComponentRectPositionProvider(
SiblingMenuPositionProvider() anchor = Alignment.TopEnd,
} alignment = Alignment.BottomEnd,
offset = offset,
)
Popup( Popup(
popupPositionProvider = positionProvider, popupPositionProvider = positionProvider,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,

View File

@ -33,6 +33,7 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ir.amirab.util.compose.modifiers.autoMirror
import javax.swing.KeyStroke import javax.swing.KeyStroke
enum class MenuDisabledItemBehavior { enum class MenuDisabledItemBehavior {
@ -76,7 +77,7 @@ fun MenuBar(
.padding(horizontal = 8.dp, vertical = 4.dp) .padding(horizontal = 8.dp, vertical = 4.dp)
.wrapContentHeight(Alignment.CenterVertically) .wrapContentHeight(Alignment.CenterVertically)
) { ) {
val text = subMenu.title.collectAsState().value val text = subMenu.title.collectAsState().value.rememberString()
val (firstChar, leadingText) = remember(text) { val (firstChar, leadingText) = remember(text) {
when (text.length) { when (text.length) {
0 -> "" to "" 0 -> "" to ""
@ -248,7 +249,7 @@ private fun ReactableItem(
} }
} }
Text( Text(
title, title.rememberString(),
Modifier.weight(1f), Modifier.weight(1f),
fontSize = myTextSizes.base, fontSize = myTextSizes.base,
softWrap = false, softWrap = false,
@ -332,7 +333,9 @@ fun RenderSubMenuItem(
MyIcon( MyIcon(
MyIcons.next, MyIcons.next,
null, null,
Modifier.size(16.dp), Modifier
.size(16.dp)
.autoMirror(),
) )
}) })
if (openedItem == menuItem) { if (openedItem == menuItem) {

View File

@ -47,7 +47,7 @@ fun ShowOptions(
val itemPadding = Modifier.padding(horizontal = 12.dp, vertical = 8.dp) val itemPadding = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)
val title by menu.title.collectAsState() val title by menu.title.collectAsState()
Text( Text(
title, title.rememberString(),
Modifier Modifier
.then(itemPadding) .then(itemPadding)
.basicMarquee( .basicMarquee(

View File

@ -1,6 +1,9 @@
package com.abdownloadmanager.desktop.utils package com.abdownloadmanager.desktop.utils
import com.abdownloadmanager.resources.Res
import ir.amirab.downloader.utils.ByteConverter import ir.amirab.downloader.utils.ByteConverter
import ir.amirab.util.compose.StringSource
import ir.amirab.util.compose.asStringSource
data class HumanReadableSize( data class HumanReadableSize(
val value:Double, val value:Double,
@ -52,8 +55,9 @@ fun convertBytesToHumanReadable(size: Long): String? {
} }
} }
fun convertSizeToHumanReadable(size: Long): String { fun convertSizeToHumanReadable(size: Long): StringSource {
return convertBytesToHumanReadable(size) ?: "unknown" return convertBytesToHumanReadable(size)?.asStringSource()
?: Res.string.unknown.asStringSource()
} }
fun convertSpeedToHumanReadable(size: Long, perUnit: String="s"): String { fun convertSpeedToHumanReadable(size: Long, perUnit: String="s"): String {

View File

@ -1,6 +1,10 @@
package com.abdownloadmanager.desktop.utils package com.abdownloadmanager.desktop.utils
import androidx.compose.runtime.* import androidx.compose.runtime.*
import com.abdownloadmanager.resources.Res
import ir.amirab.util.compose.StringSource
import ir.amirab.util.compose.asStringSource
import ir.amirab.util.compose.asStringSourceWithARgs
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.datetime.DateTimePeriod import kotlinx.datetime.DateTimePeriod
@ -81,10 +85,11 @@ fun prettifyRelativeTime(
count = count, count = count,
names = names, names = names,
) )
val leftOrAgo = return (if (isLater) {
if (isLater) names.left names.left(relativeTime)
else names.ago } else {
return "$relativeTime $leftOrAgo" names.ago(relativeTime)
}).getString()
} }
private fun relativeTime( private fun relativeTime(
@ -102,8 +107,7 @@ private fun relativeTime(
val relativeTime = buildString { val relativeTime = buildString {
if (years > 0) { if (years > 0) {
used++ used++
append(years) append(names.years(years).getString())
append(" ${names.years}")
} }
if (used == count) return@buildString if (used == count) return@buildString
if (months > 0) { if (months > 0) {
@ -111,8 +115,7 @@ private fun relativeTime(
append(" ") append(" ")
} }
used++ used++
append(months) append(names.months(months).getString())
append(" ${names.months}")
} }
if (used == count) return@buildString if (used == count) return@buildString
if (days > 0) { if (days > 0) {
@ -120,8 +123,7 @@ private fun relativeTime(
append(" ") append(" ")
} }
used++ used++
append(days) append(names.days(days).getString())
append(" ${names.days}")
} }
if (used == count) return@buildString if (used == count) return@buildString
if (hours > 0) { if (hours > 0) {
@ -129,8 +131,7 @@ private fun relativeTime(
append(" ") append(" ")
} }
used++ used++
append(hours) append(names.hours(hours).getString())
append(" ${names.hours}")
} }
if (used == count) return@buildString if (used == count) return@buildString
if (minutes > 0) { if (minutes > 0) {
@ -138,8 +139,7 @@ private fun relativeTime(
append(" ") append(" ")
} }
used++ used++
append(minutes) append(names.minutes(minutes).getString())
append(" ${names.minutes}")
} }
if (used == count) return@buildString if (used == count) return@buildString
if (seconds > 0) { if (seconds > 0) {
@ -147,12 +147,11 @@ private fun relativeTime(
append(" ") append(" ")
} }
used++ used++
append(seconds) append(names.seconds(seconds).getString())
append(" ${names.seconds}")
} }
if (used == count) return@buildString if (used == count) return@buildString
if (used == 0) { if (used == 0) {
append("0 ${names.seconds}") append(names.seconds(0).getString())
} }
} }
return relativeTime return relativeTime
@ -201,37 +200,82 @@ fun convertTimeRemainingToHumanReadable(
@Stable @Stable
interface TimeNames { interface TimeNames {
val years: String fun years(years: Int): StringSource
val months: String fun months(months: Int): StringSource
val days: String fun days(days: Int): StringSource
val hours: String fun hours(hours: Int): StringSource
val minutes: String fun minutes(minutes: Int): StringSource
val seconds: String fun seconds(seconds: Int): StringSource
val ago: String fun ago(time: String): StringSource
val left: String fun left(time: String): StringSource
@Stable @Stable
object SimpleNames : TimeNames { object SimpleNames : TimeNames {
override val years: String = "years" override fun years(years: Int): StringSource = Res.string.relative_time_long_years.asStringSourceWithARgs(
override val months: String = "months" Res.string.relative_time_long_years_createArgs(years = years.toString())
override val days: String = "days" )
override val hours: String = "hours"
override val minutes: String = "minutes" override fun months(months: Int): StringSource = Res.string.relative_time_long_months.asStringSourceWithARgs(
override val seconds: String = "seconds" Res.string.relative_time_long_months_createArgs(months = months.toString())
override val left: String = "left" )
override val ago: String = "ago"
override fun days(days: Int): StringSource =
Res.string.relative_time_long_days.asStringSourceWithARgs(Res.string.relative_time_long_days_createArgs(days = days.toString()))
override fun hours(hours: Int): StringSource = Res.string.relative_time_long_hours.asStringSourceWithARgs(
Res.string.relative_time_long_hours_createArgs(hours = hours.toString())
)
override fun minutes(minutes: Int): StringSource =
Res.string.relative_time_long_minutes.asStringSourceWithARgs(
Res.string.relative_time_long_minutes_createArgs(minutes = minutes.toString())
)
override fun seconds(seconds: Int): StringSource =
Res.string.relative_time_long_seconds.asStringSourceWithARgs(
Res.string.relative_time_long_seconds_createArgs(seconds = seconds.toString())
)
override fun left(time: String): StringSource =
Res.string.relative_time_left.asStringSourceWithARgs(Res.string.relative_time_left_createArgs(time = time))
override fun ago(time: String): StringSource =
Res.string.relative_time_ago.asStringSourceWithARgs(Res.string.relative_time_ago_createArgs(time = time))
} }
object ShortNames : TimeNames { object ShortNames : TimeNames {
override val years: String = "yr" override fun years(years: Int): StringSource = Res.string.relative_time_short_years.asStringSourceWithARgs(
override val months: String = "mn" Res.string.relative_time_short_years_createArgs(years = years.toString())
override val days: String = "d" )
override val hours: String = "hr"
override val minutes: String = "m" override fun months(months: Int): StringSource = Res.string.relative_time_short_months.asStringSourceWithARgs(
override val seconds: String = "s" Res.string.relative_time_short_months_createArgs(months = months.toString())
override val left: String = "left" )
override val ago: String = "ago"
override fun days(days: Int): StringSource = Res.string.relative_time_short_days.asStringSourceWithARgs(
Res.string.relative_time_short_days_createArgs(days = days.toString())
)
override fun hours(hours: Int): StringSource = Res.string.relative_time_short_hours.asStringSourceWithARgs(
Res.string.relative_time_short_hours_createArgs(hours = hours.toString())
)
override fun minutes(minutes: Int): StringSource =
Res.string.relative_time_short_minutes.asStringSourceWithARgs(
Res.string.relative_time_short_minutes_createArgs(minutes = minutes.toString())
)
override fun seconds(seconds: Int): StringSource =
Res.string.relative_time_short_seconds.asStringSourceWithARgs(
Res.string.relative_time_short_seconds_createArgs(seconds = seconds.toString())
)
override fun left(time: String): StringSource =
Res.string.relative_time_left.asStringSourceWithARgs(Res.string.relative_time_left_createArgs(time = time))
override fun ago(time: String): StringSource =
Res.string.relative_time_ago.asStringSourceWithARgs(Res.string.relative_time_ago_createArgs(time = time))
} }
} }

View File

@ -1,6 +1,8 @@
package ir.amirab.downloader.utils package ir.amirab.downloader.utils
import java.text.DecimalFormat import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.*
object ByteConverter { object ByteConverter {
const val BYTES = 1L const val BYTES = 1L
@ -8,7 +10,7 @@ object ByteConverter {
const val M_BYTES = K_BYTES * 1024L const val M_BYTES = K_BYTES * 1024L
const val G_BYTES = M_BYTES * 1024L const val G_BYTES = M_BYTES * 1024L
const val T_BYTES = G_BYTES * 1024L const val T_BYTES = G_BYTES * 1024L
private val format = DecimalFormat("#.##") private val format = DecimalFormat("#.##", DecimalFormatSymbols(Locale.US))
fun byteTo(value: Long, unit: Long): Double { fun byteTo(value: Long, unit: Long): Double {
return (value / unit.toDouble()) return (value / unit.toDouble())
} }

View File

@ -25,6 +25,8 @@ include("integration:server")
include("shared:utils") include("shared:utils")
include("shared:app-utils") include("shared:app-utils")
include("shared:compose-utils") include("shared:compose-utils")
include("shared:resources")
include("shared:resources:contracts")
include("shared:config") include("shared:config")
include("shared:updater") include("shared:updater")
include("shared:auto-start") include("shared:auto-start")

View File

@ -6,5 +6,7 @@ dependencies {
implementation(compose.runtime) implementation(compose.runtime)
implementation(compose.foundation) implementation(compose.foundation)
implementation(compose.ui) implementation(compose.ui)
implementation(compose.components.resources)
implementation(project(":shared:utils")) implementation(project(":shared:utils"))
api(project(":shared:resources:contracts"))
} }

View File

@ -6,12 +6,11 @@ import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import ir.amirab.util.compose.contants.RESOURCE_PROTOCOL
import okio.FileSystem import okio.FileSystem
import okio.Path.Companion.toPath import okio.Path.Companion.toPath
import java.net.URI import java.net.URI
private const val RESOURCE_PROTOCOL = "app-resource"
@Immutable @Immutable
sealed interface IconSource { sealed interface IconSource {
val value: Any val value: Any

View File

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

View File

@ -1,9 +1,10 @@
package ir.amirab.util.compose.action package ir.amirab.util.compose.action
import ir.amirab.util.compose.IconSource import ir.amirab.util.compose.IconSource
import ir.amirab.util.compose.StringSource
abstract class AnAction( abstract class AnAction(
title: String, title: StringSource,
icon: IconSource? = null, icon: IconSource? = null,
) : MenuItem.SingleItem( ) : MenuItem.SingleItem(
title=title, title=title,

View File

@ -1,12 +1,13 @@
package ir.amirab.util.compose.action package ir.amirab.util.compose.action
import ir.amirab.util.compose.IconSource import ir.amirab.util.compose.IconSource
import ir.amirab.util.compose.StringSource
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
inline fun simpleAction( inline fun simpleAction(
title: String, title: StringSource,
icon: IconSource?=null, icon: IconSource? = null,
crossinline onActionPerformed: AnAction.() -> Unit crossinline onActionPerformed: AnAction.() -> Unit,
): AnAction { ): AnAction {
return object : AnAction( return object : AnAction(
title = title, icon = icon, title = title, icon = icon,
@ -15,10 +16,10 @@ inline fun simpleAction(
} }
} }
inline fun simpleAction( inline fun simpleAction(
title: String, title: StringSource,
icon: IconSource?=null, icon: IconSource? = null,
checkEnable:StateFlow<Boolean>, checkEnable: StateFlow<Boolean>,
crossinline onActionPerformed: AnAction.() -> Unit crossinline onActionPerformed: AnAction.() -> Unit,
): AnAction { ): AnAction {
return object : AnAction( return object : AnAction(
title = title, icon = icon, title = title, icon = icon,

View File

@ -1,6 +1,7 @@
package ir.amirab.util.compose.action package ir.amirab.util.compose.action
import ir.amirab.util.compose.IconSource import ir.amirab.util.compose.IconSource
import ir.amirab.util.compose.StringSource
@DslMarker @DslMarker
private annotation class MenuDsl private annotation class MenuDsl
@ -9,7 +10,7 @@ private annotation class MenuDsl
class MenuScope { class MenuScope {
private val list = mutableListOf<MenuItem>() private val list = mutableListOf<MenuItem>()
fun item( fun item(
title: String, title: StringSource,
icon: IconSource? = null, icon: IconSource? = null,
onClick: AnAction.() -> Unit, onClick: AnAction.() -> Unit,
) { ) {
@ -18,7 +19,7 @@ class MenuScope {
} }
fun subMenu( fun subMenu(
title: String, title: StringSource,
icon: IconSource? = null, icon: IconSource? = null,
block: MenuScope.() -> Unit, block: MenuScope.() -> Unit,
) { ) {

View File

@ -3,6 +3,7 @@ package ir.amirab.util.compose.action
import ir.amirab.util.compose.IconSource import ir.amirab.util.compose.IconSource
import ir.amirab.util.flow.mapStateFlow import ir.amirab.util.flow.mapStateFlow
import androidx.compose.runtime.* import androidx.compose.runtime.*
import ir.amirab.util.compose.StringSource
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
sealed interface MenuItem { sealed interface MenuItem {
@ -12,12 +13,12 @@ sealed interface MenuItem {
val icon: StateFlow<IconSource?> val icon: StateFlow<IconSource?>
//compose aware property //compose aware property
val title: StateFlow<String> val title: StateFlow<StringSource>
} }
interface CanBeModified { interface CanBeModified {
fun setIcon(icon: IconSource?) fun setIcon(icon: IconSource?)
fun setTitle(title: String) fun setTitle(title: StringSource)
} }
interface HasEnable { interface HasEnable {
@ -34,7 +35,7 @@ sealed interface MenuItem {
} }
abstract class SingleItem( abstract class SingleItem(
title: String, title: StringSource,
icon: IconSource? = null, icon: IconSource? = null,
) : MenuItem, ) : MenuItem,
ClickableItem, ClickableItem,
@ -45,11 +46,11 @@ sealed interface MenuItem {
var shouldDismissOnClick: Boolean = true var shouldDismissOnClick: Boolean = true
private val _title: MutableStateFlow<String> = MutableStateFlow(title) private val _title: MutableStateFlow<StringSource> = MutableStateFlow(title)
private val _icon: MutableStateFlow<IconSource?> = MutableStateFlow(icon) private val _icon: MutableStateFlow<IconSource?> = MutableStateFlow(icon)
private val _isEnabled: MutableStateFlow<Boolean> = MutableStateFlow(true) private val _isEnabled: MutableStateFlow<Boolean> = MutableStateFlow(true)
override val title: StateFlow<String> = _title.asStateFlow() override val title: StateFlow<StringSource> = _title.asStateFlow()
override val icon: StateFlow<IconSource?> = _icon.asStateFlow() override val icon: StateFlow<IconSource?> = _icon.asStateFlow()
override val isEnabled: StateFlow<Boolean> = _isEnabled.asStateFlow() override val isEnabled: StateFlow<Boolean> = _isEnabled.asStateFlow()
@ -61,7 +62,7 @@ sealed interface MenuItem {
_icon.update { icon } _icon.update { icon }
} }
override fun setTitle(title: String) { override fun setTitle(title: StringSource) {
_title.update { title } _title.update { title }
} }
@ -76,17 +77,17 @@ sealed interface MenuItem {
class SubMenu( class SubMenu(
icon: IconSource? = null, icon: IconSource? = null,
title: String, title: StringSource,
items: List<MenuItem>, items: List<MenuItem>,
) : MenuItem, ) : MenuItem,
ReadableItem, ReadableItem,
HasEnable { HasEnable {
private var _icon: MutableStateFlow<IconSource?> = MutableStateFlow(icon) private var _icon: MutableStateFlow<IconSource?> = MutableStateFlow(icon)
private var _title: MutableStateFlow<String> = MutableStateFlow(title) private var _title: MutableStateFlow<StringSource> = MutableStateFlow(title)
private val _items: MutableStateFlow<List<MenuItem>> = MutableStateFlow(items) private val _items: MutableStateFlow<List<MenuItem>> = MutableStateFlow(items)
override var icon: StateFlow<IconSource?> = _icon.asStateFlow() override var icon: StateFlow<IconSource?> = _icon.asStateFlow()
override var title: StateFlow<String> = _title.asStateFlow() override var title: StateFlow<StringSource> = _title.asStateFlow()
val items: StateFlow<List<MenuItem>> = _items.asStateFlow() val items: StateFlow<List<MenuItem>> = _items.asStateFlow()
fun setItems(newItems: List<MenuItem>) { fun setItems(newItems: List<MenuItem>) {

View File

@ -0,0 +1,4 @@
package ir.amirab.util.compose.contants
const val RESOURCE_PROTOCOL = "app-resource"
const val FILE_PROTOCOL = "file"

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

View File

@ -0,0 +1,7 @@
package ir.amirab.util.compose.localizationmanager
import kotlinx.coroutines.flow.MutableStateFlow
interface LanguageStorage {
val selectedLanguage: MutableStateFlow<String>
}

View File

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

View File

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

View File

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

View File

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

View 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")
}
}
}

View File

@ -0,0 +1,3 @@
plugins {
id(MyPlugins.kotlin)
}

View File

@ -0,0 +1,4 @@
package ir.amirab.resources.contracts
@JvmInline
value class MyStringResource(val id: String)

View File

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