Merge pull request #141 from amir1376/feature/localization-support

Add localization
This commit is contained in:
AmirHossein Abdolmotallebi 2024-10-31 00:30:55 +03:30 committed by GitHub
commit d31923bded
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
84 changed files with 2082 additions and 697 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,8 +29,12 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.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,
)

View File

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

View File

@ -30,12 +30,15 @@ import com.abdownloadmanager.desktop.ui.icon.MyIcons
import com.abdownloadmanager.desktop.ui.theme.myColors
import com.abdownloadmanager.desktop.ui.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(),
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,11 @@ import com.abdownloadmanager.desktop.actions.handle
import com.abdownloadmanager.desktop.ui.customwindow.CustomWindow
import com.abdownloadmanager.desktop.ui.customwindow.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(),
)

View File

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

View File

@ -20,6 +20,9 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.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)

View File

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

View File

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

View File

@ -9,13 +9,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.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()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,12 +24,16 @@ import ir.amirab.util.compose.action.buildMenu
import com.abdownloadmanager.desktop.utils.isInDebugMode
import com.abdownloadmanager.desktop.utils.mvi.HandleEffects
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.window.*
import com.abdownloadmanager.desktop.pages.batchdownload.BatchDownloadWindow
import com.abdownloadmanager.desktop.pages.category.ShowCategoryDialogs
import com.abdownloadmanager.desktop.pages.home.HomeWindow
import com.abdownloadmanager.desktop.pages.settings.ThemeManager
import com.abdownloadmanager.desktop.ui.widget.ProvideLanguageManager
import com.abdownloadmanager.utils.compose.ProvideDebugInfo
import ir.amirab.util.compose.localizationmanager.LanguageManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
@ -45,13 +49,16 @@ object Ui : KoinComponent {
) {
val appComponent: AppComponent = get()
val themeManager: ThemeManager = get()
val languageManager: LanguageManager = get()
themeManager.boot()
languageManager.boot()
if (!appArguments.startSilent) {
appComponent.openHome()
}
application {
val theme by themeManager.currentThemeColor.collectAsState()
ProvideDebugInfo(AppInfo.isInDebugMode()) {
ProvideLanguageManager(languageManager) {
ProvideNotificationManager {
ABDownloaderTheme(
myColors = theme,
@ -63,7 +70,7 @@ object Ui : KoinComponent {
SystemTray(appComponent, trayState)
val showHomeSlot = appComponent.showHomeSlot.collectAsState().value
showHomeSlot.child?.instance?.let {
HomeWindow(it,appComponent::closeHome)
HomeWindow(it, appComponent::closeHome)
}
val showSettingSlot = appComponent.showSettingSlot.collectAsState().value
showSettingSlot.child?.instance?.let {
@ -92,6 +99,7 @@ object Ui : KoinComponent {
}
}
}
}
}
@Composable

View File

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

View File

@ -0,0 +1,22 @@
package com.abdownloadmanager.desktop.ui.customwindow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
val LocalTitleBarDirection = staticCompositionLocalOf<LayoutDirection> {
error("TitleBarDirection not provided")
}
@Composable
fun WithTitleBarDirection(
content: @Composable () -> Unit,
) {
CompositionLocalProvider(
LocalLayoutDirection provides LocalTitleBarDirection.current
) {
content()
}
}

View File

@ -18,6 +18,7 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.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()
})
}

View File

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

View File

@ -0,0 +1,32 @@
package com.abdownloadmanager.desktop.ui.widget
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import com.abdownloadmanager.desktop.ui.customwindow.LocalTitleBarDirection
import ir.amirab.util.compose.localizationmanager.LanguageManager
import ir.amirab.util.compose.localizationmanager.LocalLanguageManager
import ir.amirab.util.compose.localizationmanager.LocaleLanguageDirection
@Composable
fun ProvideLanguageManager(
languageManager: LanguageManager,
content: @Composable () -> Unit,
) {
val isRtl = languageManager.isRtl.collectAsState().value
val languageDirection = if (isRtl) {
LayoutDirection.Rtl
} else {
LayoutDirection.Ltr
}
CompositionLocalProvider(
LocalLanguageManager provides languageManager,
LocalLayoutDirection provides languageDirection,
LocalTitleBarDirection provides LayoutDirection.Ltr,
LocaleLanguageDirection provides languageDirection
) {
content()
}
}

View File

@ -19,6 +19,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,12 +6,11 @@ import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.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

View File

@ -0,0 +1,141 @@
package ir.amirab.util.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import arrow.core.combine
import ir.amirab.util.compose.localizationmanager.LanguageManager
import ir.amirab.util.compose.localizationmanager.withReplacedArgs
import ir.amirab.util.compose.resources.MyStringResource
import ir.amirab.util.compose.resources.myStringResource
@Immutable
sealed interface StringSource {
@Composable
fun rememberString(): String
@Composable
fun rememberString(args: Map<String, String>): String
fun getString(): String
fun getString(args: Map<String, String>): String
@Immutable
data class FromString(
val value: String,
) : StringSource {
@Composable
override fun rememberString(): String {
return value
}
@Composable
override fun rememberString(args: Map<String, String>): String {
return remember(args) {
if (args.isEmpty()) {
value
} else {
value.withReplacedArgs(args)
}
}
}
override fun getString(): String {
return value
}
override fun getString(args: Map<String, String>): String {
return if (args.isEmpty()) {
value
} else {
value.withReplacedArgs(args)
}
}
}
@Immutable
data class FromStringResource(
val value: MyStringResource,
val extraArgs: Map<String, String> = emptyMap(),
) : StringSource {
@Composable
override fun rememberString(): String {
return myStringResource(value, extraArgs)
}
@Composable
override fun rememberString(args: Map<String, String>): String {
val argList = remember(extraArgs, args) {
extraArgs.plus(args)
}
return if (argList.isEmpty()) {
myStringResource(value)
} else {
myStringResource(value, argList)
}
}
private fun getLanguageManager(): LanguageManager {
return LanguageManager.instance
}
override fun getString(): String {
return getLanguageManager()
.getMessage(value.id)
.withReplacedArgs(extraArgs)
}
override fun getString(args: Map<String, String>): String {
return getLanguageManager()
.getMessage(value.id)
.withReplacedArgs(extraArgs.plus(args))
}
}
@Immutable
data class CombinedStringSource(
val values: List<StringSource>,
val separator: String,
) : StringSource {
@Composable
override fun rememberString(): String {
return values.map {
it.rememberString()
}.joinToString()
}
@Composable
override fun rememberString(args: Map<String, String>): String {
return values.map {
it.rememberString(args)
}.joinToString()
}
override fun getString(): String {
return values.map {
it.getString()
}.joinToString()
}
override fun getString(args: Map<String, String>): String {
return values.map {
it.getString(args)
}.joinToString()
}
}
}
fun MyStringResource.asStringSource(): StringSource {
return StringSource.FromStringResource(this)
}
fun MyStringResource.asStringSourceWithARgs(args: Map<String, String>): StringSource {
return StringSource.FromStringResource(this, args)
}
fun String.asStringSource(): StringSource {
return StringSource.FromString(this)
}
fun List<StringSource>.combineStringSources(separator: String = ""): StringSource {
return StringSource.CombinedStringSource(this, separator)
}

View File

@ -1,9 +1,10 @@
package ir.amirab.util.compose.action
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,

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,225 @@
package ir.amirab.util.compose.localizationmanager
import androidx.compose.runtime.Immutable
import ir.amirab.util.compose.contants.FILE_PROTOCOL
import ir.amirab.util.compose.contants.RESOURCE_PROTOCOL
import ir.amirab.util.flow.mapStateFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import okio.FileSystem
import okio.Path.Companion.toPath
import okio.buffer
import java.io.InputStream
import java.net.URI
import java.util.Locale
import java.util.Properties
class LanguageManager(
private val storage: LanguageStorage,
) {
private val _languageList: MutableStateFlow<List<LanguageInfo>> = MutableStateFlow(emptyList())
val languageList = _languageList.asStateFlow()
val selectedLanguage = storage.selectedLanguage
val isRtl = selectedLanguage.mapStateFlow {
rtlLanguages.contains(it)
}
fun boot() {
_languageList.value = getAvailableLanguages()
instance = this
}
fun selectLanguage(code: String?) {
val languageCode = code ?: Locale.getDefault().language
val languageInfo = languageList.value.find { it.languageCode == languageCode }
selectedLanguage.value = (languageInfo ?: DefaultLanguageInfo).languageCode
}
fun getMessage(key: String): String {
return getMessageContainer().getMessage(key)
?: defaultLanguageData.value.getMessage(key)
?: key
}
private fun getRequestedLanguage(): String {
return selectedLanguage.value
}
@Volatile
private var loadedLanguage: LoadedLanguage? = null
private val defaultLanguageData = lazy {
createMessageContainer(DefaultLanguageInfo)
}
private fun createMessageContainer(
languageInfo: LanguageInfo,
): MessageData {
return when {
languageInfo == DefaultLanguageInfo && defaultLanguageData.isInitialized() -> defaultLanguageData.value
else -> PropertiesMessageContainer(
Properties().apply {
kotlin.runCatching {
openStream(languageInfo.path)
.reader(Charsets.UTF_8)
.use {
load(it)
}
}.onFailure {
println("Error while loading language data!")
it.printStackTrace()
}
}
)
}
}
private fun bestLanguageInfo(code: String): LanguageInfo {
return languageList.value.find {
it.languageCode == code
} ?: DefaultLanguageInfo
}
private fun getMessageContainer(): MessageData {
val requestedLanguage = getRequestedLanguage()
this.loadedLanguage.let { loadedLanguage ->
if (loadedLanguage != null && loadedLanguage.languageInfo.languageCode == requestedLanguage) {
return loadedLanguage.messageData
}
}
synchronized(this) {
// make sure not created earlier
this.loadedLanguage.let { loadedLanguage ->
if (loadedLanguage != null && loadedLanguage.languageInfo.languageCode == requestedLanguage) {
return loadedLanguage.messageData
}
}
val languageInfo = bestLanguageInfo(requestedLanguage)
val created = LoadedLanguage(
languageInfo,
createMessageContainer(languageInfo)
)
this.loadedLanguage = created
return created.messageData
}
}
private fun getAvailableLanguages(): List<LanguageInfo> {
val fileSystem = FileSystem.RESOURCES
return fileSystem
.list(LOCALES_PATH.toPath())
.mapNotNull {
kotlin.runCatching {
if (fileSystem.metadataOrNull(it)?.isRegularFile == false) {
return@runCatching null
}
val languageCodeAndCountryCode = extractLanguageCodeAndCountryCodeFromFileName(it.name)
?: return@runCatching null
val locale = if (languageCodeAndCountryCode.countryCode != null) {
Locale(languageCodeAndCountryCode.languageCode, languageCodeAndCountryCode.countryCode)
} else {
Locale(languageCodeAndCountryCode.languageCode)
}
locale to it
}.getOrNull()
}.let {
val localesWithPath = it
localesWithPath.map { (locale, path) ->
locale.toLanguageInfo(
path = "$RESOURCE_PROTOCOL://$path",
hasMultiRegion = localesWithPath.count {
it.first.language == locale.language
} > 1
)
}
}
}
companion object {
lateinit var instance: LanguageManager
private const val LOCALES_PATH = "/com/abdownloadmanager/resources/locales"
val DefaultLanguageInfo = LanguageInfo(
languageCode = "en",
countryCode = "US",
nativeName = "English",
path = URI("$RESOURCE_PROTOCOL:$LOCALES_PATH/en_US.properties")
)
fun openStream(uri: URI): InputStream {
return when (uri.scheme) {
RESOURCE_PROTOCOL -> FileSystem.RESOURCES.source(uri.path.toPath())
FILE_PROTOCOL -> FileSystem.SYSTEM.source(uri.path.toPath())
else -> error("unsupported URI")
}.buffer().inputStream()
}
private fun Locale.toLanguageInfo(
path: String,
hasMultiRegion: Boolean,
): LanguageInfo {
return LanguageInfo(
languageCode = language,
countryCode = country,
nativeName = if (hasMultiRegion) getDisplayName(this) else getDisplayLanguage(this),
path = URI(path)
)
}
private val rtlLanguages = arrayOf("ar", "fa", "he", "iw", "ji", "ur", "yi")
private fun extractLanguageCodeAndCountryCodeFromFileName(name: String): LanguageCodeAndCountryCode? {
return name
.split(".")
.firstOrNull()
?.takeIf { it.isNotBlank() }
?.let {
it.split("_").run {
LanguageCodeAndCountryCode(
languageCode = get(0),
countryCode = getOrNull(1)
)
}
}
}
}
}
private data class LanguageCodeAndCountryCode(
val languageCode: String,
val countryCode: String?,
) {
override fun toString(): String {
return buildString {
append(languageCode)
countryCode?.let {
append("_")
append(it)
}
}
}
}
interface MessageData {
fun getMessage(key: String): String?
}
class PropertiesMessageContainer(
private val properties: Properties,
) : MessageData {
override fun getMessage(key: String): String? {
return properties.getProperty(key)
}
}
private data class LoadedLanguage(
val languageInfo: LanguageInfo,
val messageData: MessageData,
)
@Immutable
data class LanguageInfo(
val languageCode: String,
val countryCode: String?,
val nativeName: String,
val path: URI,
)

View File

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

View File

@ -0,0 +1,25 @@
package ir.amirab.util.compose.localizationmanager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
val LocalLanguageManager = staticCompositionLocalOf<LanguageManager> {
error("LocalLanguageManager not provided")
}
val LocaleLanguageDirection = staticCompositionLocalOf<LayoutDirection> {
error("LocaleLanguageDirection not provided")
}
@Composable
fun WithLanguageDirection(
content: @Composable () -> Unit,
) {
CompositionLocalProvider(
LocalLayoutDirection provides LocaleLanguageDirection.current,
) {
content()
}
}

View File

@ -0,0 +1,13 @@
package ir.amirab.util.compose.localizationmanager
import arrow.core.fold
private fun String.replaceWithVariable(name: String, value: String): String {
return replace("{{$name}}", value)
}
internal fun String.withReplacedArgs(args: Map<String, String>): String {
return args.fold(this) { acc, entry ->
acc.replaceWithVariable(entry.key, entry.value)
}
}

View File

@ -0,0 +1,15 @@
package ir.amirab.util.compose.modifiers
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.scale
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
fun Modifier.autoMirror() = composed {
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
scale(
scaleX = if (isRtl) -1f else 1f,
scaleY = 1f
)
}

View File

@ -0,0 +1,30 @@
package ir.amirab.util.compose.resources
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import ir.amirab.util.compose.localizationmanager.LocalLanguageManager
import ir.amirab.util.compose.localizationmanager.withReplacedArgs
typealias MyStringResource = ir.amirab.resources.contracts.MyStringResource
@Composable
fun myStringResource(key: MyStringResource): String {
val languageManager = LocalLanguageManager.current
val language by languageManager.selectedLanguage.collectAsState()
return remember(language, key) {
languageManager.getMessage(key.id)
}
}
@Composable
fun myStringResource(key: MyStringResource, args: Map<String, String>): String {
val languageManager = LocalLanguageManager.current
val language by languageManager.selectedLanguage.collectAsState()
return remember(language, key, args) {
languageManager
.getMessage(key.id)
.withReplacedArgs(args)
}
}

View File

@ -0,0 +1,137 @@
import java.util.Properties
plugins {
id(MyPlugins.kotlin)
}
dependencies {
implementation(project(":shared:resources:contracts"))
}
val propertiesToKotlinTask by tasks.registering(PropertiesToKotlinTask::class) {
outputDir.set(file("build/tasks/propertiesToKotlinTask"))
generatedFileName.set("Res.kt")
packageName.set("com.abdownloadmanager.resources")
myStringResourceClass.set("ir.amirab.resources.contracts.MyStringResource")
propertyFiles.from("src/main/resources/com/abdownloadmanager/resources/locales/en_US.properties")
}
tasks.compileKotlin {
dependsOn(propertiesToKotlinTask)
}
sourceSets {
main {
kotlin {
srcDirs(propertiesToKotlinTask.map { it.outputDir })
}
}
}
abstract class PropertiesToKotlinTask @Inject constructor(
project: Project,
) : DefaultTask() {
@get:InputFiles
val propertyFiles = project.objects.fileCollection()
@get:Input
val packageName = project.objects.property<String>()
@get:Input
val myStringResourceClass = project.objects.property<String>()
@get:OutputDirectory
val outputDir = project.objects.directoryProperty()
@get:Input
val generatedFileName = project.objects.property<String>()
@TaskAction
fun run() {
val properties = Properties()
propertyFiles.forEach { file ->
file.inputStream().use { inputStream ->
properties.load(inputStream)
}
}
val content = createFileString(
packageName.get(),
myStringResourceClass.get(),
properties
)
outputDir.file(generatedFileName).get().asFile.writer().use {
it.write(content)
}
}
private fun createFileString(
packageName: String,
myStringResourceClass: String,
properties: Properties,
): String {
val myStringResourceClassName = myStringResourceClass
.split(".").last()
val variableRegex by lazy { "\\{\\{(?<variable>.+)\\}\\}".toRegex() }
fun findVariablesOfValue(value: String): List<String> {
return variableRegex
.findAll(value)
.toList()
.map {
it.groups["variable"]!!.value
}
}
fun propertyToCode(key: String, value: String): String {
val args = findVariablesOfValue(value)
val defination = "val `$key` = $myStringResourceClassName(\"$key\")"
if (args.isEmpty()) {
return defination
} else {
val comment = buildString {
append("/**\n")
append("accepted args:\n")
args.forEach { value ->
append("@param [$value]\n")
}
append("*/")
}
val argCreatorFunction = buildString {
append("fun `${key}_createArgs`(")
args.forEachIndexed { index, value ->
append("$value: String")
if (index != args.lastIndex) {
append(", ")
}
}
append(") = ")
append("mapOf(")
args.forEachIndexed { index, value ->
append("\"$value\" to $value")
if (index != args.lastIndex) {
append(", ")
}
}
append(")")
}
return "$defination\n$comment\n$argCreatorFunction"
}
}
return buildString {
append("@file:Suppress(\"RemoveRedundantBackticks\", \"FunctionName\")")
append("package $packageName\n")
append("import $myStringResourceClass\n")
append("object Res {\n")
append(" object string {\n")
for (property in properties) {
val key = property.key.toString()
val value = property.value.toString()
val codeLines = propertyToCode(key, value).lines()
for (line in codeLines) {
append(" $line\n")
}
}
append(" }\n")
append("}\n")
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,271 @@
app_title=AB Download Manager
confirm_auto_categorize_downloads_title=Auto categorize downloads
confirm_auto_categorize_downloads_description=Any uncategorized item will be automatically added to its related category.
confirm_reset_to_default_categories_title=Reset to Default Categories
confirm_reset_to_default_categories_description=this will REMOVE all categories and brings backs default categories
confirm_delete_download_items_title=Confirm Delete
confirm_delete_download_items_description=Are you sure you want to delete {{count}} items?
also_delete_file_from_disk=Also delete file from disk
confirm_delete_category_item_title=Removing {{name}} category
confirm_delete_category_item_description=Are you sure you want to delete "{{value}}" Category ?
your_download_will_not_be_deleted=Your downloads won't be deleted
drop_link_or_file_here=Drop link or file here.
nothing_will_be_imported=Your downloads won't be deleted
n_links_will_be_imported={{count}} links will be imported
n_items_selected={{count}} items selected
delete=Delete
remove=Remove
cancel=Cancel
close=Close
ok=Ok
add=Add
change=Change
download=Download
refresh=Refresh
settings=Settings
unknown=Unknown
unknown_error=Unknown Error
download_item_not_found=Download item not found
name=Name
download_link=Download link
not_finished=Not finished
all=All
finished=Finished
Unfinished=Unfinished
canceled=Canceled
error=Error
paused=Paused
downloading=Downloading
added=Added
idle=IDLE
preparing_file=Preparing File
creating_file=Creating File
resuming=Resuming
list_is_empty=List is empty.
search_in_the_list=Search in the List
search=Search
clear=Clear
general=General
enabled=Enabled
disabled=Disabled
file=File
tasks=Tasks
tools=Tools
help=Help
all_finished=All Finished
all_unfinished=All Unfinished
entire_list=Entire List
download_browser_integration=Download Browser Integration
exit=Exit
show_downloads=Show Downloads
new_download=New Download
stop_all=Stop All
import_from_clipboard=New Download
batch_download=Batch Download
open=Open
open_file=Open File
open_folder=Open Folder
resume=Resume
pause=Pause
restart_download=Restart Download
copy_link=Copy link
show_properties=Show Properties
move_to_queue=Move To Queue
move_to_category=Move To Category
add_category=Add Category
edit_category=Edit Category
delete_category=Delete Category
category_name=Category Name
category_download_location=Category Download Location
category_download_location_description=When this category chosen in "Add Download Page" use this directory as "Download Location"
category_file_types=Category file types
category_file_types_description=Automatically put these file types to this category. (when you add new download)\nSeparate file extensions with space (ext1 ext2 ...)
url_patterns=URL Patterns
url_patterns_description=Automatically put download from these URLs to this category. (when you add new download)\nSeparate URLs with space, you can also use * for wildcard
auto_categorize_downloads=Auto Categorise Downloads
restore_defaults=Restore Defaults
about=About
version_n=Version {{value}}
developed_with_love_for_you=Developed with ?? for you
visit_the_project_website=Visit the project website
this_is_a_free_and_open_source_software=This is a free & Open Source software
view_the_source_code=See the Source Code
powered_by_open_source_software=Powered by Open Source Software
view_the_open_source_licenses=View the Open-Source licenses
support_and_community=Support & Community
telegram=Telegram
channel=Channel
group=Group
add_download=Add Download
add_multi_download_page_header=Select Items you want to pick up for download
save_to=Save To
where_should_each_item_saved=Where should each item saved?
there_are_multiple_items_please_select_a_way_you_want_to_save_them=There are multiple items! please select a way you want to save them
each_item_on_its_own_category=Each item on its own category
each_item_on_its_own_category_description=Each item will be placed in a category that have that file type
all_items_in_one_category=All items in one Category
all_items_in_one_category_description=All files will be saved in the selected category location
all_items_in_one_Location=All items in one Location
all_items_in_one_Location_description=All items will be saved in the selected directory
no_category_selected=No Category Selected
download_location=Download Location
location=Location
select_queue=Select Queue
without_queue=Without Queue
use_category=Use Category
cant_write_to_this_folder=Can't write to this folder
file_name_already_exists=File name already exists
invalid_file_name=Invalid filename
show_solutions=Show solutions...
change_solution=Change solution
select_a_solution=Select a solution
select_download_strategy_description=The link you provided is already in download lists please specify what you want to do
download_strategy_add_a_numbered_file=Add a numbered file
download_strategy_add_a_numbered_file_description=Add an index after the end of download file name
download_strategy_override_existing_file=Override existing file
download_strategy_override_existing_file_description=Remove existing download and write to that file
download_strategy_show_downloaded_file=Show downloaded file
download_strategy_show_downloaded_file_description=Show already existing download item, so you can press on resume or open it
batch_download_link_help=Enter a link that contains wildcards (use *)
invalid_url=Invalid URL
list_is_too_large_maximum_n_items_allowed=List is too large! maximum {{count}} items allowed
enter_range=Enter range
from=From
to=To
wildcard_length=Wildcard length
first_link=First Link
last_link=Last Link
open_source_software_used_in_this_app=Open Source Software used in this App
links=Links
website=Website
developers=Developers
source_code=Source Code
license=License
no_license_found=No license found
organization=Organization
add_new_queue=Add New Queue
queue_name=Queue Name
queues=Queues
stop_queue=Stop Queue
start_queue=Start Queue
config=Config
items=Items
move_down=Move down
move_up=Move up
remove_queue=Remove Queue
queue_name_help=Specify A name for this queue
queue_name_describe=Queue name is {{value}}
queue_max_concurrent_download=Max concurrent download
queue_max_concurrent_download_description=Max download for this queue
queue_automatic_stop=Automatic stop
queue_automatic_stop_description=Automatic stop queue when there is no item in it
queue_scheduler=Scheduler
queue_enable_scheduler=Enable Scheduler
queue_active_days=Active Days
queue_active_days_description=Which days schedulers function ?
queue_scheduler_auto_start_time=Auto Start Time
queue_scheduler_enable_auto_stop_time=Enable Auto Stop Time
queue_scheduler_auto_stop_time=Auto Stop Time
appearance=Appearance
download_engine=Download Engine
browser_integration=Browser Integration
settings_download_thread_count=Thread Count
settings_download_thread_count_description=Maximum download thread per download item
settings_download_thread_count_describe=a download can have up to {{count}} threads
settings_use_server_last_modified_time=Use Server's Last-Modified Time
settings_use_server_last_modified_time_description=When downloading a file, use server's last modified time for the local file
settings_use_sparse_file_allocation=Sparse File Allocation
settings_use_sparse_file_allocation_description=When downloading a file, use server's last modified time for the local file
settings_global_speed_limiter=Global Speed Limiter
settings_global_speed_limiter_description=Global download speed limit (0 means unlimited)
settings_show_average_speed=Show Average Speed
settings_show_average_speed_description=Download speed in average or precision
settings_default_download_folder=Default Download Folder
settings_default_download_folder_description=When you add new download this location is used by default
settings_default_download_folder_describe="{{folder}}" will be used
settings_use_proxy=Use Proxy
settings_use_proxy_description=Use proxy for downloading files
settings_use_proxy_describe_no_proxy=No Proxy will be used
settings_use_proxy_describe_system_proxy=System Proxy will be used
settings_use_proxy_describe_manual_proxy="{{value}}" will be used
settings_theme=Theme
settings_theme_description=Select a theme for the App
settings_language=Language
settings_compact_top_bar=Compact Top Bar
settings_compact_top_bar_description=Merge top bar with title bar when the main window has enough width
settings_start_on_boot=Start On Boot
settings_start_on_boot_description=Auto start application on user logins
settings_notification_sound=Notification Sound
settings_notification_sound_description=Play sound on new notification
settings_browser_integration=Browser Integration
settings_browser_integration_description=Accept downloads from browsers
settings_browser_integration_server_port=Server Port
settings_browser_integration_server_port_description=Port for browser integration
settings_browser_integration_server_port_describe=App will listen to {{port}}
settings_dynamic_part_creation=Dynamic part creation
settings_dynamic_part_creation_description=When a part is finished create another part by splitting other parts to improve download speed
download_item_settings_speed_limit=Speed Limit
download_item_settings_speed_limit_description=Limit download speed for this item
download_item_settings_thread_count=Thread count
download_item_settings_thread_count_description=How much thread used to download this download item (0 for default)
download_item_settings_thread_count_describe={{count}} threads for this download
download_item_settings_username_description=Provide a username if the link is a protected resource
download_item_settings_password_description=Provide a password if the link is a protected resource
username=Username
password=Password
average_speed=Average Speed
exact_speed=Exact Speed
unlimited=Unlimited
use_global_settings=Use Global Settings
cant_run_browser_integration=Can't run browser integration
cant_open_file=Can't Open File
cant_open_folder=Can't Open Folder
# times for example 2 seconds ago
relative_time_long_years={{years}} years
relative_time_long_months={{months}} months
relative_time_long_days={{days}} days
relative_time_long_hours={{hours}} hours
relative_time_long_minutes={{minutes}} minutes
relative_time_long_seconds={{seconds}} seconds
relative_time_short_years={{years}} y
relative_time_short_months={{months}} M
relative_time_short_days={{days}} d
relative_time_short_hours={{hours}} hr
relative_time_short_minutes={{minutes}} min
relative_time_short_seconds={{seconds}} sec
relative_time_left={{time}} left
relative_time_ago={{time}} ago
auto=Auto
unspecified=Unspecified
custom=Custom
icon=Icon
author=Author
link=Link
size=Size
status=Status
parts_info_downloaded_size=Downloaded
parts_info_total_size=Total
speed=Speed
time_left=Time Left
date_added=Date Added
info=Info
download_page_downloaded_size=Downloaded
resume_support=Resume Support
yes=Yes
no=No
parts_info=Parts Info
disconnected=Disconnected
receiving_data=Receiving Data
send_get=Send Get
warning=Warning
unsupported_resume_warning=This download doesn't support resuming! You may have to RESTART it later in the Download List
stop_anyway=Stop Anyway
customize_columns=Customize Columns
reset=Reset
monday=Monday
tuesday=Tuesday
wednesday=Wednesday
thursday=Thursday
friday=Friday
saturday=Saturday
sunday=Sunday