preparing the publication
This commit is contained in:
parent
bad1e89c26
commit
128275402e
@ -2,6 +2,10 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
val appVersionName = "1.0"
|
||||
val appVersionCode = 1
|
||||
val appVersionDisplay = "$appVersionName.$appVersionCode"
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.androidApplication)
|
||||
@ -10,6 +14,38 @@ plugins {
|
||||
alias(libs.plugins.composeHotReload)
|
||||
}
|
||||
|
||||
abstract class GenerateAppVersionConstantsTask : DefaultTask() {
|
||||
@get:Input
|
||||
abstract val versionName: Property<String>
|
||||
|
||||
@get:Input
|
||||
abstract val versionCode: Property<Int>
|
||||
|
||||
@get:OutputDirectory
|
||||
abstract val outputDir: DirectoryProperty
|
||||
|
||||
@TaskAction
|
||||
fun generate() {
|
||||
val outputFile = outputDir.file("net/sergeych/toread/AppVersion.kt").get().asFile
|
||||
outputFile.parentFile.mkdirs()
|
||||
outputFile.writeText(
|
||||
"""
|
||||
package net.sergeych.toread
|
||||
|
||||
internal const val AppVersionName = "${versionName.get()}"
|
||||
internal const val AppVersionCode = ${versionCode.get()}
|
||||
internal const val AppVersionDisplay = "${versionName.get()}.${versionCode.get()}"
|
||||
""".trimIndent() + "\n",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val generateAppVersionConstants by tasks.registering(GenerateAppVersionConstantsTask::class) {
|
||||
versionName.set(appVersionName)
|
||||
versionCode.set(appVersionCode)
|
||||
outputDir.set(layout.buildDirectory.dir("generated/appVersion/commonMain/kotlin"))
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
|
||||
@ -33,6 +69,9 @@ kotlin {
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
kotlin.srcDir(generateAppVersionConstants)
|
||||
}
|
||||
androidMain.dependencies {
|
||||
implementation(libs.compose.uiToolingPreview)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
@ -69,8 +108,8 @@ android {
|
||||
applicationId = "net.sergeych.toread"
|
||||
minSdk = libs.versions.android.minSdk.get().toInt()
|
||||
targetSdk = libs.versions.android.targetSdk.get().toInt()
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
versionCode = appVersionCode
|
||||
versionName = appVersionName
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
@ -100,7 +139,7 @@ compose.desktop {
|
||||
nativeDistributions {
|
||||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
||||
packageName = "To Read"
|
||||
packageVersion = "1.0.0"
|
||||
packageVersion = appVersionDisplay
|
||||
|
||||
macOS {
|
||||
iconFile.set(project.file("src/jvmMain/resources/icons/icon.icns"))
|
||||
|
||||
BIN
composeApp/release/baselineProfiles/0/composeApp-release.dm
Normal file
BIN
composeApp/release/baselineProfiles/0/composeApp-release.dm
Normal file
Binary file not shown.
BIN
composeApp/release/baselineProfiles/1/composeApp-release.dm
Normal file
BIN
composeApp/release/baselineProfiles/1/composeApp-release.dm
Normal file
Binary file not shown.
BIN
composeApp/release/composeApp-release.apk
Normal file
BIN
composeApp/release/composeApp-release.apk
Normal file
Binary file not shown.
37
composeApp/release/output-metadata.json
Normal file
37
composeApp/release/output-metadata.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"version": 3,
|
||||
"artifactType": {
|
||||
"type": "APK",
|
||||
"kind": "Directory"
|
||||
},
|
||||
"applicationId": "net.sergeych.toread",
|
||||
"variantName": "release",
|
||||
"elements": [
|
||||
{
|
||||
"type": "SINGLE",
|
||||
"filters": [],
|
||||
"attributes": [],
|
||||
"versionCode": 1,
|
||||
"versionName": "1.0",
|
||||
"outputFile": "composeApp-release.apk"
|
||||
}
|
||||
],
|
||||
"elementType": "File",
|
||||
"baselineProfiles": [
|
||||
{
|
||||
"minApi": 28,
|
||||
"maxApi": 30,
|
||||
"baselineProfiles": [
|
||||
"baselineProfiles/1/composeApp-release.dm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"minApi": 31,
|
||||
"maxApi": 2147483647,
|
||||
"baselineProfiles": [
|
||||
"baselineProfiles/0/composeApp-release.dm"
|
||||
]
|
||||
}
|
||||
],
|
||||
"minSdkVersionForDexing": 26
|
||||
}
|
||||
@ -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')
|
||||
|
||||
BIN
screenshots/Screenshot_20260523_170411.png
Normal file
BIN
screenshots/Screenshot_20260523_170411.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 194 KiB |
BIN
screenshots/Screenshot_20260523_170540.png
Normal file
BIN
screenshots/Screenshot_20260523_170540.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
BIN
screenshots/Screenshot_20260523_170604 (101).
Normal file
BIN
screenshots/Screenshot_20260523_170604 (101).
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 179 KiB |
BIN
screenshots/Screenshot_20260523_170621.png
Normal file
BIN
screenshots/Screenshot_20260523_170621.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 171 KiB |
Loading…
x
Reference in New Issue
Block a user