preparing the publication

This commit is contained in:
sergeych committed 2026-05-23 17:30:14 +03:00
1 parent bad1e89c26
commit 128275402e
18 files changed
+242 -22

No files matched your search

@@ -438,6 +438,18 @@ actual suspend fun saveScanDownloadsAutomatically(enabled: Boolean) = withContex
}
}
actual suspend fun loadAppLocaleTag(): String? = withContext(Dispatchers.IO) {
openLibraryDatabase().useLibrary { db ->
db.getAppFlag(AppLocaleTagFlag)?.takeIf { it.isNotBlank() }
}
}
actual suspend fun saveAppLocaleTag(localeTag: String?) = withContext(Dispatchers.IO) {
openLibraryDatabase().useLibrary { db ->
db.setAppFlag(AppLocaleTagFlag, localeTag?.takeIf { it.isNotBlank() })
}
}
actual suspend fun loadDownloadsWasScanned(): Boolean = withContext(Dispatchers.IO) {
openLibraryDatabase().useLibrary { db ->
db.getAppFlag(DownloadsWasScannedFlag)?.toBooleanStrictOrNull()
@@ -781,4 +793,5 @@ private val SearchPrefixRegex = Regex("""[\p{L}\p{N}]+""")
private const val ActiveReadingFileIdFlag = "active_reading_file_id"
private const val ThemeModeFlag = "theme_mode"
private const val ScanDownloadsAutomaticallyFlag = "scan_downloads_automatically"
private const val AppLocaleTagFlag = "app_locale_tag"
private const val DownloadsWasScannedFlag = "downloads_was_scanned"
@@ -2,4 +2,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
</adaptive-icon>
@@ -2,4 +2,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
</adaptive-icon>
@@ -51,6 +51,7 @@ private data class PendingLibraryDelete(
fun App() {
var themeMode by remember { mutableStateOf(ThemeMode.SYSTEM) }
var systemDark by remember { mutableStateOf(isPlatformDarkTheme()) }
var localePreferenceLoaded by remember { mutableStateOf(false) }
var toast by remember { mutableStateOf<AppToastData?>(null) }
var nextToastId by remember { mutableStateOf(0L) }
val scope = rememberCoroutineScope()
@@ -78,6 +79,13 @@ fun App() {
themeMode = loadThemeMode()
}
LaunchedEffect(Unit) {
selectedAppLocaleTag = loadAppLocaleTag()?.takeIf { saved ->
availableAppLanguages.any { it.localeTag == saved }
}
localePreferenceLoaded = true
}
LaunchedEffect(toast?.id) {
val current = toast ?: return@LaunchedEffect
delay(current.durationMillis)
@@ -89,17 +97,21 @@ fun App() {
MaterialTheme(colorScheme = if (useDark) darkReaderColorScheme() else lightReaderColorScheme()) {
Box(Modifier.fillMaxSize()) {
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
BookReaderApp(
onThemeToggle = {
val next = themeMode.next()
themeMode = next
showToast(strings.themeChanged(next))
scope.launch {
saveThemeMode(next)
}
},
onShowToast = ::showToast,
)
if (localePreferenceLoaded) {
BookReaderApp(
onThemeToggle = {
val next = themeMode.next()
themeMode = next
showToast(strings.themeChanged(next))
scope.launch {
saveThemeMode(next)
}
},
onShowToast = ::showToast,
)
} else {
LoadingScreen(strings.loadingOpeningBook)
}
}
AppToast(toast, modifier = Modifier.align(Alignment.BottomCenter))
}
@@ -145,6 +145,10 @@ expect suspend fun loadScanDownloadsAutomatically(): Boolean
expect suspend fun saveScanDownloadsAutomatically(enabled: Boolean)
expect suspend fun loadAppLocaleTag(): String?
expect suspend fun saveAppLocaleTag(localeTag: String?)
expect suspend fun loadDownloadsWasScanned(): Boolean
expect suspend fun saveDownloadsWasScanned(scanned: Boolean)
@@ -24,7 +24,9 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.KeyboardArrowDown
@@ -101,6 +103,7 @@ internal fun LibraryScreen(
var recentlyAddedItems by remember(state.items) { mutableStateOf<List<LibraryItem>>(emptyList()) }
var wasScanning by remember { mutableStateOf(false) }
var settingsMenuOpen by remember { mutableStateOf(false) }
var localeMenuOpen by remember { mutableStateOf(false) }
var autoScanDownloads by remember { mutableStateOf(true) }
var autoScanSettingLoaded by remember { mutableStateOf(false) }
var backgroundDownloadsRescanStarted by remember { mutableStateOf(false) }
@@ -221,6 +224,7 @@ internal fun LibraryScreen(
fun rescanAllLibrary() {
settingsMenuOpen = false
localeMenuOpen = false
scope.launch {
busy = true
message = strings.rescanningLibrary
@@ -282,7 +286,10 @@ internal fun LibraryScreen(
}
LaunchedEffect(searchFocused) {
if (searchFocused) settingsMenuOpen = false
if (searchFocused) {
settingsMenuOpen = false
localeMenuOpen = false
}
}
LaunchedEffect(searchActive, loadingPage, endReached, libraryItems, recentlyAdded) {
@@ -366,7 +373,13 @@ internal fun LibraryScreen(
IconButton(onClick = { settingsMenuOpen = true }) {
Icon(Icons.Filled.MoreVert, contentDescription = strings.libraryOptions)
}
DropdownMenu(expanded = settingsMenuOpen, onDismissRequest = { settingsMenuOpen = false }) {
DropdownMenu(
expanded = settingsMenuOpen,
onDismissRequest = {
settingsMenuOpen = false
localeMenuOpen = false
},
) {
// DropdownMenuItem(
// text = { Text("Rescan all library") },
// enabled = !busy && activeScan == null,
@@ -385,6 +398,56 @@ internal fun LibraryScreen(
scope.launch { saveScanDownloadsAutomatically(next) }
},
)
HorizontalDivider()
Box {
DropdownMenuItem(
text = { Text(strings.locale) },
trailingIcon = {
Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null)
},
onClick = { localeMenuOpen = true },
)
DropdownMenu(expanded = localeMenuOpen, onDismissRequest = { localeMenuOpen = false }) {
availableAppLanguages.forEach { language ->
val checked = selectedAppLocaleTag == language.localeTag
DropdownMenuItem(
leadingIcon = {
if (checked) {
Icon(Icons.Filled.Check, contentDescription = null)
}
},
text = { Text(strings.appLanguageName(language)) },
onClick = {
selectedAppLocaleTag = language.localeTag
localeMenuOpen = false
settingsMenuOpen = false
scope.launch { saveAppLocaleTag(language.localeTag) }
},
)
}
HorizontalDivider()
DropdownMenuItem(
leadingIcon = {
if (selectedAppLocaleTag == null) {
Icon(Icons.Filled.Check, contentDescription = null)
}
},
text = { Text(strings.systemLocale) },
onClick = {
selectedAppLocaleTag = null
localeMenuOpen = false
settingsMenuOpen = false
scope.launch { saveAppLocaleTag(null) }
},
)
}
}
HorizontalDivider()
DropdownMenuItem(
text = { Text(strings.appVersion(AppVersionDisplay)) },
enabled = false,
onClick = {},
)
}
}
}
@@ -1,15 +1,22 @@
package net.sergeych.toread
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import net.sergeych.toread.storage.BookReadingStatus
import kotlin.math.roundToInt
internal enum class AppLanguage {
English,
Russian,
internal enum class AppLanguage(val localeTag: String) {
English("en"),
Russian("ru"),
}
internal val availableAppLanguages: List<AppLanguage> = AppLanguage.entries
internal var selectedAppLocaleTag: String? by mutableStateOf(null)
internal val appLanguage: AppLanguage
get() = if (platformLocaleTags().any { it.isRussianLanguageTag() }) {
get() = if (effectiveAppLocaleTags().any { it.isRussianLanguageTag() }) {
AppLanguage.Russian
} else {
AppLanguage.English
@@ -23,6 +30,9 @@ internal val strings: AppStrings
internal expect fun platformLocaleTags(): List<String>
private fun effectiveAppLocaleTags(): List<String> =
selectedAppLocaleTag?.let(::listOf) ?: platformLocaleTags()
private fun String.isRussianLanguageTag(): Boolean =
substringBefore('-').substringBefore('_').equals("ru", ignoreCase = true)
@@ -53,6 +63,9 @@ internal open class AppStrings {
open val libraryOptions = "Library options"
open val scanDownloadsAutomatically = "Scan Downloads automatically"
open val locale = "Locale"
open val systemLocale = "Default"
open fun appVersion(version: String): String = "Version $version"
open val scanFolder = "Scan folder"
open val chooseLibraryFilter = "Choose library filter"
open val searchLibrary = "Search library"
@@ -197,6 +210,11 @@ internal open class AppStrings {
open fun couldNotRead(name: String): String = "Could not read $name"
open fun progressLabel(progress: Double?): String =
progress?.let { "Progress ${(it * 100).roundToInt()}%" } ?: "Progress not recorded"
open fun appLanguageName(language: AppLanguage): String =
when (language) {
AppLanguage.English -> "English"
AppLanguage.Russian -> "Russian"
}
open fun sectionFallback(index: Int): String = "Section ${index + 1}"
open fun readingStatus(status: BookReadingStatus): String =
when (status) {
@@ -237,6 +255,9 @@ internal object RussianStrings : AppStrings() {
override val libraryOptions = "Параметры библиотеки"
override val scanDownloadsAutomatically = "Автоматически сканировать Загрузки"
override val locale = "Локализация"
override val systemLocale = "Системная"
override fun appVersion(version: String): String = "Версия $version"
override val scanFolder = "Сканировать папку"
override val chooseLibraryFilter = "Выбрать фильтр библиотеки"
override val searchLibrary = "Поиск"
@@ -384,6 +405,11 @@ internal object RussianStrings : AppStrings() {
override fun couldNotRead(name: String): String = "Не удалось прочитать $name"
override fun progressLabel(progress: Double?): String =
progress?.let { "Прогресс ${(it * 100).roundToInt()}%" } ?: "Прогресс не записан"
override fun appLanguageName(language: AppLanguage): String =
when (language) {
AppLanguage.English -> "Английский"
AppLanguage.Russian -> "Русский"
}
override fun sectionFallback(index: Int): String = "Раздел ${index + 1}"
override fun readingStatus(status: BookReadingStatus): String =
when (status) {
@@ -373,6 +373,18 @@ actual suspend fun saveScanDownloadsAutomatically(enabled: Boolean) = withContex
}
}
actual suspend fun loadAppLocaleTag(): String? = withContext(Dispatchers.IO) {
openLibraryDatabase().useLibrary { db ->
db.getAppFlag(AppLocaleTagFlag)?.takeIf { it.isNotBlank() }
}
}
actual suspend fun saveAppLocaleTag(localeTag: String?) = withContext(Dispatchers.IO) {
openLibraryDatabase().useLibrary { db ->
db.setAppFlag(AppLocaleTagFlag, localeTag?.takeIf { it.isNotBlank() })
}
}
actual suspend fun loadDownloadsWasScanned(): Boolean = withContext(Dispatchers.IO) {
openLibraryDatabase().useLibrary { db ->
db.getAppFlag(DownloadsWasScannedFlag)?.toBooleanStrictOrNull()
@@ -589,4 +601,5 @@ private fun runCommand(vararg command: String): String? =
private const val ActiveReadingFileIdFlag = "active_reading_file_id"
private const val ThemeModeFlag = "theme_mode"
private const val ScanDownloadsAutomaticallyFlag = "scan_downloads_automatically"
private const val AppLocaleTagFlag = "app_locale_tag"
private const val DownloadsWasScannedFlag = "downloads_was_scanned"
@@ -77,6 +77,17 @@ actual suspend fun loadScanDownloadsAutomatically(): Boolean = true
actual suspend fun saveScanDownloadsAutomatically(enabled: Boolean) = Unit
actual suspend fun loadAppLocaleTag(): String? =
window.localStorage.getItem(AppLocaleStorageKey)?.takeIf { it.isNotBlank() }
actual suspend fun saveAppLocaleTag(localeTag: String?) {
if (localeTag.isNullOrBlank()) {
window.localStorage.removeItem(AppLocaleStorageKey)
} else {
window.localStorage.setItem(AppLocaleStorageKey, localeTag)
}
}
actual suspend fun loadDownloadsWasScanned(): Boolean = false
actual suspend fun saveDownloadsWasScanned(scanned: Boolean) = Unit
@@ -99,6 +110,8 @@ actual fun watchPlatformDarkTheme(onChange: (Boolean) -> Unit): () -> Unit {
actual fun libraryLogPath(): String? = null
private const val AppLocaleStorageKey = "toread.appLocaleTag"
actual fun formatLibraryLastReadTime(millis: Long): String {
val totalMinutes = millis / 60_000L
val minute = (totalMinutes % 60).toString().padStart(2, '0')