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