View/logic improvements

This commit is contained in:
Sergey Chernov 2026-05-18 00:39:29 +03:00
parent da88a6e221
commit 26343cb8a1
10 changed files with 386 additions and 25 deletions

View File

@ -16,6 +16,9 @@ import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import net.sergeych.toread.fb2.Fb2Binary 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.BookReadingStatus
import net.sergeych.toread.storage.ContentAnchor import net.sergeych.toread.storage.ContentAnchor
import net.sergeych.toread.storage.LibraryFileRecord 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) { actual suspend fun deleteLibraryItem(fileId: String): Boolean = withContext(Dispatchers.IO) {
appendLibraryLog("delete fileId=$fileId") appendLibraryLog("delete fileId=$fileId")
openLibraryDatabase().useLibrary { db -> openLibraryDatabase().useLibrary { db ->
@ -633,6 +675,58 @@ private fun LibraryFileRecord.toLibraryItem(): LibraryItem =
importedAt = importedAt, 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 = private fun libraryLogFile(): File =
File(appContext.filesDir, "logs/toread.log") File(appContext.filesDir, "logs/toread.log")

View File

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

View File

@ -103,6 +103,41 @@ private fun BookReaderApp(onThemeToggle: () -> Unit) {
state = loadStartupState() 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) { fun startScan(path: String) {
if (scanJob?.isActive == true) return if (scanJob?.isActive == true) return
activeScan = LibraryScanProgress(0, 0, 0, 0) activeScan = LibraryScanProgress(0, 0, 0, 0)
@ -126,7 +161,17 @@ private fun BookReaderApp(onThemeToggle: () -> Unit) {
scanJob = null scanJob = null
activeScan = null activeScan = null
state = when (val current = state) { 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.Reader -> current.copy(message = message)
is AppState.BookInfo -> current.copy(message = message) is AppState.BookInfo -> current.copy(message = message)
is AppState.Error -> current is AppState.Error -> current
@ -169,31 +214,21 @@ private fun BookReaderApp(onThemeToggle: () -> Unit) {
onDeleted = { message -> onDeleted = { message ->
state = AppState.Library(emptyList(), current.scanPath, message) state = AppState.Library(emptyList(), current.scanPath, message)
}, },
onBack = { onBack = ::navigateBack,
state = AppState.Library(emptyList(), current.scanPath, current.message)
},
) )
is AppState.BookInfo -> BookInfoScreen( is AppState.BookInfo -> BookInfoScreen(
fileId = current.fileId, fileId = current.fileId,
book = current.book, book = current.book,
onImageOpen = { imageViewer = it }, onImageOpen = { imageViewer = it },
onBack = { onBack = ::navigateBack,
state = AppState.Reader(
fileId = current.fileId,
book = current.book,
libraryItems = current.libraryItems,
scanPath = current.scanPath,
message = current.message,
)
},
) )
is AppState.Error -> ErrorScreen(current.message, onBack = { state = AppState.LoadingLibrary }) is AppState.Error -> ErrorScreen(current.message, onBack = ::navigateBack)
} }
imageViewer?.let { image -> imageViewer?.let { image ->
ImageViewer( ImageViewer(
image = image, image = image,
onBack = { imageViewer = null }, onBack = ::navigateBack,
) )
} }
} }
@ -204,3 +239,6 @@ internal data class ViewedBookImage(
val mimeType: String, val mimeType: String,
val title: String, val title: String,
) )
private fun LibraryScanReport.hasLibraryChanges(): Boolean =
importedFiles > 0

View File

@ -44,6 +44,12 @@ data class LibraryScanProgress(
val totalFiles: Int? = null, val totalFiles: Int? = null,
) )
data class LibraryRescanReport(
val scannedFiles: Int,
val updatedFiles: Int,
val failedFiles: Int,
)
data class PlatformOpenBookRequest( data class PlatformOpenBookRequest(
val id: String, val id: String,
val displayName: 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 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 deleteLibraryItem(fileId: String): Boolean
expect suspend fun loadLibraryReadingPosition(fileId: String): ReadingPosition? expect suspend fun loadLibraryReadingPosition(fileId: String): ReadingPosition?

View File

@ -27,7 +27,6 @@ import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.Checkbox 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.input.key.type
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight 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.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import net.sergeych.toread.fb2.Fb2Format import net.sergeych.toread.fb2.Fb2Format
@ -103,18 +103,28 @@ internal fun LibraryScreen(
suspend fun loadPage(reset: Boolean = false) { suspend fun loadPage(reset: Boolean = false) {
if (loadingPage) return if (loadingPage) return
loadingPage = true loadingPage = true
val previousItems = items
if (reset) { if (reset) {
items = emptyList()
nextOffset = 0 nextOffset = 0
endReached = false endReached = false
coverCache.clear()
} }
val offset = if (reset) 0 else nextOffset val offset = if (reset) 0 else nextOffset
try { try {
val page = loadLibraryItemsPage(LibraryPageSize, offset) val limit = if (reset) maxOf(LibraryPageSize, previousItems.size) else LibraryPageSize
items = if (reset) page else items + page 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 nextOffset = offset + page.size
endReached = page.size < LibraryPageSize endReached = page.size < limit
} catch (t: Throwable) { } catch (t: Throwable) {
message = t.message ?: "Could not load library." message = t.message ?: "Could not load library."
endReached = true 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() { fun clearSearch() {
searchText = "" searchText = ""
searchResults = emptyList() searchResults = emptyList()
@ -212,14 +240,17 @@ internal fun LibraryScreen(
}, },
colors = themedTopAppBarColors(), colors = themedTopAppBarColors(),
actions = { actions = {
IconButton(onClick = { refresh() }, enabled = !busy && !loadingPage) {
Icon(Icons.Filled.Refresh, contentDescription = "Refresh library")
}
Box { Box {
IconButton(onClick = { settingsMenuOpen = true }) { IconButton(onClick = { settingsMenuOpen = true }) {
Icon(Icons.Filled.MoreVert, contentDescription = "Library options") Icon(Icons.Filled.MoreVert, contentDescription = "Library options")
} }
DropdownMenu(expanded = settingsMenuOpen, onDismissRequest = { settingsMenuOpen = false }) { DropdownMenu(expanded = settingsMenuOpen, onDismissRequest = { settingsMenuOpen = false }) {
// DropdownMenuItem(
// text = { Text("Rescan all library") },
// enabled = !busy && activeScan == null,
// onClick = ::rescanAllLibrary,
// )
// HorizontalDivider()
DropdownMenuItem( DropdownMenuItem(
leadingIcon = { leadingIcon = {
Checkbox(checked = autoScanDownloads, onCheckedChange = null) Checkbox(checked = autoScanDownloads, onCheckedChange = null)
@ -282,12 +313,19 @@ internal fun LibraryScreen(
val next = runCatching { val next = runCatching {
val bytes = openLibraryBook(item.fileId) ?: error("Book file is not available.") val bytes = openLibraryBook(item.fileId) ?: error("Book file is not available.")
val book = Fb2Format.parse(bytes, item.storageUri ?: item.title) 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) markLibraryReadingStatus(item.fileId, BookReadingStatus.READING)
saveActiveReadingFileId(item.fileId) saveActiveReadingFileId(item.fileId)
AppState.Reader( AppState.Reader(
fileId = item.fileId, fileId = item.fileId,
book = book, book = book,
libraryItems = visibleItems, libraryItems = readerLibraryItems,
scanPath = state.scanPath, scanPath = state.scanPath,
message = message, message = message,
) )
@ -639,12 +677,14 @@ private fun LibraryRow(
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
Text( Text(
item.authors.joinToString().ifBlank { "Unknown author" }, item.authors.joinToString().ifBlank { "Unknown author" },
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
Text( Text(
item.libraryMetadataLine(), item.libraryMetadataLine(),
@ -791,6 +831,9 @@ private fun Long.formatBytes(): String =
else -> "$this B" else -> "$this B"
} }
private fun List<LibraryItem>.replaceLibraryItem(item: LibraryItem): List<LibraryItem> =
map { current -> if (current.fileId == item.fileId) item else current }
private fun LibraryScanProgress.toCatalogScanMessage(): String { private fun LibraryScanProgress.toCatalogScanMessage(): String {
val total = totalFiles ?: return "Scanned $scannedFiles books" val total = totalFiles ?: return "Scanned $scannedFiles books"
val percent = if (total <= 0) { val percent = if (total <= 0) {

View File

@ -0,0 +1,10 @@
package net.sergeych.toread
import androidx.compose.runtime.Composable
@Composable
internal expect fun PlatformBackHandler(
enabled: Boolean,
navigationDepth: Int,
onBack: () -> Unit,
)

View File

@ -3,6 +3,9 @@ package net.sergeych.toread
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.toComposeImageBitmap import androidx.compose.ui.graphics.toComposeImageBitmap
import net.sergeych.toread.fb2.Fb2Binary 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.BookReadingStatus
import net.sergeych.toread.storage.ContentAnchor import net.sergeych.toread.storage.ContentAnchor
import net.sergeych.toread.storage.LibraryFileRecord 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) { actual suspend fun deleteLibraryItem(fileId: String): Boolean = withContext(Dispatchers.IO) {
appendLibraryLog("delete fileId=$fileId") appendLibraryLog("delete fileId=$fileId")
openLibraryDatabase().useLibrary { db -> openLibraryDatabase().useLibrary { db ->
@ -433,6 +475,58 @@ private fun LibraryFileRecord.toLibraryItem(): LibraryItem =
importedAt = importedAt, 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 = private fun libraryLogFile(): File =
File(System.getProperty("user.home"), ".toread/toread.log") File(System.getProperty("user.home"), ".toread/toread.log")

View File

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

View File

@ -38,6 +38,11 @@ actual suspend fun scanLibrarySubtree(
actual suspend fun openLibraryBook(fileId: String): ByteArray? = null 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 deleteLibraryItem(fileId: String): Boolean = false
actual suspend fun loadLibraryReadingPosition(fileId: String): ReadingPosition? = null actual suspend fun loadLibraryReadingPosition(fileId: String): ReadingPosition? = null

View File

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