From 26343cb8a1f5aeaacda60682442656243d2db255 Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 18 May 2026 00:39:29 +0300 Subject: [PATCH] View/logic improvements --- .../sergeych/toread/BookPlatform.android.kt | 94 +++++++++++++++++++ .../toread/PlatformBackHandler.android.kt | 13 +++ .../kotlin/net/sergeych/toread/App.kt | 68 +++++++++++--- .../net/sergeych/toread/LibraryPlatform.kt | 10 ++ .../net/sergeych/toread/LibraryScreen.kt | 63 +++++++++++-- .../sergeych/toread/PlatformBackHandler.kt | 10 ++ .../net/sergeych/toread/BookPlatform.jvm.kt | 94 +++++++++++++++++++ .../toread/PlatformBackHandler.jvm.kt | 10 ++ .../net/sergeych/toread/BookPlatform.web.kt | 5 + .../toread/PlatformBackHandler.web.kt | 44 +++++++++ 10 files changed, 386 insertions(+), 25 deletions(-) create mode 100644 composeApp/src/androidMain/kotlin/net/sergeych/toread/PlatformBackHandler.android.kt create mode 100644 composeApp/src/commonMain/kotlin/net/sergeych/toread/PlatformBackHandler.kt create mode 100644 composeApp/src/jvmMain/kotlin/net/sergeych/toread/PlatformBackHandler.jvm.kt create mode 100644 composeApp/src/webMain/kotlin/net/sergeych/toread/PlatformBackHandler.web.kt diff --git a/composeApp/src/androidMain/kotlin/net/sergeych/toread/BookPlatform.android.kt b/composeApp/src/androidMain/kotlin/net/sergeych/toread/BookPlatform.android.kt index 47b238a..3ca927a 100644 --- a/composeApp/src/androidMain/kotlin/net/sergeych/toread/BookPlatform.android.kt +++ b/composeApp/src/androidMain/kotlin/net/sergeych/toread/BookPlatform.android.kt @@ -16,6 +16,9 @@ import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap import androidx.core.content.FileProvider import net.sergeych.toread.fb2.Fb2Binary +import net.sergeych.toread.fb2.Fb2Book +import net.sergeych.toread.fb2.Fb2Format +import net.sergeych.toread.storage.BookRecord import net.sergeych.toread.storage.BookReadingStatus import net.sergeych.toread.storage.ContentAnchor import net.sergeych.toread.storage.LibraryFileRecord @@ -224,6 +227,45 @@ actual suspend fun openLibraryBook(fileId: String): ByteArray? = withContext(Dis } } +actual suspend fun refreshLibraryItemFromParsedBook(fileId: String, book: Fb2Book): LibraryItem? = withContext(Dispatchers.IO) { + openLibraryDatabase().useLibrary { db -> + db.refreshBookCardFromParsedBook(fileId, book).item + } +} + +actual suspend fun rescanAllLibraryBooks(): LibraryRescanReport = withContext(Dispatchers.IO) { + appendLibraryLog("rescan all library requested") + openLibraryDatabase().useLibrary { db -> + var scanned = 0 + var updated = 0 + var failed = 0 + db.files.list(Int.MAX_VALUE, 0).forEach { file -> + val bytes = file.storageUri?.let(::readStorageUriBytes) + if (bytes == null) { + failed += 1 + appendLibraryLog("rescan missing fileId=${file.id} uri=${file.storageUri}") + return@forEach + } + scanned += 1 + val parsed = runCatching { + Fb2Format.parse(bytes, file.originalFilename ?: file.storageUri ?: file.id) + } + parsed + .onSuccess { + if (db.refreshBookCardFromParsedBook(file.id, it).updated) { + updated += 1 + } + } + .onFailure { + failed += 1 + appendLibraryLog("rescan failed fileId=${file.id} error=${it.message}") + } + } + appendLibraryLog("rescan all library finished scanned=$scanned updated=$updated failed=$failed") + LibraryRescanReport(scannedFiles = scanned, updatedFiles = updated, failedFiles = failed) + } +} + actual suspend fun deleteLibraryItem(fileId: String): Boolean = withContext(Dispatchers.IO) { appendLibraryLog("delete fileId=$fileId") openLibraryDatabase().useLibrary { db -> @@ -633,6 +675,58 @@ private fun LibraryFileRecord.toLibraryItem(): LibraryItem = importedAt = importedAt, ) +private data class ParsedBookCover( + val bytes: ByteArray, + val mimeType: String?, +) + +private data class BookCardRefresh( + val updated: Boolean, + val item: LibraryItem?, +) + +private fun H2LibraryDatabase.refreshBookCardFromParsedBook(fileId: String, book: Fb2Book): BookCardRefresh { + val file = files.get(fileId) ?: return BookCardRefresh(updated = false, item = null) + val bookId = file.bookId ?: return BookCardRefresh(updated = false, item = null) + val stored = books.get(bookId) ?: return BookCardRefresh(updated = false, item = null) + val cover = book.libraryCardCover() + val next = stored.copy( + title = book.title.ifBlank { file.originalFilename?.substringBeforeLast('.') ?: stored.title.orEmpty() }, + authors = book.authors.mapNotNull { it.displayName.takeIf(String::isNotBlank) }, + language = book.language, + date = book.date, + description = book.annotation, + keywords = book.keywords, + coverImage = cover?.bytes, + coverImageMimeType = cover?.mimeType, + updatedAt = System.currentTimeMillis(), + ) + val updated = !stored.hasSameCardMetadata(next) + if (updated) { + appendLibraryLog("refresh book card fileId=$fileId bookId=$bookId title=${next.title}") + books.upsert(next) + } + return BookCardRefresh(updated = updated, item = files.getLibraryFile(fileId)?.toLibraryItem()) +} + +private fun Fb2Book.libraryCardCover(): ParsedBookCover? { + val image = coverImages.firstOrNull() ?: bodyImages.firstOrNull() + val binary = image?.let(::binaryFor) ?: return null + return runCatching { + ParsedBookCover(bytes = binary.imageBytes(), mimeType = binary.contentType) + }.getOrNull() +} + +private fun BookRecord.hasSameCardMetadata(other: BookRecord): Boolean = + title == other.title && + authors == other.authors && + language == other.language && + date == other.date && + description == other.description && + keywords == other.keywords && + coverImage.contentEquals(other.coverImage) && + coverImageMimeType == other.coverImageMimeType + private fun libraryLogFile(): File = File(appContext.filesDir, "logs/toread.log") diff --git a/composeApp/src/androidMain/kotlin/net/sergeych/toread/PlatformBackHandler.android.kt b/composeApp/src/androidMain/kotlin/net/sergeych/toread/PlatformBackHandler.android.kt new file mode 100644 index 0000000..ebe4382 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/net/sergeych/toread/PlatformBackHandler.android.kt @@ -0,0 +1,13 @@ +package net.sergeych.toread + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable + +@Composable +internal actual fun PlatformBackHandler( + enabled: Boolean, + navigationDepth: Int, + onBack: () -> Unit, +) { + BackHandler(enabled = enabled, onBack = onBack) +} diff --git a/composeApp/src/commonMain/kotlin/net/sergeych/toread/App.kt b/composeApp/src/commonMain/kotlin/net/sergeych/toread/App.kt index 0230295..0b834ce 100644 --- a/composeApp/src/commonMain/kotlin/net/sergeych/toread/App.kt +++ b/composeApp/src/commonMain/kotlin/net/sergeych/toread/App.kt @@ -103,6 +103,41 @@ private fun BookReaderApp(onThemeToggle: () -> Unit) { state = loadStartupState() } + fun navigateBack() { + imageViewer?.let { + imageViewer = null + return + } + state = when (val current = state) { + is AppState.BookInfo -> AppState.Reader( + fileId = current.fileId, + book = current.book, + libraryItems = current.libraryItems, + scanPath = current.scanPath, + message = current.message, + ) + is AppState.Reader -> { + scope.launch { saveActiveReadingFileId(null) } + AppState.Library(current.libraryItems, current.scanPath, current.message) + } + is AppState.Scan -> AppState.Library(current.items, current.scanPath, current.message) + is AppState.Error -> AppState.LoadingLibrary + is AppState.Library, AppState.LoadingLibrary -> current + } + } + + val navigationDepth = when (state) { + is AppState.BookInfo -> 2 + is AppState.Error, is AppState.Reader, is AppState.Scan -> 1 + is AppState.Library, AppState.LoadingLibrary -> 0 + } + if (imageViewer != null) 1 else 0 + + PlatformBackHandler( + enabled = navigationDepth > 0, + navigationDepth = navigationDepth, + onBack = ::navigateBack, + ) + fun startScan(path: String) { if (scanJob?.isActive == true) return activeScan = LibraryScanProgress(0, 0, 0, 0) @@ -126,7 +161,17 @@ private fun BookReaderApp(onThemeToggle: () -> Unit) { scanJob = null activeScan = null state = when (val current = state) { - is AppState.Library, is AppState.Scan, AppState.LoadingLibrary -> loadLibraryState(message, path) + is AppState.Library -> if (report.getOrNull()?.hasLibraryChanges() == true) { + loadLibraryState(message, path) + } else { + current.copy(scanPath = path, message = message) + } + is AppState.Scan -> if (report.getOrNull()?.hasLibraryChanges() == true) { + loadLibraryState(message, path) + } else { + AppState.Library(current.items, path, message) + } + AppState.LoadingLibrary -> loadLibraryState(message, path) is AppState.Reader -> current.copy(message = message) is AppState.BookInfo -> current.copy(message = message) is AppState.Error -> current @@ -169,31 +214,21 @@ private fun BookReaderApp(onThemeToggle: () -> Unit) { onDeleted = { message -> state = AppState.Library(emptyList(), current.scanPath, message) }, - onBack = { - state = AppState.Library(emptyList(), current.scanPath, current.message) - }, + onBack = ::navigateBack, ) is AppState.BookInfo -> BookInfoScreen( fileId = current.fileId, book = current.book, onImageOpen = { imageViewer = it }, - onBack = { - state = AppState.Reader( - fileId = current.fileId, - book = current.book, - libraryItems = current.libraryItems, - scanPath = current.scanPath, - message = current.message, - ) - }, + onBack = ::navigateBack, ) - is AppState.Error -> ErrorScreen(current.message, onBack = { state = AppState.LoadingLibrary }) + is AppState.Error -> ErrorScreen(current.message, onBack = ::navigateBack) } imageViewer?.let { image -> ImageViewer( image = image, - onBack = { imageViewer = null }, + onBack = ::navigateBack, ) } } @@ -204,3 +239,6 @@ internal data class ViewedBookImage( val mimeType: String, val title: String, ) + +private fun LibraryScanReport.hasLibraryChanges(): Boolean = + importedFiles > 0 diff --git a/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryPlatform.kt b/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryPlatform.kt index 8d6d37a..490c9d3 100644 --- a/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryPlatform.kt +++ b/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryPlatform.kt @@ -44,6 +44,12 @@ data class LibraryScanProgress( val totalFiles: Int? = null, ) +data class LibraryRescanReport( + val scannedFiles: Int, + val updatedFiles: Int, + val failedFiles: Int, +) + data class PlatformOpenBookRequest( val id: String, val displayName: String, @@ -100,6 +106,10 @@ expect suspend fun scanLibrarySubtree(path: String, onProgress: (LibraryScanProg expect suspend fun openLibraryBook(fileId: String): ByteArray? +expect suspend fun refreshLibraryItemFromParsedBook(fileId: String, book: Fb2Book): LibraryItem? + +expect suspend fun rescanAllLibraryBooks(): LibraryRescanReport + expect suspend fun deleteLibraryItem(fileId: String): Boolean expect suspend fun loadLibraryReadingPosition(fileId: String): ReadingPosition? diff --git a/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryScreen.kt b/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryScreen.kt index 1568658..0e942aa 100644 --- a/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryScreen.kt +++ b/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryScreen.kt @@ -27,7 +27,6 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Search import androidx.compose.material3.Card import androidx.compose.material3.Checkbox @@ -62,6 +61,7 @@ import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.type import androidx.compose.ui.layout.ContentScale 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 @@ -103,18 +103,28 @@ internal fun LibraryScreen( suspend fun loadPage(reset: Boolean = false) { if (loadingPage) return loadingPage = true + val previousItems = items if (reset) { - items = emptyList() nextOffset = 0 endReached = false - coverCache.clear() } val offset = if (reset) 0 else nextOffset try { - val page = loadLibraryItemsPage(LibraryPageSize, offset) - items = if (reset) page else items + page + val limit = if (reset) maxOf(LibraryPageSize, previousItems.size) else LibraryPageSize + val page = loadLibraryItemsPage(limit, offset) + if (reset) { + if (page != previousItems) { + items = page + } + val visibleFileIds = page.mapTo(mutableSetOf()) { it.fileId } + coverCache.keys.toList().forEach { fileId -> + if (fileId !in visibleFileIds) coverCache.remove(fileId) + } + } else { + items = items + page + } nextOffset = offset + page.size - endReached = page.size < LibraryPageSize + endReached = page.size < limit } catch (t: Throwable) { message = t.message ?: "Could not load library." endReached = true @@ -136,6 +146,24 @@ internal fun LibraryScreen( } } + fun rescanAllLibrary() { + settingsMenuOpen = false + scope.launch { + busy = true + message = "Rescanning library..." + try { + val report = rescanAllLibraryBooks() + refresh( + "Rescanned ${report.scannedFiles}, updated ${report.updatedFiles}, failed ${report.failedFiles}.", + ) + } catch (t: Throwable) { + message = t.message ?: "Library rescan failed." + } finally { + busy = false + } + } + } + fun clearSearch() { searchText = "" searchResults = emptyList() @@ -212,14 +240,17 @@ internal fun LibraryScreen( }, colors = themedTopAppBarColors(), actions = { - IconButton(onClick = { refresh() }, enabled = !busy && !loadingPage) { - Icon(Icons.Filled.Refresh, contentDescription = "Refresh library") - } 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, +// onClick = ::rescanAllLibrary, +// ) +// HorizontalDivider() DropdownMenuItem( leadingIcon = { Checkbox(checked = autoScanDownloads, onCheckedChange = null) @@ -282,12 +313,19 @@ internal fun LibraryScreen( val next = runCatching { 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 -> + items = items.replaceLibraryItem(updatedItem) + searchResults = searchResults.replaceLibraryItem(updatedItem) + readerLibraryItems = readerLibraryItems.replaceLibraryItem(updatedItem) + coverCache[updatedItem.fileId] = loadLibraryItemCover(updatedItem.fileId) + } markLibraryReadingStatus(item.fileId, BookReadingStatus.READING) saveActiveReadingFileId(item.fileId) AppState.Reader( fileId = item.fileId, book = book, - libraryItems = visibleItems, + libraryItems = readerLibraryItems, scanPath = state.scanPath, message = message, ) @@ -639,12 +677,14 @@ private fun LibraryRow( style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, maxLines = 1, + overflow = TextOverflow.Ellipsis, ) Text( item.authors.joinToString().ifBlank { "Unknown author" }, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.secondary, maxLines = 1, + overflow = TextOverflow.Ellipsis, ) Text( item.libraryMetadataLine(), @@ -791,6 +831,9 @@ private fun Long.formatBytes(): String = else -> "$this B" } +private fun List.replaceLibraryItem(item: LibraryItem): List = + map { current -> if (current.fileId == item.fileId) item else current } + private fun LibraryScanProgress.toCatalogScanMessage(): String { val total = totalFiles ?: return "Scanned $scannedFiles books" val percent = if (total <= 0) { diff --git a/composeApp/src/commonMain/kotlin/net/sergeych/toread/PlatformBackHandler.kt b/composeApp/src/commonMain/kotlin/net/sergeych/toread/PlatformBackHandler.kt new file mode 100644 index 0000000..aa9d548 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/sergeych/toread/PlatformBackHandler.kt @@ -0,0 +1,10 @@ +package net.sergeych.toread + +import androidx.compose.runtime.Composable + +@Composable +internal expect fun PlatformBackHandler( + enabled: Boolean, + navigationDepth: Int, + onBack: () -> Unit, +) diff --git a/composeApp/src/jvmMain/kotlin/net/sergeych/toread/BookPlatform.jvm.kt b/composeApp/src/jvmMain/kotlin/net/sergeych/toread/BookPlatform.jvm.kt index b900b01..305d413 100644 --- a/composeApp/src/jvmMain/kotlin/net/sergeych/toread/BookPlatform.jvm.kt +++ b/composeApp/src/jvmMain/kotlin/net/sergeych/toread/BookPlatform.jvm.kt @@ -3,6 +3,9 @@ package net.sergeych.toread import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.toComposeImageBitmap import net.sergeych.toread.fb2.Fb2Binary +import net.sergeych.toread.fb2.Fb2Book +import net.sergeych.toread.fb2.Fb2Format +import net.sergeych.toread.storage.BookRecord import net.sergeych.toread.storage.BookReadingStatus import net.sergeych.toread.storage.ContentAnchor import net.sergeych.toread.storage.LibraryFileRecord @@ -175,6 +178,45 @@ actual suspend fun openLibraryBook(fileId: String): ByteArray? = withContext(Dis } } +actual suspend fun refreshLibraryItemFromParsedBook(fileId: String, book: Fb2Book): LibraryItem? = withContext(Dispatchers.IO) { + openLibraryDatabase().useLibrary { db -> + db.refreshBookCardFromParsedBook(fileId, book).item + } +} + +actual suspend fun rescanAllLibraryBooks(): LibraryRescanReport = withContext(Dispatchers.IO) { + appendLibraryLog("rescan all library requested") + openLibraryDatabase().useLibrary { db -> + var scanned = 0 + var updated = 0 + var failed = 0 + db.files.list(Int.MAX_VALUE, 0).forEach { file -> + val bytes = file.storageUri?.let { File(it) }?.takeIf { it.isFile }?.readBytes() + if (bytes == null) { + failed += 1 + appendLibraryLog("rescan missing fileId=${file.id} uri=${file.storageUri}") + return@forEach + } + scanned += 1 + val parsed = runCatching { + Fb2Format.parse(bytes, file.originalFilename ?: file.storageUri ?: file.id) + } + parsed + .onSuccess { + if (db.refreshBookCardFromParsedBook(file.id, it).updated) { + updated += 1 + } + } + .onFailure { + failed += 1 + appendLibraryLog("rescan failed fileId=${file.id} error=${it.message}") + } + } + appendLibraryLog("rescan all library finished scanned=$scanned updated=$updated failed=$failed") + LibraryRescanReport(scannedFiles = scanned, updatedFiles = updated, failedFiles = failed) + } +} + actual suspend fun deleteLibraryItem(fileId: String): Boolean = withContext(Dispatchers.IO) { appendLibraryLog("delete fileId=$fileId") openLibraryDatabase().useLibrary { db -> @@ -433,6 +475,58 @@ private fun LibraryFileRecord.toLibraryItem(): LibraryItem = importedAt = importedAt, ) +private data class ParsedBookCover( + val bytes: ByteArray, + val mimeType: String?, +) + +private data class BookCardRefresh( + val updated: Boolean, + val item: LibraryItem?, +) + +private fun H2LibraryDatabase.refreshBookCardFromParsedBook(fileId: String, book: Fb2Book): BookCardRefresh { + val file = files.get(fileId) ?: return BookCardRefresh(updated = false, item = null) + val bookId = file.bookId ?: return BookCardRefresh(updated = false, item = null) + val stored = books.get(bookId) ?: return BookCardRefresh(updated = false, item = null) + val cover = book.libraryCardCover() + val next = stored.copy( + title = book.title.ifBlank { file.originalFilename?.substringBeforeLast('.') ?: stored.title.orEmpty() }, + authors = book.authors.mapNotNull { it.displayName.takeIf(String::isNotBlank) }, + language = book.language, + date = book.date, + description = book.annotation, + keywords = book.keywords, + coverImage = cover?.bytes, + coverImageMimeType = cover?.mimeType, + updatedAt = System.currentTimeMillis(), + ) + val updated = !stored.hasSameCardMetadata(next) + if (updated) { + appendLibraryLog("refresh book card fileId=$fileId bookId=$bookId title=${next.title}") + books.upsert(next) + } + return BookCardRefresh(updated = updated, item = files.getLibraryFile(fileId)?.toLibraryItem()) +} + +private fun Fb2Book.libraryCardCover(): ParsedBookCover? { + val image = coverImages.firstOrNull() ?: bodyImages.firstOrNull() + val binary = image?.let(::binaryFor) ?: return null + return runCatching { + ParsedBookCover(bytes = binary.imageBytes(), mimeType = binary.contentType) + }.getOrNull() +} + +private fun BookRecord.hasSameCardMetadata(other: BookRecord): Boolean = + title == other.title && + authors == other.authors && + language == other.language && + date == other.date && + description == other.description && + keywords == other.keywords && + coverImage.contentEquals(other.coverImage) && + coverImageMimeType == other.coverImageMimeType + private fun libraryLogFile(): File = File(System.getProperty("user.home"), ".toread/toread.log") diff --git a/composeApp/src/jvmMain/kotlin/net/sergeych/toread/PlatformBackHandler.jvm.kt b/composeApp/src/jvmMain/kotlin/net/sergeych/toread/PlatformBackHandler.jvm.kt new file mode 100644 index 0000000..cbe9c60 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/net/sergeych/toread/PlatformBackHandler.jvm.kt @@ -0,0 +1,10 @@ +package net.sergeych.toread + +import androidx.compose.runtime.Composable + +@Composable +internal actual fun PlatformBackHandler( + enabled: Boolean, + navigationDepth: Int, + onBack: () -> Unit, +) = Unit diff --git a/composeApp/src/webMain/kotlin/net/sergeych/toread/BookPlatform.web.kt b/composeApp/src/webMain/kotlin/net/sergeych/toread/BookPlatform.web.kt index 379144c..12c0f4a 100644 --- a/composeApp/src/webMain/kotlin/net/sergeych/toread/BookPlatform.web.kt +++ b/composeApp/src/webMain/kotlin/net/sergeych/toread/BookPlatform.web.kt @@ -38,6 +38,11 @@ actual suspend fun scanLibrarySubtree( actual suspend fun openLibraryBook(fileId: String): ByteArray? = null +actual suspend fun refreshLibraryItemFromParsedBook(fileId: String, book: net.sergeych.toread.fb2.Fb2Book): LibraryItem? = null + +actual suspend fun rescanAllLibraryBooks(): LibraryRescanReport = + LibraryRescanReport(scannedFiles = 0, updatedFiles = 0, failedFiles = 0) + actual suspend fun deleteLibraryItem(fileId: String): Boolean = false actual suspend fun loadLibraryReadingPosition(fileId: String): ReadingPosition? = null diff --git a/composeApp/src/webMain/kotlin/net/sergeych/toread/PlatformBackHandler.web.kt b/composeApp/src/webMain/kotlin/net/sergeych/toread/PlatformBackHandler.web.kt new file mode 100644 index 0000000..8205603 --- /dev/null +++ b/composeApp/src/webMain/kotlin/net/sergeych/toread/PlatformBackHandler.web.kt @@ -0,0 +1,44 @@ +package net.sergeych.toread + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberUpdatedState +import kotlin.js.ExperimentalWasmJsInterop +import kotlinx.browser.window +import org.w3c.dom.events.Event + +private var currentBrowserNavigationDepth = 0 + +@Composable +@OptIn(ExperimentalWasmJsInterop::class) +internal actual fun PlatformBackHandler( + enabled: Boolean, + navigationDepth: Int, + onBack: () -> Unit, +) { + val currentOnBack = rememberUpdatedState(onBack) + + LaunchedEffect(navigationDepth) { + if (navigationDepth > currentBrowserNavigationDepth) { + repeat(navigationDepth - currentBrowserNavigationDepth) { + window.history.pushState(null, "", window.location.href) + } + } + currentBrowserNavigationDepth = navigationDepth + } + + DisposableEffect(enabled) { + if (!enabled) { + onDispose { } + } else { + val listener: (Event) -> Unit = { + currentOnBack.value() + } + window.addEventListener("popstate", listener) + onDispose { + window.removeEventListener("popstate", listener) + } + } + } +}