Compare commits
No commits in common. "128275402e48d5169907d292c05a569c84a6141d" and "75ba13bde2447ae8147e1d2c8c564abdcfcffa58" have entirely different histories.
128275402e
...
75ba13bde2
@ -2,10 +2,6 @@ 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)
|
||||
@ -14,38 +10,6 @@ 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)
|
||||
|
||||
@ -69,9 +33,6 @@ kotlin {
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
kotlin.srcDir(generateAppVersionConstants)
|
||||
}
|
||||
androidMain.dependencies {
|
||||
implementation(libs.compose.uiToolingPreview)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
@ -108,8 +69,8 @@ android {
|
||||
applicationId = "net.sergeych.toread"
|
||||
minSdk = libs.versions.android.minSdk.get().toInt()
|
||||
targetSdk = libs.versions.android.targetSdk.get().toInt()
|
||||
versionCode = appVersionCode
|
||||
versionName = appVersionName
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
@ -138,8 +99,8 @@ compose.desktop {
|
||||
|
||||
nativeDistributions {
|
||||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
||||
packageName = "To Read"
|
||||
packageVersion = appVersionDisplay
|
||||
packageName = "net.sergeych.toread"
|
||||
packageVersion = "1.0.0"
|
||||
|
||||
macOS {
|
||||
iconFile.set(project.file("src/jvmMain/resources/icons/icon.icns"))
|
||||
@ -150,7 +111,6 @@ compose.desktop {
|
||||
menu = true
|
||||
}
|
||||
linux {
|
||||
packageName = "to-read"
|
||||
iconFile.set(project.file("src/jvmMain/resources/icons/icon.png"))
|
||||
shortcut = true
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,37 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@ -198,7 +198,7 @@ actual suspend fun scanLibrarySubtree(
|
||||
scanLibraryContentTree(Uri.parse(path), ::emitProgress)
|
||||
} else {
|
||||
if (path.requiresExternalFileAccess() && directoryChooser?.ensureExternalFileAccess() != true) {
|
||||
error(strings.allFilesAccessRequired(path))
|
||||
error("All files access is required to scan $path.")
|
||||
}
|
||||
openLibraryDatabase().useLibrary { db ->
|
||||
val summary = LibraryScanner(db, ::appendLibraryLog).scanSubtree(File(path)) {
|
||||
@ -363,7 +363,7 @@ actual suspend fun shareLibraryBookFile(fileId: String): Boolean = withContext(D
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
val chooser = Intent.createChooser(intent, strings.shareBook).apply {
|
||||
val chooser = Intent.createChooser(intent, "Share book").apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
@ -438,18 +438,6 @@ 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()
|
||||
@ -539,7 +527,7 @@ private fun scanLibraryContentTree(
|
||||
onProgress(LibraryScanProgress(scanned, imported, skipped, failed, document.name))
|
||||
val result = runCatching {
|
||||
val bytes = appContext.contentResolver.openInputStream(document.uri)?.use { it.readBytes() }
|
||||
?: error(strings.couldNotRead(document.name))
|
||||
?: error("Could not read ${document.name}")
|
||||
scanner.importExternalFile(
|
||||
bytes = bytes,
|
||||
displayName = document.name,
|
||||
@ -793,5 +781,4 @@ 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"
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
package net.sergeych.toread
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
internal actual fun platformLocaleTags(): List<String> =
|
||||
listOf(Locale.getDefault().toLanguageTag())
|
||||
@ -82,8 +82,8 @@ class MainActivity : ComponentActivity(), AndroidLibraryDirectoryChooser {
|
||||
private fun showDirectoryChoice(result: CompletableDeferred<String?>) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(strings.chooseLibraryFolder)
|
||||
.setItems(arrayOf(strings.downloads, strings.otherFolder)) { _, which ->
|
||||
.setTitle("Choose library folder")
|
||||
.setItems(arrayOf("Downloads", "Other folder...")) { _, which ->
|
||||
when (which) {
|
||||
0 -> chooseDownloadsDirectory(result)
|
||||
else -> launchSystemDirectoryPicker(result)
|
||||
|
||||
@ -146,7 +146,7 @@ private object AndroidReadAloudEngine {
|
||||
if (status != TextToSpeech.SUCCESS) {
|
||||
mutableSettingsState.value = mutableSettingsState.value.copy(
|
||||
loading = false,
|
||||
message = strings.couldNotLoadAndroidTtsEngines,
|
||||
message = "Could not load Android TTS engines.",
|
||||
)
|
||||
probe?.shutdown()
|
||||
if (engineProbe === probe) engineProbe = null
|
||||
@ -319,7 +319,7 @@ private object AndroidReadAloudEngine {
|
||||
if (status != TextToSpeech.SUCCESS) {
|
||||
mutableSettingsState.value = mutableSettingsState.value.copy(
|
||||
loading = false,
|
||||
message = strings.couldNotLoadSelectedTtsVoices,
|
||||
message = "Could not load voices for the selected TTS engine.",
|
||||
)
|
||||
probe?.shutdown()
|
||||
if (voiceProbe === probe) voiceProbe = null
|
||||
@ -349,7 +349,7 @@ private object AndroidReadAloudEngine {
|
||||
voices = voices,
|
||||
selectedVoiceId = savedVoice,
|
||||
loading = false,
|
||||
message = if (voices.isEmpty()) strings.noOfflineVoices else null,
|
||||
message = if (voices.isEmpty()) "No offline voices reported by the selected TTS engine." else null,
|
||||
)
|
||||
if (savedVoice == null && selectedVoiceId(context) != null) {
|
||||
context.readAloudPrefs().edit().remove(SelectedVoiceKey).apply()
|
||||
@ -443,20 +443,20 @@ class ReadAloudService : Service() {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
val playStopAction = if (state.playing) ActionStop else ActionPlay
|
||||
val playStopTitle = if (state.playing) strings.stop else strings.play
|
||||
val playStopTitle = if (state.playing) "Stop" else "Play"
|
||||
val playStopIcon = if (state.playing) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play
|
||||
|
||||
return NotificationCompat.Builder(this, ChannelId)
|
||||
.setSmallIcon(R.drawable.ic_launcher_background)
|
||||
.setContentTitle(strings.readAloud)
|
||||
.setContentText(strings.readingInBackground)
|
||||
.setContentTitle("Read aloud")
|
||||
.setContentText("Reading in the background")
|
||||
.setContentIntent(contentIntent)
|
||||
.setOngoing(state.playing)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.addAction(android.R.drawable.ic_media_previous, strings.previous, serviceIntent(ActionBack))
|
||||
.addAction(android.R.drawable.ic_media_previous, "Previous", serviceIntent(ActionBack))
|
||||
.addAction(playStopIcon, playStopTitle, serviceIntent(playStopAction))
|
||||
.addAction(android.R.drawable.ic_media_next, strings.next, serviceIntent(ActionForward))
|
||||
.addAction(android.R.drawable.ic_media_next, "Next", serviceIntent(ActionForward))
|
||||
.build()
|
||||
}
|
||||
|
||||
@ -481,7 +481,7 @@ class ReadAloudService : Service() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
val channel = NotificationChannel(
|
||||
ChannelId,
|
||||
strings.readAloud,
|
||||
"Read aloud",
|
||||
NotificationManager.IMPORTANCE_LOW,
|
||||
)
|
||||
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
|
||||
|
||||
@ -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>
|
||||
@ -1,3 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name">Читать</string>
|
||||
</resources>
|
||||
@ -1,3 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">To Read</string>
|
||||
</resources>
|
||||
<string name="app_name">toread</string>
|
||||
</resources>
|
||||
@ -51,7 +51,6 @@ 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()
|
||||
@ -79,13 +78,6 @@ 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)
|
||||
@ -97,21 +89,17 @@ fun App() {
|
||||
MaterialTheme(colorScheme = if (useDark) darkReaderColorScheme() else lightReaderColorScheme()) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||
if (localePreferenceLoaded) {
|
||||
BookReaderApp(
|
||||
onThemeToggle = {
|
||||
val next = themeMode.next()
|
||||
themeMode = next
|
||||
showToast(strings.themeChanged(next))
|
||||
scope.launch {
|
||||
saveThemeMode(next)
|
||||
}
|
||||
},
|
||||
onShowToast = ::showToast,
|
||||
)
|
||||
} else {
|
||||
LoadingScreen(strings.loadingOpeningBook)
|
||||
}
|
||||
BookReaderApp(
|
||||
onThemeToggle = {
|
||||
val next = themeMode.next()
|
||||
themeMode = next
|
||||
showToast("Theme: ${next.displayName}")
|
||||
scope.launch {
|
||||
saveThemeMode(next)
|
||||
}
|
||||
},
|
||||
onShowToast = ::showToast,
|
||||
)
|
||||
}
|
||||
AppToast(toast, modifier = Modifier.align(Alignment.BottomCenter))
|
||||
}
|
||||
@ -249,15 +237,15 @@ private fun BookReaderApp(
|
||||
}
|
||||
|
||||
onShowToast(
|
||||
strings.removed(request.title),
|
||||
strings.undo,
|
||||
"Removed ${request.title}.",
|
||||
"Undo",
|
||||
{
|
||||
if (pendingDelete?.id == deleteId) {
|
||||
pendingDeleteJob?.cancel()
|
||||
pendingDeleteJob = null
|
||||
pendingDelete = null
|
||||
restore()
|
||||
onShowToast(strings.restored(request.title), null, null, DefaultToastDurationMillis)
|
||||
onShowToast("Restored ${request.title}.", null, null, DefaultToastDurationMillis)
|
||||
}
|
||||
},
|
||||
DeleteUndoDurationMillis,
|
||||
@ -315,9 +303,9 @@ private fun BookReaderApp(
|
||||
if (isDownloadsScanPath(path)) {
|
||||
saveDownloadsWasScanned(true)
|
||||
}
|
||||
strings.scanReport(it.scannedFiles, it.importedFiles, it.skippedFiles, it.failedFiles)
|
||||
"Scanned ${it.scannedFiles}, imported ${it.importedFiles}, skipped ${it.skippedFiles}, failed ${it.failedFiles}."
|
||||
},
|
||||
onFailure = { it.message ?: strings.scanFailed },
|
||||
onFailure = { it.message ?: "Scan failed." },
|
||||
)
|
||||
scanJob = null
|
||||
activeScan = null
|
||||
@ -341,7 +329,7 @@ private fun BookReaderApp(
|
||||
}
|
||||
|
||||
when (val current = state) {
|
||||
AppState.LoadingStartup -> LoadingScreen(strings.loadingOpeningBook)
|
||||
AppState.LoadingStartup -> LoadingScreen("Opening book")
|
||||
is AppState.Library -> LibraryScreen(
|
||||
state = current,
|
||||
activeScan = activeScan,
|
||||
@ -357,7 +345,7 @@ private fun BookReaderApp(
|
||||
onStateChange = { state = it },
|
||||
onStartScan = { path ->
|
||||
startScan(path)
|
||||
state = AppState.Library(current.items, path, strings.scanning)
|
||||
state = AppState.Library(current.items, path, "Scanning...")
|
||||
},
|
||||
)
|
||||
is AppState.Reader -> BookView(
|
||||
|
||||
@ -40,7 +40,7 @@ internal suspend fun loadStartupState(): AppState {
|
||||
val scanPath = try {
|
||||
defaultLibraryScanPath().orEmpty()
|
||||
} catch (t: Throwable) {
|
||||
return AppState.Error(t.message ?: strings.couldNotOpenLibrary)
|
||||
return AppState.Error(t.message ?: "Could not open library.")
|
||||
}
|
||||
loadPlatformOpenBookRequest()?.let { request ->
|
||||
return runCatching {
|
||||
@ -50,8 +50,8 @@ internal suspend fun loadStartupState(): AppState {
|
||||
libraryItems = emptyList(),
|
||||
scanPath = scanPath,
|
||||
)
|
||||
}.getOrElse {
|
||||
AppState.Library(emptyList(), scanPath, it.message ?: strings.couldNotOpen(request.displayName))
|
||||
}.getOrElse {
|
||||
AppState.Library(emptyList(), scanPath, it.message ?: "Could not open ${request.displayName}.")
|
||||
}
|
||||
}
|
||||
val activeFileId = loadActiveReadingFileId() ?: return AppState.Library(emptyList(), scanPath)
|
||||
@ -61,7 +61,7 @@ internal suspend fun loadStartupState(): AppState {
|
||||
return AppState.Library(emptyList(), scanPath)
|
||||
}
|
||||
return runCatching {
|
||||
val bytes = openLibraryBook(activeFileId) ?: error(strings.bookFileNotAvailable)
|
||||
val bytes = openLibraryBook(activeFileId) ?: error("Book file is not available.")
|
||||
AppState.Reader(
|
||||
fileId = activeFileId,
|
||||
book = Fb2Format.parse(bytes, item.storageUri ?: item.title),
|
||||
@ -70,7 +70,7 @@ internal suspend fun loadStartupState(): AppState {
|
||||
)
|
||||
}.getOrElse {
|
||||
saveActiveReadingFileId(null)
|
||||
AppState.Library(emptyList(), scanPath, it.message ?: strings.couldNotReopenLastBook())
|
||||
AppState.Library(emptyList(), scanPath, it.message ?: "Could not reopen last book.")
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,5 +82,5 @@ internal suspend fun loadLibraryState(message: String? = null, scanPath: String?
|
||||
message = message,
|
||||
)
|
||||
}.getOrElse {
|
||||
AppState.Error(it.message ?: strings.couldNotOpenLibrary)
|
||||
AppState.Error(it.message ?: "Could not open library.")
|
||||
}
|
||||
|
||||
@ -51,10 +51,10 @@ internal fun BookInfoScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = { Text(strings.bookInfo) },
|
||||
title = { Text("Book info") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = strings.backToReader)
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back to reader")
|
||||
}
|
||||
},
|
||||
colors = themedTopAppBarColors(),
|
||||
@ -70,46 +70,46 @@ internal fun BookInfoScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
item {
|
||||
InfoSection(strings.titleInfo) {
|
||||
InfoSection("Title Info") {
|
||||
CoverAndTitle(book, onImageOpen = onImageOpen)
|
||||
DetailLine(strings.title, book.title)
|
||||
DetailLine(strings.authors, book.authors.joinToString { it.displayName }.ifBlank { strings.unknownAuthor })
|
||||
DetailLine(strings.language, book.language?.uppercase() ?: strings.notSpecified)
|
||||
DetailLine(strings.date, book.date ?: strings.notSpecified)
|
||||
DetailLine(strings.genres, book.genres.joinToString().ifBlank { strings.notSpecified })
|
||||
DetailLine(strings.source, book.sourceLanguage?.uppercase() ?: strings.notSpecified)
|
||||
DetailLine("Title", book.title)
|
||||
DetailLine("Authors", book.authors.joinToString { it.displayName }.ifBlank { "Unknown author" })
|
||||
DetailLine("Language", book.language?.uppercase() ?: "Not specified")
|
||||
DetailLine("Date", book.date ?: "Not specified")
|
||||
DetailLine("Genres", book.genres.joinToString().ifBlank { "Not specified" })
|
||||
DetailLine("Source", book.sourceLanguage?.uppercase() ?: "Not specified")
|
||||
if (book.annotation.isNullOrBlank().not()) {
|
||||
Text(book.annotation.orEmpty(), style = MaterialTheme.typography.bodyMedium, lineHeight = 20.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
InfoSection(strings.statistics) {
|
||||
InfoSection("Statistics") {
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
StatBlock(strings.words, stats.words.formatCompact())
|
||||
StatBlock(strings.sections, stats.sections.toString())
|
||||
StatBlock(strings.images, stats.images.toString())
|
||||
StatBlock("Words", stats.words.formatCompact())
|
||||
StatBlock("Sections", stats.sections.toString())
|
||||
StatBlock("Images", stats.images.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
InfoSection(strings.lastReadingPosition) {
|
||||
InfoSection("Last Reading Position") {
|
||||
val position = extras?.lastReadingPosition
|
||||
if (position == null) {
|
||||
Text(strings.noSavedPosition, style = MaterialTheme.typography.bodyMedium)
|
||||
Text("No saved position", style = MaterialTheme.typography.bodyMedium)
|
||||
} else {
|
||||
DetailLine(strings.listItem, position.itemIndex.toString())
|
||||
DetailLine(strings.scrollOffset, "${position.scrollOffset}px")
|
||||
DetailLine("List item", position.itemIndex.toString())
|
||||
DetailLine("Scroll offset", "${position.scrollOffset}px")
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
InfoSection(strings.bookmarks) {
|
||||
InfoSection("Bookmarks") {
|
||||
val bookmarks = extras?.bookmarks.orEmpty()
|
||||
if (extras == null) {
|
||||
Text(strings.loading, style = MaterialTheme.typography.bodyMedium)
|
||||
Text("Loading...", style = MaterialTheme.typography.bodyMedium)
|
||||
} else if (bookmarks.isEmpty()) {
|
||||
Text(strings.noBookmarks, style = MaterialTheme.typography.bodyMedium)
|
||||
Text("No bookmarks", style = MaterialTheme.typography.bodyMedium)
|
||||
} else {
|
||||
bookmarks.forEach { bookmark ->
|
||||
BookmarkInfoLine(bookmark)
|
||||
@ -118,12 +118,12 @@ internal fun BookInfoScreen(
|
||||
}
|
||||
}
|
||||
item {
|
||||
InfoSection(strings.notes) {
|
||||
InfoSection("Notes") {
|
||||
val notes = extras?.notes.orEmpty()
|
||||
if (extras == null) {
|
||||
Text(strings.loading, style = MaterialTheme.typography.bodyMedium)
|
||||
Text("Loading...", style = MaterialTheme.typography.bodyMedium)
|
||||
} else if (notes.isEmpty()) {
|
||||
Text(strings.noNotes, style = MaterialTheme.typography.bodyMedium)
|
||||
Text("No notes", style = MaterialTheme.typography.bodyMedium)
|
||||
} else {
|
||||
notes.forEach { note ->
|
||||
NoteInfoLine(note)
|
||||
@ -148,7 +148,7 @@ private fun InfoSection(title: String, content: @Composable ColumnScope.() -> Un
|
||||
@Composable
|
||||
private fun BookmarkInfoLine(bookmark: BookmarkInfo) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp), modifier = Modifier.fillMaxWidth()) {
|
||||
Text(bookmark.title?.ifBlank { null } ?: strings.bookmark, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.SemiBold)
|
||||
Text(bookmark.title?.ifBlank { null } ?: "Bookmark", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.SemiBold)
|
||||
bookmark.selectedText?.ifBlank { null }?.let {
|
||||
Text(it, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.secondary, maxLines = 3)
|
||||
}
|
||||
|
||||
@ -88,9 +88,9 @@ internal fun ImageViewer(
|
||||
fun copyImage() {
|
||||
scope.launch {
|
||||
val message = if (copyImageToClipboard(image.bytes, image.mimeType, image.title)) {
|
||||
strings.imageCopied
|
||||
"Image copied"
|
||||
} else {
|
||||
strings.copyFailed
|
||||
"Copy failed"
|
||||
}
|
||||
snackbarHostState.showSnackbar(message, duration = SnackbarDuration.Short)
|
||||
}
|
||||
@ -110,7 +110,7 @@ internal fun ImageViewer(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = strings.back)
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
Text(
|
||||
image.title,
|
||||
@ -119,15 +119,15 @@ internal fun ImageViewer(
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
IconButton(onClick = { setScale(scale * 1.25f) }) {
|
||||
Icon(Icons.Filled.ZoomIn, contentDescription = strings.zoomIn)
|
||||
Icon(Icons.Filled.ZoomIn, contentDescription = "Zoom in")
|
||||
}
|
||||
IconButton(onClick = { setScale(scale / 1.25f) }, enabled = scale > MinImageScale) {
|
||||
Icon(Icons.Filled.ZoomOut, contentDescription = strings.zoomOut)
|
||||
Icon(Icons.Filled.ZoomOut, contentDescription = "Zoom out")
|
||||
}
|
||||
IconButton(
|
||||
onClick = ::copyImage,
|
||||
) {
|
||||
Icon(Icons.Filled.ContentCopy, contentDescription = strings.copyImage)
|
||||
Icon(Icons.Filled.ContentCopy, contentDescription = "Copy image")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,6 @@ internal suspend fun deleteLibraryBook(fileId: String, title: String): LibraryDe
|
||||
val deleted = runCatching { deleteLibraryItem(fileId) }.getOrDefault(false)
|
||||
return LibraryDeleteResult(
|
||||
deleted = deleted,
|
||||
message = if (deleted) strings.removed(title) else strings.couldNotRemove(title),
|
||||
message = if (deleted) "Removed $title." else "Could not remove $title.",
|
||||
)
|
||||
}
|
||||
|
||||
@ -145,10 +145,6 @@ 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)
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
package net.sergeych.toread
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
@ -26,7 +24,6 @@ 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
|
||||
@ -57,7 +54,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.input.key.Key
|
||||
@ -67,9 +63,9 @@ import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import net.sergeych.toread.fb2.Fb2Format
|
||||
import net.sergeych.toread.storage.BookReadingStatus
|
||||
@ -93,7 +89,6 @@ internal fun LibraryScreen(
|
||||
) -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val focusManager = LocalFocusManager.current
|
||||
var busy by remember { mutableStateOf(false) }
|
||||
var message by remember(state.message) { mutableStateOf(state.message) }
|
||||
var items by remember(state.items) { mutableStateOf(state.items) }
|
||||
@ -103,24 +98,24 @@ 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) }
|
||||
var searchText by remember { mutableStateOf("") }
|
||||
var searchFocused by remember { mutableStateOf(false) }
|
||||
var searchResults by remember { mutableStateOf<List<LibraryItem>>(emptyList()) }
|
||||
var searching by remember { mutableStateOf(false) }
|
||||
var selectedFilter by remember(state.scanPath) { mutableStateOf(LibraryFilter.ReadingNow) }
|
||||
var filterChosenByUser by remember(state.scanPath) { mutableStateOf(false) }
|
||||
var readingNowCollapsed by remember { mutableStateOf(false) }
|
||||
var favoritesCollapsed by remember { mutableStateOf(false) }
|
||||
var toReadCollapsed by remember { mutableStateOf(false) }
|
||||
var readCollapsed by remember { mutableStateOf(false) }
|
||||
var recentlyAddedCollapsed by remember { mutableStateOf(false) }
|
||||
var myLibraryCollapsed by remember { mutableStateOf(false) }
|
||||
var notInterestedCollapsed by remember { mutableStateOf(false) }
|
||||
val coverCache = remember { mutableStateMapOf<String, LibraryCover?>() }
|
||||
val searchActive = searchText.isNotBlank()
|
||||
val libraryItems = items.filterNot { it.fileId in hiddenFileIds }
|
||||
val visibleSearchResults = searchResults.filterNot { it.fileId in hiddenFileIds }
|
||||
val sourceItems = if (searchActive) visibleSearchResults else libraryItems
|
||||
val recentlyAdded = recentlyAddedItems.filterNot { it.fileId in hiddenFileIds }
|
||||
val visibleItems = selectedFilter.apply(sourceItems, recentlyAdded, searchActive)
|
||||
val canLoadMore = !searchActive && selectedFilter.usesPagedLibrary && !endReached
|
||||
val visibleItems = if (searchActive) visibleSearchResults else libraryItems
|
||||
|
||||
suspend fun loadPage(reset: Boolean = false) {
|
||||
if (loadingPage) return
|
||||
@ -148,7 +143,7 @@ internal fun LibraryScreen(
|
||||
nextOffset = offset + page.size
|
||||
endReached = page.size < limit
|
||||
} catch (t: Throwable) {
|
||||
message = t.message ?: strings.couldNotLoadLibrary
|
||||
message = t.message ?: "Could not load library."
|
||||
endReached = true
|
||||
} finally {
|
||||
loadingPage = false
|
||||
@ -194,7 +189,7 @@ internal fun LibraryScreen(
|
||||
loadRecentlyAdded()
|
||||
}
|
||||
} else {
|
||||
message = strings.couldNotUpdate(item.title)
|
||||
message = "Could not update ${item.title}."
|
||||
}
|
||||
}
|
||||
|
||||
@ -218,23 +213,22 @@ internal fun LibraryScreen(
|
||||
loadRecentlyAdded()
|
||||
}
|
||||
} else {
|
||||
message = strings.couldNotUpdate(item.title)
|
||||
message = "Could not update ${item.title}."
|
||||
}
|
||||
}
|
||||
|
||||
fun rescanAllLibrary() {
|
||||
settingsMenuOpen = false
|
||||
localeMenuOpen = false
|
||||
scope.launch {
|
||||
busy = true
|
||||
message = strings.rescanningLibrary
|
||||
message = "Rescanning library..."
|
||||
try {
|
||||
val report = rescanAllLibraryBooks()
|
||||
refresh(
|
||||
strings.rescanReport(report.scannedFiles, report.updatedFiles, report.failedFiles),
|
||||
"Rescanned ${report.scannedFiles}, updated ${report.updatedFiles}, failed ${report.failedFiles}.",
|
||||
)
|
||||
} catch (t: Throwable) {
|
||||
message = t.message ?: strings.libraryRescanFailed
|
||||
message = t.message ?: "Library rescan failed."
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
@ -247,12 +241,6 @@ internal fun LibraryScreen(
|
||||
searching = false
|
||||
}
|
||||
|
||||
fun closeSearch() {
|
||||
clearSearch()
|
||||
focusManager.clearFocus()
|
||||
searchFocused = false
|
||||
}
|
||||
|
||||
LaunchedEffect(state.scanPath, state.message) {
|
||||
if (items.isEmpty() && !endReached) {
|
||||
loadPage(reset = true)
|
||||
@ -279,26 +267,12 @@ internal fun LibraryScreen(
|
||||
.onFailure {
|
||||
if (searchText == query) {
|
||||
searchResults = emptyList()
|
||||
message = it.message ?: strings.searchFailed
|
||||
message = it.message ?: "Search failed."
|
||||
}
|
||||
}
|
||||
if (searchText == query) searching = false
|
||||
}
|
||||
|
||||
LaunchedEffect(searchFocused) {
|
||||
if (searchFocused) {
|
||||
settingsMenuOpen = false
|
||||
localeMenuOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(searchActive, loadingPage, endReached, libraryItems, recentlyAdded) {
|
||||
val libraryDataLoaded = endReached || libraryItems.isNotEmpty() || recentlyAdded.isNotEmpty()
|
||||
if (!filterChosenByUser && !searchActive && !loadingPage && libraryDataLoaded) {
|
||||
selectedFilter = defaultLibraryFilter(libraryItems, recentlyAdded)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
autoScanDownloads = loadScanDownloadsAutomatically()
|
||||
autoScanSettingLoaded = true
|
||||
@ -316,7 +290,7 @@ internal fun LibraryScreen(
|
||||
if (loadDownloadsWasScanned()) {
|
||||
downloadsScanPath()?.let { path ->
|
||||
backgroundDownloadsRescanStarted = true
|
||||
message = strings.scanningDownloads
|
||||
message = "Scanning Downloads..."
|
||||
onStartScan(path)
|
||||
}
|
||||
}
|
||||
@ -341,45 +315,20 @@ internal fun LibraryScreen(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().animateContentSize(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
AnimatedVisibility(visible = !searchFocused) {
|
||||
LibraryFilterSelect(
|
||||
selected = selectedFilter,
|
||||
onSelected = {
|
||||
filterChosenByUser = true
|
||||
selectedFilter = it
|
||||
},
|
||||
)
|
||||
}
|
||||
LibrarySearchField(
|
||||
value = searchText,
|
||||
focused = searchFocused,
|
||||
onValueChange = { searchText = it },
|
||||
onClear = ::clearSearch,
|
||||
onClose = ::closeSearch,
|
||||
onFocusChanged = { searchFocused = it },
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
LibrarySearchField(
|
||||
value = searchText,
|
||||
onValueChange = { searchText = it },
|
||||
onClear = ::clearSearch,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
colors = themedTopAppBarColors(),
|
||||
actions = {
|
||||
AnimatedVisibility(visible = !searchFocused) {
|
||||
Box {
|
||||
IconButton(onClick = { settingsMenuOpen = true }) {
|
||||
Icon(Icons.Filled.MoreVert, contentDescription = strings.libraryOptions)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = settingsMenuOpen,
|
||||
onDismissRequest = {
|
||||
settingsMenuOpen = false
|
||||
localeMenuOpen = false
|
||||
},
|
||||
) {
|
||||
Box {
|
||||
IconButton(onClick = { settingsMenuOpen = true }) {
|
||||
Icon(Icons.Filled.MoreVert, contentDescription = "Library options")
|
||||
}
|
||||
DropdownMenu(expanded = settingsMenuOpen, onDismissRequest = { settingsMenuOpen = false }) {
|
||||
// DropdownMenuItem(
|
||||
// text = { Text("Rescan all library") },
|
||||
// enabled = !busy && activeScan == null,
|
||||
@ -390,7 +339,7 @@ internal fun LibraryScreen(
|
||||
leadingIcon = {
|
||||
Checkbox(checked = autoScanDownloads, onCheckedChange = null)
|
||||
},
|
||||
text = { Text(strings.scanDownloadsAutomatically) },
|
||||
text = { Text("Scan Downloads automatically") },
|
||||
onClick = {
|
||||
val next = !autoScanDownloads
|
||||
autoScanDownloads = next
|
||||
@ -398,57 +347,6 @@ 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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -456,7 +354,7 @@ internal fun LibraryScreen(
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = onNavigateToScan) {
|
||||
Icon(Icons.Filled.Add, contentDescription = strings.scanFolder)
|
||||
Icon(Icons.Filled.Add, contentDescription = "Scan folder")
|
||||
}
|
||||
},
|
||||
) {
|
||||
@ -465,21 +363,11 @@ internal fun LibraryScreen(
|
||||
.fillMaxSize()
|
||||
.padding(it)
|
||||
.onPreviewKeyEvent { event ->
|
||||
if (event.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false
|
||||
when {
|
||||
event.key == Key.Escape && searchText.isNotBlank() -> {
|
||||
clearSearch()
|
||||
true
|
||||
}
|
||||
event.key == Key.Escape && searchFocused -> {
|
||||
closeSearch()
|
||||
true
|
||||
}
|
||||
event.key.isEnterKey() && searchFocused && searchText.isBlank() -> {
|
||||
closeSearch()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
if (event.type == KeyEventType.KeyDown && event.key == Key.Escape && searchText.isNotBlank()) {
|
||||
clearSearch()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
.background(readerBackground()),
|
||||
@ -489,14 +377,11 @@ internal fun LibraryScreen(
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else if (visibleItems.isEmpty() && !canLoadMore) {
|
||||
} else if (visibleItems.isEmpty()) {
|
||||
if (searchActive) {
|
||||
EmptySearchPane(modifier = Modifier.fillMaxSize().padding(if (wide) 24.dp else 14.dp))
|
||||
} else {
|
||||
EmptyLibraryPane(
|
||||
filter = selectedFilter,
|
||||
modifier = Modifier.fillMaxSize().padding(if (wide) 24.dp else 14.dp),
|
||||
)
|
||||
EmptyLibraryPane(modifier = Modifier.fillMaxSize().padding(if (wide) 24.dp else 14.dp))
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
@ -510,7 +395,7 @@ internal fun LibraryScreen(
|
||||
busy = true
|
||||
try {
|
||||
val next = runCatching {
|
||||
val bytes = openLibraryBook(item.fileId) ?: error(strings.bookFileNotAvailable)
|
||||
val bytes = openLibraryBook(item.fileId) ?: error("Book file is not available.")
|
||||
val book = Fb2Format.parse(bytes, item.storageUri ?: item.title)
|
||||
var readerLibraryItems = visibleItems
|
||||
refreshLibraryItemFromParsedBook(item.fileId, book)?.let { updatedItem ->
|
||||
@ -537,7 +422,7 @@ internal fun LibraryScreen(
|
||||
message = message,
|
||||
)
|
||||
}.getOrElse {
|
||||
AppState.Library(visibleItems, state.scanPath, it.message ?: strings.couldNotOpenBook)
|
||||
AppState.Library(visibleItems, state.scanPath, it.message ?: "Could not open book.")
|
||||
}
|
||||
onStateChange(next)
|
||||
} finally {
|
||||
@ -549,7 +434,7 @@ internal fun LibraryScreen(
|
||||
scope.launch {
|
||||
busy = true
|
||||
try {
|
||||
applyReadingStatus(item, BookReadingStatus.READ, strings.markedAsRead(item.title))
|
||||
applyReadingStatus(item, BookReadingStatus.READ, "Marked ${item.title} as read.")
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
@ -559,7 +444,7 @@ internal fun LibraryScreen(
|
||||
scope.launch {
|
||||
busy = true
|
||||
try {
|
||||
applyReadingStatus(item, BookReadingStatus.NEW, strings.markedAsUnread(item.title))
|
||||
applyReadingStatus(item, BookReadingStatus.NEW, "Marked ${item.title} as unread.")
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
@ -569,7 +454,7 @@ internal fun LibraryScreen(
|
||||
scope.launch {
|
||||
busy = true
|
||||
try {
|
||||
applyReadingStatus(item, BookReadingStatus.NEW, strings.removedMarks(item.title))
|
||||
applyReadingStatus(item, BookReadingStatus.NEW, "Removed marks from ${item.title}.")
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
@ -579,7 +464,7 @@ internal fun LibraryScreen(
|
||||
scope.launch {
|
||||
busy = true
|
||||
try {
|
||||
applyReadingStatus(item, BookReadingStatus.TO_READ, strings.markedToRead(item.title))
|
||||
applyReadingStatus(item, BookReadingStatus.TO_READ, "Marked ${item.title} to read.")
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
@ -589,7 +474,7 @@ internal fun LibraryScreen(
|
||||
scope.launch {
|
||||
busy = true
|
||||
try {
|
||||
applyReadingStatus(item, BookReadingStatus.NOT_INTERESTED, strings.markedNotInterested(item.title))
|
||||
applyReadingStatus(item, BookReadingStatus.NOT_INTERESTED, "Marked ${item.title} as not interested.")
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
@ -599,7 +484,7 @@ internal fun LibraryScreen(
|
||||
scope.launch {
|
||||
busy = true
|
||||
try {
|
||||
val label = if (favorite) strings.addedToFavorites(item.title) else strings.removedFromFavorites(item.title)
|
||||
val label = if (favorite) "Added ${item.title} to favorites." else "Removed ${item.title} from favorites."
|
||||
applyFavorite(item, favorite, label)
|
||||
} finally {
|
||||
busy = false
|
||||
@ -636,8 +521,8 @@ internal fun LibraryScreen(
|
||||
},
|
||||
)
|
||||
|
||||
fun LazyListScope.libraryRows(rowItems: List<LibraryItem>) {
|
||||
itemsIndexed(rowItems, key = { _, item -> item.fileId }) { _, item ->
|
||||
fun LazyListScope.libraryRows(sectionKey: String, sectionItems: List<LibraryItem>) {
|
||||
itemsIndexed(sectionItems, key = { _, item -> "$sectionKey-${item.fileId}" }) { _, item ->
|
||||
LibraryRow(
|
||||
item = item,
|
||||
coverCache = coverCache,
|
||||
@ -647,9 +532,98 @@ internal fun LibraryScreen(
|
||||
}
|
||||
}
|
||||
|
||||
libraryRows(visibleItems)
|
||||
if (searchActive) {
|
||||
libraryRows("search", visibleItems)
|
||||
} else {
|
||||
val readingNow = visibleItems.filter { it.readingStatus == BookReadingStatus.READING }
|
||||
val favorites = visibleItems.filter { it.favorite }
|
||||
val toRead = visibleItems.filter { it.readingStatus == BookReadingStatus.TO_READ }
|
||||
val read = visibleItems.filter { it.readingStatus == BookReadingStatus.READ }
|
||||
val recentlyAdded = recentlyAddedItems.filter {
|
||||
it.fileId !in hiddenFileIds && it.readingStatus == BookReadingStatus.NEW
|
||||
}
|
||||
val recentlyAddedIds = recentlyAdded.mapTo(mutableSetOf()) { it.fileId }
|
||||
val myLibrary = visibleItems.filter {
|
||||
it.fileId !in recentlyAddedIds &&
|
||||
it.readingStatus != BookReadingStatus.READING &&
|
||||
it.readingStatus != BookReadingStatus.TO_READ &&
|
||||
it.readingStatus != BookReadingStatus.READ &&
|
||||
it.readingStatus != BookReadingStatus.NOT_INTERESTED
|
||||
}
|
||||
val notInterested = visibleItems.filter { it.readingStatus == BookReadingStatus.NOT_INTERESTED }
|
||||
|
||||
if (canLoadMore) {
|
||||
librarySection(
|
||||
key = "reading",
|
||||
title = "reading now",
|
||||
itemCount = readingNow.size,
|
||||
displayCount = readingNow.size.takeIf { endReached },
|
||||
collapsed = readingNowCollapsed,
|
||||
onCollapsedChange = { readingNowCollapsed = it },
|
||||
) {
|
||||
libraryRows("reading", readingNow)
|
||||
}
|
||||
librarySection(
|
||||
key = "favorites",
|
||||
title = "favorites",
|
||||
itemCount = favorites.size,
|
||||
displayCount = favorites.size.takeIf { endReached },
|
||||
collapsed = favoritesCollapsed,
|
||||
onCollapsedChange = { favoritesCollapsed = it },
|
||||
) {
|
||||
libraryRows("favorites", favorites)
|
||||
}
|
||||
librarySection(
|
||||
key = "to-read",
|
||||
title = "to read",
|
||||
itemCount = toRead.size,
|
||||
displayCount = toRead.size.takeIf { endReached },
|
||||
collapsed = toReadCollapsed,
|
||||
onCollapsedChange = { toReadCollapsed = it },
|
||||
) {
|
||||
libraryRows("to-read", toRead)
|
||||
}
|
||||
librarySection(
|
||||
key = "recently-added",
|
||||
title = "recently added",
|
||||
itemCount = recentlyAdded.size,
|
||||
displayCount = null,
|
||||
collapsed = recentlyAddedCollapsed,
|
||||
onCollapsedChange = { recentlyAddedCollapsed = it },
|
||||
) {
|
||||
libraryRows("recently-added", recentlyAdded)
|
||||
}
|
||||
librarySection(
|
||||
key = "library",
|
||||
title = "my library",
|
||||
itemCount = myLibrary.size,
|
||||
displayCount = myLibrary.size.takeIf { endReached },
|
||||
collapsed = myLibraryCollapsed,
|
||||
onCollapsedChange = { myLibraryCollapsed = it },
|
||||
) {
|
||||
libraryRows("library", myLibrary)
|
||||
}
|
||||
librarySection(
|
||||
key = "read",
|
||||
title = "read",
|
||||
itemCount = read.size,
|
||||
displayCount = read.size.takeIf { endReached },
|
||||
collapsed = readCollapsed,
|
||||
onCollapsedChange = { readCollapsed = it },
|
||||
) {
|
||||
libraryRows("read", read)
|
||||
}
|
||||
librarySection(
|
||||
key = "not-interested",
|
||||
title = "not interested",
|
||||
itemCount = notInterested.size,
|
||||
displayCount = notInterested.size.takeIf { endReached },
|
||||
collapsed = notInterestedCollapsed,
|
||||
onCollapsedChange = { notInterestedCollapsed = it },
|
||||
) {
|
||||
libraryRows("not-interested", notInterested)
|
||||
}
|
||||
}
|
||||
if (!searchActive && !endReached) {
|
||||
item(key = "load-more") {
|
||||
LaunchedEffect(nextOffset, items.size) {
|
||||
if (!loadingPage) loadPage()
|
||||
@ -676,60 +650,11 @@ internal fun LibraryScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LibraryFilterSelect(
|
||||
selected: LibraryFilter,
|
||||
onSelected: (LibraryFilter) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var open by remember { mutableStateOf(false) }
|
||||
Box(modifier = modifier) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(42.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.clickable { open = true }
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.padding(start = 14.dp, end = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
selected.label,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Icon(
|
||||
Icons.Filled.KeyboardArrowDown,
|
||||
contentDescription = strings.chooseLibraryFilter,
|
||||
tint = MaterialTheme.colorScheme.outline,
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
}
|
||||
DropdownMenu(expanded = open, onDismissRequest = { open = false }) {
|
||||
LibraryFilter.entries.forEach { filter ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(filter.label) },
|
||||
onClick = {
|
||||
open = false
|
||||
onSelected(filter)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LibrarySearchField(
|
||||
value: String,
|
||||
focused: Boolean,
|
||||
onValueChange: (String) -> Unit,
|
||||
onClear: () -> Unit,
|
||||
onClose: () -> Unit,
|
||||
onFocusChanged: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val shape = RoundedCornerShape(16.dp)
|
||||
@ -740,28 +665,18 @@ private fun LibrarySearchField(
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.padding(horizontal = 12.dp)
|
||||
.onPreviewKeyEvent { event ->
|
||||
if (event.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false
|
||||
when {
|
||||
event.key == Key.Escape && value.isNotBlank() -> {
|
||||
onClear()
|
||||
true
|
||||
}
|
||||
event.key == Key.Escape && focused -> {
|
||||
onClose()
|
||||
true
|
||||
}
|
||||
event.key.isEnterKey() && focused && value.isBlank() -> {
|
||||
onClose()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
if (event.type == KeyEventType.KeyDown && event.key == Key.Escape && value.isNotBlank()) {
|
||||
onClear()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Search,
|
||||
contentDescription = strings.searchLibrary,
|
||||
contentDescription = "Search library",
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = MaterialTheme.colorScheme.outline,
|
||||
)
|
||||
@ -777,43 +692,32 @@ private fun LibrarySearchField(
|
||||
singleLine = true,
|
||||
textStyle = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onFocusChanged { onFocusChanged(it.isFocused) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
if (value.isBlank()) {
|
||||
Text(
|
||||
strings.searchLibrary,
|
||||
"Search library",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.72f),
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (value.isNotBlank() || focused) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (value.isBlank()) onClose() else onClear()
|
||||
},
|
||||
modifier = Modifier.size(30.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Close,
|
||||
contentDescription = if (value.isBlank()) strings.closeSearch else strings.clearSearch,
|
||||
modifier = Modifier.size(17.dp),
|
||||
)
|
||||
if (value.isNotBlank()) {
|
||||
IconButton(onClick = onClear, modifier = Modifier.size(30.dp)) {
|
||||
Icon(Icons.Filled.Close, contentDescription = "Clear search", modifier = Modifier.size(17.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyLibraryPane(filter: LibraryFilter, modifier: Modifier = Modifier) {
|
||||
private fun EmptyLibraryPane(modifier: Modifier = Modifier) {
|
||||
Box(modifier, contentAlignment = Alignment.Center) {
|
||||
Card(shape = RoundedCornerShape(8.dp), colors = quietCardColors()) {
|
||||
Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(strings.noBooksIn(filter.label), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||
Text(strings.scanFolderOrChooseFilter, style = MaterialTheme.typography.bodyMedium)
|
||||
Text("No books indexed", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||
Text("Scan a folder containing FB2 or FB2.ZIP files.", style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -822,7 +726,60 @@ private fun EmptyLibraryPane(filter: LibraryFilter, modifier: Modifier = Modifie
|
||||
@Composable
|
||||
private fun EmptySearchPane(modifier: Modifier = Modifier) {
|
||||
Box(modifier, contentAlignment = Alignment.Center) {
|
||||
Text(strings.noMatches, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.outline)
|
||||
Text("No matches", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.outline)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.librarySection(
|
||||
key: String,
|
||||
title: String,
|
||||
itemCount: Int,
|
||||
displayCount: Int?,
|
||||
collapsed: Boolean,
|
||||
onCollapsedChange: (Boolean) -> Unit,
|
||||
content: LazyListScope.() -> Unit,
|
||||
) {
|
||||
if (itemCount == 0) return
|
||||
item(key = "section-$key") {
|
||||
LibrarySectionHeader(
|
||||
text = title,
|
||||
count = displayCount,
|
||||
collapsed = collapsed,
|
||||
onToggle = { onCollapsedChange(!collapsed) },
|
||||
)
|
||||
}
|
||||
if (!collapsed) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LibrarySectionHeader(
|
||||
text: String,
|
||||
count: Int?,
|
||||
collapsed: Boolean,
|
||||
onToggle: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onToggle)
|
||||
.padding(top = 10.dp, bottom = 4.dp, start = 8.dp, end = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Icon(
|
||||
if (collapsed) Icons.AutoMirrored.Filled.KeyboardArrowRight else Icons.Filled.KeyboardArrowDown,
|
||||
contentDescription = if (collapsed) "Expand $text" else "Collapse $text",
|
||||
tint = MaterialTheme.colorScheme.outline,
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
Text(
|
||||
if (count == null) text else "$text ($count)",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -847,7 +804,7 @@ private fun LibraryScanStatusPanel(progress: LibraryScanProgress, modifier: Modi
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Text(
|
||||
strings.importedSkippedFailed(progress.importedFiles, progress.skippedFiles, progress.failedFiles),
|
||||
"Imported ${progress.importedFiles}, skipped ${progress.skippedFiles}, failed ${progress.failedFiles}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
)
|
||||
@ -893,14 +850,14 @@ private fun LibraryRow(
|
||||
if (item.favorite) {
|
||||
Icon(
|
||||
Icons.Filled.Favorite,
|
||||
contentDescription = strings.favorite,
|
||||
contentDescription = "Favorite",
|
||||
tint = Color(0xFFD32F2F),
|
||||
modifier = Modifier.size(favoriteIconSize),
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
item.authors.joinToString().ifBlank { strings.unknownAuthor },
|
||||
item.authors.joinToString().ifBlank { "Unknown author" },
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
maxLines = 1,
|
||||
@ -915,11 +872,11 @@ private fun LibraryRow(
|
||||
}
|
||||
Box {
|
||||
IconButton(onClick = { menuOpen = true }, enabled = enabled) {
|
||||
Icon(Icons.Filled.MoreVert, contentDescription = strings.bookMenuFor(item.title))
|
||||
Icon(Icons.Filled.MoreVert, contentDescription = "Book menu for ${item.title}")
|
||||
}
|
||||
DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(strings.open) },
|
||||
text = { Text("Open") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
actions.onOpen()
|
||||
@ -928,7 +885,7 @@ private fun LibraryRow(
|
||||
HorizontalDivider()
|
||||
if (item.readingStatus != BookReadingStatus.READ) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(strings.markAsRead) },
|
||||
text = { Text("Mark as read") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
actions.onMarkAsRead()
|
||||
@ -937,7 +894,7 @@ private fun LibraryRow(
|
||||
}
|
||||
if (item.readingStatus == BookReadingStatus.READ) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(strings.markAsUnread) },
|
||||
text = { Text("Mark as unread") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
actions.onMarkAsUnread()
|
||||
@ -946,7 +903,7 @@ private fun LibraryRow(
|
||||
}
|
||||
if (item.readingStatus != BookReadingStatus.TO_READ) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(strings.markToRead) },
|
||||
text = { Text("Mark to read") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
actions.onMarkToRead()
|
||||
@ -955,7 +912,7 @@ private fun LibraryRow(
|
||||
}
|
||||
if (item.readingStatus != BookReadingStatus.NEW) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(if (item.readingStatus == BookReadingStatus.TO_READ) strings.removeToRead else strings.removeMarks) },
|
||||
text = { Text(if (item.readingStatus == BookReadingStatus.TO_READ) "Remove to read" else "Remove marks") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
actions.onRemoveMarks()
|
||||
@ -963,14 +920,14 @@ private fun LibraryRow(
|
||||
)
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text(if (item.favorite) strings.removeFavorite else strings.addFavorite) },
|
||||
text = { Text(if (item.favorite) "Remove favorite" else "Add favorite") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
actions.onFavoriteChange(!item.favorite)
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(strings.notInterested) },
|
||||
text = { Text("Not interested") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
actions.onNotInterested()
|
||||
@ -978,7 +935,7 @@ private fun LibraryRow(
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text(strings.delete) },
|
||||
text = { Text("Delete") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
actions.onDelete()
|
||||
@ -1001,67 +958,6 @@ private data class LibraryItemActions(
|
||||
val onDelete: () -> Unit,
|
||||
)
|
||||
|
||||
private enum class LibraryFilter(val usesPagedLibrary: Boolean = true) {
|
||||
ReadingNow,
|
||||
RecentlyAdded(usesPagedLibrary = false),
|
||||
MyLibrary,
|
||||
ToRead,
|
||||
Favorites,
|
||||
Read,
|
||||
NotInterested,
|
||||
;
|
||||
|
||||
fun apply(
|
||||
sourceItems: List<LibraryItem>,
|
||||
recentlyAddedItems: List<LibraryItem>,
|
||||
searchActive: Boolean,
|
||||
): List<LibraryItem> {
|
||||
val recentlyAddedIds = recentlyAddedItems
|
||||
.filter { it.readingStatus == BookReadingStatus.NEW }
|
||||
.mapTo(mutableSetOf()) { it.fileId }
|
||||
return when (this) {
|
||||
ReadingNow -> sourceItems.filter { it.readingStatus == BookReadingStatus.READING }
|
||||
RecentlyAdded -> if (searchActive) {
|
||||
sourceItems.filter { it.fileId in recentlyAddedIds && it.readingStatus == BookReadingStatus.NEW }
|
||||
} else {
|
||||
recentlyAddedItems.filter { it.readingStatus == BookReadingStatus.NEW }
|
||||
}
|
||||
MyLibrary -> sourceItems.filter {
|
||||
it.fileId !in recentlyAddedIds &&
|
||||
it.readingStatus != BookReadingStatus.READING &&
|
||||
it.readingStatus != BookReadingStatus.TO_READ &&
|
||||
it.readingStatus != BookReadingStatus.READ &&
|
||||
it.readingStatus != BookReadingStatus.NOT_INTERESTED
|
||||
}
|
||||
ToRead -> sourceItems.filter { it.readingStatus == BookReadingStatus.TO_READ }
|
||||
Favorites -> sourceItems.filter { it.favorite }
|
||||
Read -> sourceItems.filter { it.readingStatus == BookReadingStatus.READ }
|
||||
NotInterested -> sourceItems.filter { it.readingStatus == BookReadingStatus.NOT_INTERESTED }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val LibraryFilter.label: String
|
||||
get() = when (this) {
|
||||
LibraryFilter.ReadingNow -> strings.filterReadingNow
|
||||
LibraryFilter.RecentlyAdded -> strings.filterRecentlyAdded
|
||||
LibraryFilter.MyLibrary -> strings.filterMyLibrary
|
||||
LibraryFilter.ToRead -> strings.filterToRead
|
||||
LibraryFilter.Favorites -> strings.favorite
|
||||
LibraryFilter.Read -> strings.filterRead
|
||||
LibraryFilter.NotInterested -> strings.notInterested
|
||||
}
|
||||
|
||||
private fun defaultLibraryFilter(
|
||||
libraryItems: List<LibraryItem>,
|
||||
recentlyAddedItems: List<LibraryItem>,
|
||||
): LibraryFilter =
|
||||
when {
|
||||
libraryItems.any { it.readingStatus == BookReadingStatus.READING } -> LibraryFilter.ReadingNow
|
||||
recentlyAddedItems.any { it.readingStatus == BookReadingStatus.NEW } -> LibraryFilter.RecentlyAdded
|
||||
else -> LibraryFilter.MyLibrary
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LibraryCover(
|
||||
item: LibraryItem,
|
||||
@ -1102,16 +998,25 @@ private fun LibraryCover(
|
||||
|
||||
private fun LibraryItem.libraryMetadataLine(): String =
|
||||
listOfNotNull(
|
||||
strings.favorite.takeIf { favorite },
|
||||
strings.readingStatus(readingStatus),
|
||||
"Favorite".takeIf { favorite },
|
||||
readingStatus.displayLabel,
|
||||
lastReadAt?.formatLastRead(),
|
||||
date?.yearOrRaw(),
|
||||
language?.uppercase(),
|
||||
format?.uppercase(),
|
||||
sizeBytes?.formatBytes(),
|
||||
).joinToString(" | ").ifBlank { strings.noMetadata }
|
||||
).joinToString(" | ").ifBlank { "No metadata" }
|
||||
|
||||
private fun Long.formatLastRead(): String = "${strings.lastReadPrefix} ${formatLibraryLastReadTime(this)}"
|
||||
private val BookReadingStatus.displayLabel: String
|
||||
get() = when (this) {
|
||||
BookReadingStatus.NEW -> "New"
|
||||
BookReadingStatus.TO_READ -> "To read"
|
||||
BookReadingStatus.READING -> "Reading"
|
||||
BookReadingStatus.READ -> "Read"
|
||||
BookReadingStatus.NOT_INTERESTED -> "Not interested"
|
||||
}
|
||||
|
||||
private fun Long.formatLastRead(): String = "Last read ${formatLibraryLastReadTime(this)}"
|
||||
|
||||
private fun String.yearOrRaw(): String =
|
||||
Regex("""\d{4}""").find(this)?.value ?: this
|
||||
@ -1126,16 +1031,14 @@ private fun Long.formatBytes(): String =
|
||||
private fun List<LibraryItem>.replaceLibraryItem(item: LibraryItem): List<LibraryItem> =
|
||||
map { current -> if (current.fileId == item.fileId) item else current }
|
||||
|
||||
private fun Key.isEnterKey(): Boolean = this == Key.Enter || this == Key.NumPadEnter
|
||||
|
||||
private fun LibraryScanProgress.toCatalogScanMessage(): String {
|
||||
val total = totalFiles ?: return strings.scannedBooks(scannedFiles)
|
||||
val total = totalFiles ?: return "Scanned $scannedFiles books"
|
||||
val percent = if (total <= 0) {
|
||||
100
|
||||
} else {
|
||||
((scannedFiles.toDouble() / total.toDouble()) * 100.0).roundToInt().coerceIn(0, 100)
|
||||
}
|
||||
return strings.scannedProgress(scannedFiles, total, percent)
|
||||
return "Scanned $scannedFiles of $total, $percent% done"
|
||||
}
|
||||
|
||||
private const val LibraryPageSize: Int = 50
|
||||
|
||||
@ -1,429 +0,0 @@
|
||||
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(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 (effectiveAppLocaleTags().any { it.isRussianLanguageTag() }) {
|
||||
AppLanguage.Russian
|
||||
} else {
|
||||
AppLanguage.English
|
||||
}
|
||||
|
||||
internal val strings: AppStrings
|
||||
get() = when (appLanguage) {
|
||||
AppLanguage.English -> EnglishStrings
|
||||
AppLanguage.Russian -> RussianStrings
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
internal open class AppStrings {
|
||||
open val appName = "To Read"
|
||||
open val loadingOpeningBook = "Opening book"
|
||||
open val scanning = "Scanning..."
|
||||
open val scanningDownloads = "Scanning Downloads..."
|
||||
open val undo = "Undo"
|
||||
open val retry = "Retry"
|
||||
open val done = "Done"
|
||||
|
||||
open val libraryError = "Library error"
|
||||
open val couldNotOpenLibrary = "Could not open library."
|
||||
open val couldNotLoadLibrary = "Could not load library."
|
||||
open val couldNotOpenBook = "Could not open book."
|
||||
open val couldNotUpdateBook = "Could not update book."
|
||||
open val bookFileNotAvailable = "Book file is not available."
|
||||
open val scanFailed = "Scan failed."
|
||||
open val searchFailed = "Search failed."
|
||||
open val libraryRescanFailed = "Library rescan failed."
|
||||
open val rescanningLibrary = "Rescanning library..."
|
||||
|
||||
open val theme = "Theme"
|
||||
open val themeLight = "Light"
|
||||
open val themeDark = "Dark"
|
||||
open val themeSystem = "System"
|
||||
|
||||
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"
|
||||
open val closeSearch = "Close search"
|
||||
open val clearSearch = "Clear search"
|
||||
open val noMatches = "No matches"
|
||||
open val scanFolderOrChooseFilter = "Scan a folder or choose another library filter."
|
||||
open val favorite = "Favorite"
|
||||
open val unknownAuthor = "Unknown author"
|
||||
open val noMetadata = "No metadata"
|
||||
open val lastReadPrefix = "Last read"
|
||||
open val open = "Open"
|
||||
open val markAsRead = "Mark as read"
|
||||
open val markAsUnread = "Mark as unread"
|
||||
open val markToRead = "Mark to read"
|
||||
open val removeToRead = "Remove to read"
|
||||
open val removeMarks = "Remove marks"
|
||||
open val notInterested = "Not interested"
|
||||
open val clearMarks = "Clear marks"
|
||||
open val addFavorite = "Add favorite"
|
||||
open val removeFavorite = "Remove favorite"
|
||||
open val delete = "Delete"
|
||||
|
||||
open val filterReadingNow = "Reading now"
|
||||
open val filterRecentlyAdded = "Recently added"
|
||||
open val filterMyLibrary = "My library"
|
||||
open val filterToRead = "To read"
|
||||
open val filterRead = "Read"
|
||||
|
||||
open val scan = "Scan"
|
||||
open val rootFolder = "Root folder"
|
||||
open val choose = "Choose"
|
||||
open val logPrefix = "Log"
|
||||
|
||||
open val bookInfo = "Book info"
|
||||
open val back = "Back"
|
||||
open val backToLibrary = "Back to library"
|
||||
open val backToReader = "Back to reader"
|
||||
open val titleInfo = "Title Info"
|
||||
open val title = "Title"
|
||||
open val authors = "Authors"
|
||||
open val language = "Language"
|
||||
open val date = "Date"
|
||||
open val genres = "Genres"
|
||||
open val source = "Source"
|
||||
open val translator = "Translator"
|
||||
open val notSpecified = "Not specified"
|
||||
open val statistics = "Statistics"
|
||||
open val words = "Words"
|
||||
open val sections = "Sections"
|
||||
open val images = "Images"
|
||||
open val lastReadingPosition = "Last Reading Position"
|
||||
open val noSavedPosition = "No saved position"
|
||||
open val listItem = "List item"
|
||||
open val scrollOffset = "Scroll offset"
|
||||
open val bookmarks = "Bookmarks"
|
||||
open val bookmark = "Bookmark"
|
||||
open val noBookmarks = "No bookmarks"
|
||||
open val notes = "Notes"
|
||||
open val noNotes = "No notes"
|
||||
open val loading = "Loading..."
|
||||
open val noImage = "No image"
|
||||
|
||||
open val readerTheme = "Theme"
|
||||
open val readAloud = "Read aloud"
|
||||
open val readerMenu = "Book reader menu"
|
||||
open val info = "Info..."
|
||||
open val share = "Share"
|
||||
open val viewFile = "View file"
|
||||
open val previousSentence = "Previous sentence"
|
||||
open val stopReading = "Stop reading"
|
||||
open val startReading = "Start reading"
|
||||
open val nextSentence = "Next sentence"
|
||||
open val readAloudSettings = "Read aloud settings"
|
||||
open val ttsEngine = "TTS engine"
|
||||
open val systemDefault = "System default"
|
||||
open val offlineVoice = "Offline voice"
|
||||
open val autoRussian = "Auto Russian"
|
||||
open val loadingVoices = "Loading voices..."
|
||||
|
||||
open val imageCopied = "Image copied"
|
||||
open val copyFailed = "Copy failed"
|
||||
open val zoomIn = "Zoom in"
|
||||
open val zoomOut = "Zoom out"
|
||||
open val copyImage = "Copy image"
|
||||
|
||||
open val chooseLibraryFolder = "Choose library folder"
|
||||
open val downloads = "Downloads"
|
||||
open val otherFolder = "Other folder..."
|
||||
open val play = "Play"
|
||||
open val stop = "Stop"
|
||||
open val previous = "Previous"
|
||||
open val next = "Next"
|
||||
open val readingInBackground = "Reading in the background"
|
||||
open val shareBook = "Share book"
|
||||
open val couldNotLoadAndroidTtsEngines = "Could not load Android TTS engines."
|
||||
open val couldNotLoadSelectedTtsVoices = "Could not load voices for the selected TTS engine."
|
||||
open val noOfflineVoices = "No offline voices reported by the selected TTS engine."
|
||||
|
||||
open fun themeChanged(mode: ThemeMode): String = "$theme: ${mode.localizedName()}"
|
||||
open fun removed(title: String): String = "Removed $title."
|
||||
open fun restored(title: String): String = "Restored $title."
|
||||
open fun couldNotRemove(title: String): String = "Could not remove $title."
|
||||
open fun couldNotOpen(displayName: String): String = "Could not open $displayName."
|
||||
open fun couldNotReopenLastBook(): String = "Could not reopen last book."
|
||||
open fun scanReport(scanned: Int, imported: Int, skipped: Int, failed: Int): String =
|
||||
"Scanned $scanned, imported $imported, skipped $skipped, failed $failed."
|
||||
open fun rescanReport(scanned: Int, updated: Int, failed: Int): String =
|
||||
"Rescanned $scanned, updated $updated, failed $failed."
|
||||
open fun checkingFile(currentFile: String?, scanned: Int, imported: Int, skipped: Int, failed: Int): String =
|
||||
buildString {
|
||||
append("Checking")
|
||||
currentFile?.takeIf(String::isNotBlank)?.let { append(" $it") }
|
||||
append(". Found $scanned supported files")
|
||||
append(", imported $imported")
|
||||
append(", skipped $skipped")
|
||||
append(", failed $failed.")
|
||||
}
|
||||
open fun importedSkippedFailed(imported: Int, skipped: Int, failed: Int): String =
|
||||
"Imported $imported, skipped $skipped, failed $failed"
|
||||
open fun scannedBooks(scanned: Int): String = "Scanned $scanned books"
|
||||
open fun scannedProgress(scanned: Int, total: Int, percent: Int): String =
|
||||
"Scanned $scanned of $total, $percent% done"
|
||||
open fun noBooksIn(filterLabel: String): String = "No books in ${filterLabel.lowercase()}"
|
||||
open fun bookMenuFor(title: String): String = "Book menu for $title"
|
||||
open fun markedAsRead(title: String? = null): String = title?.let { "Marked $it as read." } ?: "Marked as read."
|
||||
open fun markedAsUnread(title: String): String = "Marked $title as unread."
|
||||
open fun markedToRead(title: String? = null): String = title?.let { "Marked $it to read." } ?: "Marked to read."
|
||||
open fun markedNotInterested(title: String? = null): String =
|
||||
title?.let { "Marked $it as not interested." } ?: "Marked as not interested."
|
||||
open fun removedMarks(title: String? = null): String = title?.let { "Removed marks from $it." } ?: "Cleared marks."
|
||||
open fun addedToFavorites(title: String? = null): String =
|
||||
title?.let { "Added $it to favorites." } ?: "Added to favorites."
|
||||
open fun removedFromFavorites(title: String? = null): String =
|
||||
title?.let { "Removed $it from favorites." } ?: "Removed from favorites."
|
||||
open fun couldNotUpdate(title: String): String = "Could not update $title."
|
||||
open fun shareOpened(): String = "Share opened."
|
||||
open fun couldNotShareBook(): String = "Could not share book."
|
||||
open fun openedFileLocation(): String = "Opened file location."
|
||||
open fun couldNotOpenFileLocation(): String = "Could not open file location."
|
||||
open fun allFilesAccessRequired(path: String): String = "All files access is required to scan $path."
|
||||
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) {
|
||||
BookReadingStatus.NEW -> "New"
|
||||
BookReadingStatus.TO_READ -> "To read"
|
||||
BookReadingStatus.READING -> "Reading"
|
||||
BookReadingStatus.READ -> "Read"
|
||||
BookReadingStatus.NOT_INTERESTED -> "Not interested"
|
||||
}
|
||||
}
|
||||
|
||||
internal object EnglishStrings : AppStrings()
|
||||
|
||||
internal object RussianStrings : AppStrings() {
|
||||
override val appName = "Читать"
|
||||
override val loadingOpeningBook = "Открываем книгу"
|
||||
override val scanning = "Сканируем..."
|
||||
override val scanningDownloads = "Сканируем Загрузки..."
|
||||
override val undo = "Отменить"
|
||||
override val retry = "Повторить"
|
||||
override val done = "Готово"
|
||||
|
||||
override val libraryError = "Ошибка библиотеки"
|
||||
override val couldNotOpenLibrary = "Не удалось открыть библиотеку."
|
||||
override val couldNotLoadLibrary = "Не удалось загрузить библиотеку."
|
||||
override val couldNotOpenBook = "Не удалось открыть книгу."
|
||||
override val couldNotUpdateBook = "Не удалось обновить книгу."
|
||||
override val bookFileNotAvailable = "Файл книги недоступен."
|
||||
override val scanFailed = "Сканирование не удалось."
|
||||
override val searchFailed = "Поиск не удался."
|
||||
override val libraryRescanFailed = "Повторное сканирование библиотеки не удалось."
|
||||
override val rescanningLibrary = "Пересканируем библиотеку..."
|
||||
|
||||
override val theme = "Тема"
|
||||
override val themeLight = "светлая"
|
||||
override val themeDark = "темная"
|
||||
override val themeSystem = "системная"
|
||||
|
||||
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 = "Поиск"
|
||||
override val closeSearch = "Закрыть поиск"
|
||||
override val clearSearch = "Очистить поиск"
|
||||
override val noMatches = "Ничего не найдено"
|
||||
override val scanFolderOrChooseFilter = "Просканируйте папку или выберите другой фильтр библиотеки."
|
||||
override val favorite = "Избранное"
|
||||
override val unknownAuthor = "Автор неизвестен"
|
||||
override val noMetadata = "Нет метаданных"
|
||||
override val lastReadPrefix = "Читали"
|
||||
override val open = "Открыть"
|
||||
override val markAsRead = "Отметить прочитанной"
|
||||
override val markAsUnread = "Отметить непрочитанной"
|
||||
override val markToRead = "Отложить к чтению"
|
||||
override val removeToRead = "Убрать из отложенного"
|
||||
override val removeMarks = "Снять отметки"
|
||||
override val notInterested = "Не интересно"
|
||||
override val clearMarks = "Очистить отметки"
|
||||
override val addFavorite = "Добавить в избранное"
|
||||
override val removeFavorite = "Убрать из избранного"
|
||||
override val delete = "Удалить"
|
||||
|
||||
override val filterReadingNow = "Сейчас читаю"
|
||||
override val filterRecentlyAdded = "Недавно добавленные"
|
||||
override val filterMyLibrary = "Моя библиотека"
|
||||
override val filterToRead = "К чтению"
|
||||
override val filterRead = "Прочитанные"
|
||||
|
||||
override val scan = "Сканирование"
|
||||
override val rootFolder = "Корневая папка"
|
||||
override val choose = "Выбрать"
|
||||
override val logPrefix = "Лог"
|
||||
|
||||
override val bookInfo = "Информация о книге"
|
||||
override val back = "Назад"
|
||||
override val backToLibrary = "Вернуться в библиотеку"
|
||||
override val backToReader = "Вернуться к чтению"
|
||||
override val titleInfo = "Описание"
|
||||
override val title = "Название"
|
||||
override val authors = "Авторы"
|
||||
override val language = "Язык"
|
||||
override val date = "Дата"
|
||||
override val genres = "Жанры"
|
||||
override val source = "Исходный язык"
|
||||
override val translator = "Переводчик"
|
||||
override val notSpecified = "Не указано"
|
||||
override val statistics = "Статистика"
|
||||
override val words = "Слова"
|
||||
override val sections = "Разделы"
|
||||
override val images = "Иллюстрации"
|
||||
override val lastReadingPosition = "Последняя позиция чтения"
|
||||
override val noSavedPosition = "Позиция не сохранена"
|
||||
override val listItem = "Элемент списка"
|
||||
override val scrollOffset = "Смещение прокрутки"
|
||||
override val bookmarks = "Закладки"
|
||||
override val bookmark = "Закладка"
|
||||
override val noBookmarks = "Закладок нет"
|
||||
override val notes = "Заметки"
|
||||
override val noNotes = "Заметок нет"
|
||||
override val loading = "Загрузка..."
|
||||
override val noImage = "Нет изображения"
|
||||
|
||||
override val readerTheme = "Тема"
|
||||
override val readAloud = "Читать вслух"
|
||||
override val readerMenu = "Меню чтения"
|
||||
override val info = "Информация..."
|
||||
override val share = "Поделиться"
|
||||
override val viewFile = "Показать файл"
|
||||
override val previousSentence = "Предыдущее предложение"
|
||||
override val stopReading = "Остановить чтение"
|
||||
override val startReading = "Начать чтение"
|
||||
override val nextSentence = "Следующее предложение"
|
||||
override val readAloudSettings = "Настройки чтения вслух"
|
||||
override val ttsEngine = "Движок TTS"
|
||||
override val systemDefault = "Системный по умолчанию"
|
||||
override val offlineVoice = "Офлайн-голос"
|
||||
override val autoRussian = "Авто: русский"
|
||||
override val loadingVoices = "Загружаем голоса..."
|
||||
|
||||
override val imageCopied = "Изображение скопировано"
|
||||
override val copyFailed = "Не удалось скопировать"
|
||||
override val zoomIn = "Увеличить"
|
||||
override val zoomOut = "Уменьшить"
|
||||
override val copyImage = "Скопировать изображение"
|
||||
|
||||
override val chooseLibraryFolder = "Выберите папку библиотеки"
|
||||
override val downloads = "Загрузки"
|
||||
override val otherFolder = "Другая папка..."
|
||||
override val play = "Пуск"
|
||||
override val stop = "Стоп"
|
||||
override val previous = "Назад"
|
||||
override val next = "Вперед"
|
||||
override val readingInBackground = "Чтение в фоне"
|
||||
override val shareBook = "Поделиться книгой"
|
||||
override val couldNotLoadAndroidTtsEngines = "Не удалось загрузить Android TTS-движки."
|
||||
override val couldNotLoadSelectedTtsVoices = "Не удалось загрузить голоса для выбранного TTS-движка."
|
||||
override val noOfflineVoices = "Выбранный TTS-движок не сообщил об офлайн-голосах."
|
||||
|
||||
override fun themeChanged(mode: ThemeMode): String = "$theme: ${mode.localizedName()}"
|
||||
override fun removed(title: String): String = "Удалено: $title."
|
||||
override fun restored(title: String): String = "Восстановлено: $title."
|
||||
override fun couldNotRemove(title: String): String = "Не удалось удалить: $title."
|
||||
override fun couldNotOpen(displayName: String): String = "Не удалось открыть $displayName."
|
||||
override fun couldNotReopenLastBook(): String = "Не удалось открыть последнюю книгу."
|
||||
override fun scanReport(scanned: Int, imported: Int, skipped: Int, failed: Int): String =
|
||||
"Проверено: $scanned, импортировано: $imported, пропущено: $skipped, ошибок: $failed."
|
||||
override fun rescanReport(scanned: Int, updated: Int, failed: Int): String =
|
||||
"Пересканировано: $scanned, обновлено: $updated, ошибок: $failed."
|
||||
override fun checkingFile(currentFile: String?, scanned: Int, imported: Int, skipped: Int, failed: Int): String =
|
||||
buildString {
|
||||
append("Проверяем")
|
||||
currentFile?.takeIf(String::isNotBlank)?.let { append(" $it") }
|
||||
append(". Найдено поддерживаемых файлов: $scanned")
|
||||
append(", импортировано: $imported")
|
||||
append(", пропущено: $skipped")
|
||||
append(", ошибок: $failed.")
|
||||
}
|
||||
override fun importedSkippedFailed(imported: Int, skipped: Int, failed: Int): String =
|
||||
"Импортировано: $imported, пропущено: $skipped, ошибок: $failed"
|
||||
override fun scannedBooks(scanned: Int): String = "Просканировано книг: $scanned"
|
||||
override fun scannedProgress(scanned: Int, total: Int, percent: Int): String =
|
||||
"Просканировано $scanned из $total, готово $percent%"
|
||||
override fun noBooksIn(filterLabel: String): String = "Нет книг: ${filterLabel.lowercase()}"
|
||||
override fun bookMenuFor(title: String): String = "Меню книги: $title"
|
||||
override fun markedAsRead(title: String?): String =
|
||||
title?.let { "Отмечено как прочитанное: $it." } ?: "Отмечено как прочитанное."
|
||||
override fun markedAsUnread(title: String): String = "Отмечено как непрочитанное: $title."
|
||||
override fun markedToRead(title: String?): String =
|
||||
title?.let { "Отложено к чтению: $it." } ?: "Отложено к чтению."
|
||||
override fun markedNotInterested(title: String?): String =
|
||||
title?.let { "Отмечено как неинтересное: $it." } ?: "Отмечено как неинтересное."
|
||||
override fun removedMarks(title: String?): String =
|
||||
title?.let { "Сняты отметки: $it." } ?: "Отметки сняты."
|
||||
override fun addedToFavorites(title: String?): String =
|
||||
title?.let { "Добавлено в избранное: $it." } ?: "Добавлено в избранное."
|
||||
override fun removedFromFavorites(title: String?): String =
|
||||
title?.let { "Убрано из избранного: $it." } ?: "Убрано из избранного."
|
||||
override fun couldNotUpdate(title: String): String = "Не удалось обновить: $title."
|
||||
override fun shareOpened(): String = "Открыто окно отправки."
|
||||
override fun couldNotShareBook(): String = "Не удалось поделиться книгой."
|
||||
override fun openedFileLocation(): String = "Расположение файла открыто."
|
||||
override fun couldNotOpenFileLocation(): String = "Не удалось открыть расположение файла."
|
||||
override fun allFilesAccessRequired(path: String): String = "Для сканирования $path нужен доступ ко всем файлам."
|
||||
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) {
|
||||
BookReadingStatus.NEW -> "Новая"
|
||||
BookReadingStatus.TO_READ -> "К чтению"
|
||||
BookReadingStatus.READING -> "Читаю"
|
||||
BookReadingStatus.READ -> "Прочитана"
|
||||
BookReadingStatus.NOT_INTERESTED -> "Не интересно"
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ThemeMode.localizedName(): String =
|
||||
when (this) {
|
||||
ThemeMode.LIGHT -> strings.themeLight
|
||||
ThemeMode.DARK -> strings.themeDark
|
||||
ThemeMode.SYSTEM -> strings.themeSystem
|
||||
}
|
||||
@ -315,7 +315,7 @@ private fun DetailsPane(
|
||||
}
|
||||
item {
|
||||
Text(
|
||||
strings.sections,
|
||||
"Sections",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(top = 6.dp, start = 2.dp),
|
||||
@ -356,7 +356,7 @@ internal fun CoverAndTitle(book: Fb2Book, onImageOpen: (ViewedBookImage) -> Unit
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.weight(1f)) {
|
||||
Text(book.title, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(
|
||||
book.authors.joinToString { it.displayName }.ifBlank { strings.unknownAuthor },
|
||||
book.authors.joinToString { it.displayName }.ifBlank { "Unknown author" },
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
@ -374,9 +374,9 @@ internal fun CoverAndTitle(book: Fb2Book, onImageOpen: (ViewedBookImage) -> Unit
|
||||
internal fun MetadataCard(book: Fb2Book) {
|
||||
Card(shape = RoundedCornerShape(8.dp), colors = quietCardColors(), modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
DetailLine(strings.genres, book.genres.joinToString().ifBlank { strings.notSpecified })
|
||||
DetailLine(strings.translator, book.translators.joinToString { it.displayName }.ifBlank { strings.notSpecified })
|
||||
DetailLine(strings.source, book.sourceLanguage?.uppercase() ?: strings.notSpecified)
|
||||
DetailLine("Genres", book.genres.joinToString().ifBlank { "Not specified" })
|
||||
DetailLine("Translator", book.translators.joinToString { it.displayName }.ifBlank { "Not specified" })
|
||||
DetailLine("Source", book.sourceLanguage?.uppercase() ?: "Not specified")
|
||||
if (book.annotation.isNullOrBlank().not()) {
|
||||
Text(book.annotation.orEmpty(), style = MaterialTheme.typography.bodyMedium, lineHeight = 20.sp)
|
||||
}
|
||||
@ -395,9 +395,9 @@ internal fun MetadataCard(book: Fb2Book) {
|
||||
internal fun StatsCard(stats: BookStats) {
|
||||
Card(shape = RoundedCornerShape(8.dp), colors = quietCardColors(), modifier = Modifier.fillMaxWidth()) {
|
||||
Row(Modifier.fillMaxWidth().padding(16.dp), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
StatBlock(strings.words, stats.words.formatCompact())
|
||||
StatBlock(strings.sections, stats.sections.toString())
|
||||
StatBlock(strings.images, stats.images.toString())
|
||||
StatBlock("Words", stats.words.formatCompact())
|
||||
StatBlock("Sections", stats.sections.toString())
|
||||
StatBlock("Images", stats.images.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -586,7 +586,7 @@ private fun BookImage(
|
||||
contentScale = contentScale,
|
||||
)
|
||||
} else {
|
||||
Text(strings.noImage, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.outline)
|
||||
Text("No image", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.outline)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -835,7 +835,7 @@ private const val EllipsisPauseAfterMillis = 350L
|
||||
|
||||
private fun List<Fb2Section>.flattenSections(depth: Int = 0): List<ChapterEntry> =
|
||||
flatMapIndexed { index, section ->
|
||||
val fallback = strings.sectionFallback(index)
|
||||
val fallback = "Section ${index + 1}"
|
||||
listOf(ChapterEntry(section.title?.ifBlank { null } ?: fallback, section, depth)) +
|
||||
section.sections.flattenSections(depth + 1)
|
||||
}
|
||||
|
||||
@ -108,7 +108,7 @@ internal fun BookView(
|
||||
if (status == BookReadingStatus.NEW) markedRead = false
|
||||
showMessage(successMessage)
|
||||
} else {
|
||||
showMessage(strings.couldNotUpdateBook)
|
||||
showMessage("Could not update book.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -186,16 +186,16 @@ internal fun BookView(
|
||||
}
|
||||
},
|
||||
onMarkAsRead = {
|
||||
setReadingStatus(BookReadingStatus.READ, strings.markedAsRead())
|
||||
setReadingStatus(BookReadingStatus.READ, "Marked as read.")
|
||||
},
|
||||
onMarkToRead = {
|
||||
setReadingStatus(BookReadingStatus.TO_READ, strings.markedToRead())
|
||||
setReadingStatus(BookReadingStatus.TO_READ, "Marked to read.")
|
||||
},
|
||||
onNotInterested = {
|
||||
setReadingStatus(BookReadingStatus.NOT_INTERESTED, strings.markedNotInterested())
|
||||
setReadingStatus(BookReadingStatus.NOT_INTERESTED, "Marked as not interested.")
|
||||
},
|
||||
onClearMarks = {
|
||||
setReadingStatus(BookReadingStatus.NEW, strings.removedMarks())
|
||||
setReadingStatus(BookReadingStatus.NEW, "Cleared marks.")
|
||||
},
|
||||
readingStatus = libraryItem?.readingStatus,
|
||||
favorite = libraryItem?.favorite == true,
|
||||
@ -203,9 +203,9 @@ internal fun BookView(
|
||||
scope.launch {
|
||||
if (markLibraryFavorite(fileId, favorite)) {
|
||||
libraryItem = loadLibraryItem(fileId) ?: libraryItem?.copy(favorite = favorite)
|
||||
showMessage(if (favorite) strings.addedToFavorites() else strings.removedFromFavorites())
|
||||
showMessage(if (favorite) "Added to favorites." else "Removed from favorites.")
|
||||
} else {
|
||||
showMessage(strings.couldNotUpdateBook)
|
||||
showMessage("Could not update book.")
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -213,14 +213,14 @@ internal fun BookView(
|
||||
onShare = {
|
||||
scope.launch {
|
||||
val shared = shareLibraryBookFile(fileId)
|
||||
showMessage(if (shared) strings.shareOpened() else strings.couldNotShareBook())
|
||||
showMessage(if (shared) "Share opened." else "Could not share book.")
|
||||
}
|
||||
},
|
||||
showViewFileAction = showViewFileAction,
|
||||
onViewFile = {
|
||||
scope.launch {
|
||||
val opened = viewLibraryBookFile(fileId)
|
||||
showMessage(if (opened) strings.openedFileLocation() else strings.couldNotOpenFileLocation())
|
||||
showMessage(if (opened) "Opened file location." else "Could not open file location.")
|
||||
}
|
||||
},
|
||||
showReadAloudAction = showReadAloudAction,
|
||||
@ -337,7 +337,7 @@ private fun CompactReaderTopBar(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = strings.backToLibrary)
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back to library")
|
||||
}
|
||||
Text(
|
||||
title,
|
||||
@ -346,20 +346,20 @@ private fun CompactReaderTopBar(
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
IconButton(onClick = onThemeToggle) {
|
||||
Icon(Icons.Filled.Palette, contentDescription = strings.readerTheme)
|
||||
Icon(Icons.Filled.Palette, contentDescription = "Theme")
|
||||
}
|
||||
if (showReadAloudAction) {
|
||||
IconButton(onClick = onReadAloud) {
|
||||
Icon(Icons.AutoMirrored.Filled.VolumeUp, contentDescription = strings.readAloud)
|
||||
Icon(Icons.AutoMirrored.Filled.VolumeUp, contentDescription = "Read aloud")
|
||||
}
|
||||
}
|
||||
Box {
|
||||
IconButton(onClick = { menuOpen = true }) {
|
||||
Icon(Icons.Filled.MoreVert, contentDescription = strings.readerMenu)
|
||||
Icon(Icons.Filled.MoreVert, contentDescription = "Book reader menu")
|
||||
}
|
||||
DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(strings.info) },
|
||||
text = { Text("Info...") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onBookInfo()
|
||||
@ -367,7 +367,7 @@ private fun CompactReaderTopBar(
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text(strings.markAsRead) },
|
||||
text = { Text("Mark as read") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onMarkAsRead()
|
||||
@ -375,7 +375,7 @@ private fun CompactReaderTopBar(
|
||||
)
|
||||
if (readingStatus == BookReadingStatus.TO_READ) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(strings.removeToRead) },
|
||||
text = { Text("Remove to read") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onClearMarks()
|
||||
@ -383,7 +383,7 @@ private fun CompactReaderTopBar(
|
||||
)
|
||||
} else {
|
||||
DropdownMenuItem(
|
||||
text = { Text(strings.markToRead) },
|
||||
text = { Text("Mark to read") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onMarkToRead()
|
||||
@ -391,21 +391,21 @@ private fun CompactReaderTopBar(
|
||||
)
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text(strings.notInterested) },
|
||||
text = { Text("Not interested") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onNotInterested()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(strings.clearMarks) },
|
||||
text = { Text("Clear marks") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onClearMarks()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(if (favorite) strings.removeFavorite else strings.addFavorite) },
|
||||
text = { Text(if (favorite) "Remove favorite" else "Add favorite") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onFavoriteChange(!favorite)
|
||||
@ -416,7 +416,7 @@ private fun CompactReaderTopBar(
|
||||
}
|
||||
if (showShareAction) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(strings.share) },
|
||||
text = { Text("Share") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onShare()
|
||||
@ -425,7 +425,7 @@ private fun CompactReaderTopBar(
|
||||
}
|
||||
if (showViewFileAction) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(strings.viewFile) },
|
||||
text = { Text("View file") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onViewFile()
|
||||
@ -434,7 +434,7 @@ private fun CompactReaderTopBar(
|
||||
}
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text(strings.delete) },
|
||||
text = { Text("Delete") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
onDelete()
|
||||
@ -466,19 +466,19 @@ private fun ReadAloudPanel(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.Filled.Replay, contentDescription = strings.previousSentence)
|
||||
Icon(Icons.Filled.Replay, contentDescription = "Previous sentence")
|
||||
}
|
||||
IconButton(onClick = onPlayStop) {
|
||||
Icon(
|
||||
if (playing) Icons.Filled.Stop else Icons.Filled.PlayArrow,
|
||||
contentDescription = if (playing) strings.stopReading else strings.startReading,
|
||||
contentDescription = if (playing) "Stop reading" else "Start reading",
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onForward) {
|
||||
Icon(Icons.Filled.FastForward, contentDescription = strings.nextSentence)
|
||||
Icon(Icons.Filled.FastForward, contentDescription = "Next sentence")
|
||||
}
|
||||
IconButton(onClick = onSettings) {
|
||||
Icon(Icons.Filled.Settings, contentDescription = strings.readAloudSettings)
|
||||
Icon(Icons.Filled.Settings, contentDescription = "Read aloud settings")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -493,24 +493,24 @@ private fun ReadAloudSettingsDialog(
|
||||
) {
|
||||
var engineMenuOpen by remember { mutableStateOf(false) }
|
||||
var voiceMenuOpen by remember { mutableStateOf(false) }
|
||||
val selectedEngineLabel = state.engines.firstOrNull { it.id == state.selectedEngineId }?.label ?: strings.systemDefault
|
||||
val selectedEngineLabel = state.engines.firstOrNull { it.id == state.selectedEngineId }?.label ?: "System default"
|
||||
val selectedVoiceLabel = state.voices.firstOrNull { it.id == state.selectedVoiceId }
|
||||
?.let { "${it.label} (${it.localeTag})" }
|
||||
?: strings.autoRussian
|
||||
?: "Auto Russian"
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(strings.readAloud) },
|
||||
title = { Text("Read aloud") },
|
||||
text = {
|
||||
Column {
|
||||
Text(strings.ttsEngine, style = MaterialTheme.typography.labelMedium)
|
||||
Text("TTS engine", style = MaterialTheme.typography.labelMedium)
|
||||
Box {
|
||||
TextButton(onClick = { engineMenuOpen = true }) {
|
||||
Text(selectedEngineLabel)
|
||||
}
|
||||
DropdownMenu(expanded = engineMenuOpen, onDismissRequest = { engineMenuOpen = false }) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(strings.systemDefault) },
|
||||
text = { Text("System default") },
|
||||
onClick = {
|
||||
engineMenuOpen = false
|
||||
onEngineSelected(null)
|
||||
@ -528,14 +528,14 @@ private fun ReadAloudSettingsDialog(
|
||||
}
|
||||
}
|
||||
|
||||
Text(strings.offlineVoice, style = MaterialTheme.typography.labelMedium)
|
||||
Text("Offline voice", style = MaterialTheme.typography.labelMedium)
|
||||
Box {
|
||||
TextButton(onClick = { voiceMenuOpen = true }, enabled = !state.loading && state.voices.isNotEmpty()) {
|
||||
Text(selectedVoiceLabel)
|
||||
}
|
||||
DropdownMenu(expanded = voiceMenuOpen, onDismissRequest = { voiceMenuOpen = false }) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(strings.autoRussian) },
|
||||
text = { Text("Auto Russian") },
|
||||
onClick = {
|
||||
voiceMenuOpen = false
|
||||
onVoiceSelected(null)
|
||||
@ -556,7 +556,7 @@ private fun ReadAloudSettingsDialog(
|
||||
if (state.loading) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
CircularProgressIndicator()
|
||||
Text(strings.loadingVoices, modifier = Modifier.padding(start = 12.dp))
|
||||
Text("Loading voices...", modifier = Modifier.padding(start = 12.dp))
|
||||
}
|
||||
}
|
||||
state.message?.let { Text(it, color = MaterialTheme.colorScheme.error) }
|
||||
@ -564,7 +564,7 @@ private fun ReadAloudSettingsDialog(
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(strings.done)
|
||||
Text("Done")
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@ -55,10 +55,10 @@ internal fun ScanScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = { Text(strings.scan) },
|
||||
title = { Text("Scan") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onStateChange(AppState.Library(state.items, scanPath, message)) }) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = strings.backToLibrary)
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back to library")
|
||||
}
|
||||
},
|
||||
colors = themedTopAppBarColors(),
|
||||
@ -83,21 +83,21 @@ internal fun ScanScreen(
|
||||
OutlinedTextField(
|
||||
value = scanPath,
|
||||
onValueChange = { scanPath = it },
|
||||
label = { Text(strings.rootFolder) },
|
||||
label = { Text("Root folder") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Button(
|
||||
onClick = {
|
||||
message = strings.scanning
|
||||
message = "Scanning..."
|
||||
onStartScan(scanPath)
|
||||
},
|
||||
enabled = !busy && scanPath.isNotBlank(),
|
||||
) {
|
||||
Icon(Icons.Filled.Scanner, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(strings.scan)
|
||||
Text("Scan")
|
||||
}
|
||||
FilledTonalButton(
|
||||
onClick = {
|
||||
@ -109,7 +109,7 @@ internal fun ScanScreen(
|
||||
) {
|
||||
Icon(Icons.Filled.FolderOpen, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(strings.choose)
|
||||
Text("Choose")
|
||||
}
|
||||
if (busy) {
|
||||
CircularProgressIndicator(modifier = Modifier.width(24.dp).height(24.dp), strokeWidth = 2.dp)
|
||||
@ -126,7 +126,7 @@ internal fun ScanScreen(
|
||||
Text(it, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.secondary)
|
||||
}
|
||||
libraryLogPath()?.let {
|
||||
Text("${strings.logPrefix}: $it", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline)
|
||||
Text("Log: $it", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -136,4 +136,12 @@ internal fun ScanScreen(
|
||||
}
|
||||
|
||||
private fun LibraryScanProgress.toScanMessage(): String =
|
||||
strings.checkingFile(currentFile, scannedFiles, importedFiles, skippedFiles, failedFiles)
|
||||
buildString {
|
||||
append("Checking")
|
||||
currentFile?.takeIf(String::isNotBlank)?.let { append(" $it") }
|
||||
append(". Found $scannedFiles supported files")
|
||||
append(", imported $importedFiles")
|
||||
append(", skipped $skippedFiles")
|
||||
append(", failed $failedFiles")
|
||||
append(".")
|
||||
}
|
||||
|
||||
@ -42,10 +42,10 @@ internal fun ErrorScreen(message: String, onBack: () -> Unit) {
|
||||
Box(Modifier.fillMaxSize().padding(24.dp), contentAlignment = Alignment.Center) {
|
||||
Card(shape = RoundedCornerShape(8.dp), colors = quietCardColors()) {
|
||||
Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(strings.libraryError, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||
Text("Library error", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||
Text(message, style = MaterialTheme.typography.bodyMedium)
|
||||
TextButton(onClick = onBack) {
|
||||
Text(strings.retry)
|
||||
Text("Retry")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -112,4 +112,4 @@ internal fun Int.formatCompact(): String =
|
||||
if (this >= 10_000) "${(this / 1000.0).roundToInt()}k" else toString()
|
||||
|
||||
internal fun Double?.progressLabel(): String =
|
||||
strings.progressLabel(this)
|
||||
this?.let { "Progress ${(it * 100).roundToInt()}%" } ?: "Progress not recorded"
|
||||
|
||||
@ -9,6 +9,13 @@ internal fun ThemeMode.next(): ThemeMode =
|
||||
ThemeMode.DARK -> ThemeMode.SYSTEM
|
||||
}
|
||||
|
||||
internal val ThemeMode.displayName: String
|
||||
get() = when (this) {
|
||||
ThemeMode.LIGHT -> "Light"
|
||||
ThemeMode.DARK -> "Dark"
|
||||
ThemeMode.SYSTEM -> "System"
|
||||
}
|
||||
|
||||
internal fun lightReaderColorScheme() = androidx.compose.material3.lightColorScheme(
|
||||
primary = Color(0xFF425D56),
|
||||
onPrimary = Color.White,
|
||||
|
||||
@ -75,7 +75,7 @@ actual suspend fun loadPlatformOpenBookRequest(): PlatformOpenBookRequest? = nul
|
||||
actual suspend fun chooseLibraryScanDirectory(): String? = withContext(Dispatchers.IO) {
|
||||
val chooser = JFileChooser(defaultLibraryScanPath())
|
||||
chooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
|
||||
chooser.dialogTitle = strings.chooseLibraryFolder
|
||||
chooser.dialogTitle = "Choose library folder"
|
||||
if (chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
|
||||
chooser.selectedFile.absolutePath
|
||||
} else {
|
||||
@ -373,18 +373,6 @@ 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()
|
||||
@ -601,5 +589,4 @@ 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"
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
package net.sergeych.toread
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
internal actual fun platformLocaleTags(): List<String> =
|
||||
listOf(Locale.getDefault().toLanguageTag())
|
||||
@ -6,8 +6,8 @@ import androidx.compose.ui.window.application
|
||||
fun main() = application {
|
||||
Window(
|
||||
onCloseRequest = ::exitApplication,
|
||||
title = strings.appName,
|
||||
title = "toread",
|
||||
) {
|
||||
App()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -77,17 +77,6 @@ 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
|
||||
@ -110,8 +99,6 @@ 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')
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
package net.sergeych.toread
|
||||
|
||||
import kotlinx.browser.window
|
||||
|
||||
internal actual fun platformLocaleTags(): List<String> =
|
||||
listOf(window.navigator.language)
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 194 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 200 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 179 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 171 KiB |
Loading…
x
Reference in New Issue
Block a user