From 17c885b3f5f41ab7e8b1b8ec30cfbb0e7639b667 Mon Sep 17 00:00:00 2001 From: sergeych Date: Sun, 24 May 2026 18:42:52 +0300 Subject: [PATCH] better images and correct library re-sorting --- .../kotlin/net/sergeych/toread/App.kt | 137 ++++++++++++------ .../net/sergeych/toread/LibraryScreen.kt | 12 ++ .../net/sergeych/toread/ReaderContent.kt | 22 ++- .../net/sergeych/toread/ReaderScreen.kt | 5 + 4 files changed, 125 insertions(+), 51 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/net/sergeych/toread/App.kt b/composeApp/src/commonMain/kotlin/net/sergeych/toread/App.kt index fc811ac..6bd5567 100644 --- a/composeApp/src/commonMain/kotlin/net/sergeych/toread/App.kt +++ b/composeApp/src/commonMain/kotlin/net/sergeych/toread/App.kt @@ -46,6 +46,11 @@ private data class PendingLibraryDelete( val restore: () -> Unit, ) +internal data class LibraryItemRefreshRequest( + val id: Long, + val fileId: String, +) + @Composable @Preview fun App() { @@ -165,15 +170,23 @@ private fun BookReaderApp( ) -> Unit, ) { var state by remember { mutableStateOf(AppState.LoadingStartup) } + var libraryBackState by remember { mutableStateOf(null) } var activeScan by remember { mutableStateOf(null) } var scanJob by remember { mutableStateOf(null) } var pendingDelete by remember { mutableStateOf(null) } var pendingDeleteJob by remember { mutableStateOf(null) } var hiddenDeletedFileIds by remember { mutableStateOf>(emptySet()) } var nextDeleteId by remember { mutableStateOf(0L) } + var nextLibraryItemRefreshId by remember { mutableStateOf(0L) } + var libraryItemRefreshRequest by remember { mutableStateOf(null) } var imageViewer by remember { mutableStateOf(null) } val scope = rememberCoroutineScope() + fun refreshLibraryItem(fileId: String) { + nextLibraryItemRefreshId += 1 + libraryItemRefreshRequest = LibraryItemRefreshRequest(nextLibraryItemRefreshId, fileId) + } + LaunchedEffect(Unit) { state = loadStartupState() } @@ -279,7 +292,11 @@ private fun BookReaderApp( ) is AppState.Reader -> { scope.launch { saveActiveReadingFileId(null) } - AppState.Library(current.libraryItems, current.scanPath, current.message) + refreshLibraryItem(current.fileId) + val backState = libraryBackState?.copy(message = current.message) + ?: AppState.Library(current.libraryItems, current.scanPath, current.message) + libraryBackState = null + backState } is AppState.Scan -> AppState.Library(current.items, current.scanPath, current.message) is AppState.Error -> AppState.LoadingStartup @@ -340,53 +357,77 @@ private fun BookReaderApp( } } - when (val current = state) { - AppState.LoadingStartup -> LoadingScreen(strings.loadingOpeningBook) - is AppState.Library -> LibraryScreen( - state = current, - activeScan = activeScan, - hiddenFileIds = hiddenDeletedFileIds + pendingDelete?.request?.fileId?.let(::setOf).orEmpty(), - onStateChange = { state = it }, - onNavigateToScan = { state = AppState.Scan(current.items, current.scanPath, current.message) }, - onStartScan = ::startScan, - onDeleteRequested = ::requestDelete, - ) - is AppState.Scan -> ScanScreen( - state = current, - activeScan = activeScan, - onStateChange = { state = it }, - onStartScan = { path -> - startScan(path) - state = AppState.Library(current.items, path, strings.scanning) - }, - ) - is AppState.Reader -> BookView( - fileId = current.fileId, - book = current.book, - onImageOpen = { imageViewer = it }, - onThemeToggle = onThemeToggle, - onBookInfo = { - state = AppState.BookInfo( - fileId = current.fileId, - book = current.book, - libraryItems = current.libraryItems, - scanPath = current.scanPath, - message = current.message, - ) - }, - onDeleted = { message -> - state = AppState.Library(emptyList(), current.scanPath, message) - }, - onDeleteRequested = ::requestDelete, - onBack = ::navigateBack, - ) - is AppState.BookInfo -> BookInfoScreen( - fileId = current.fileId, - book = current.book, - onImageOpen = { imageViewer = it }, - onBack = ::navigateBack, - ) - is AppState.Error -> ErrorScreen(current.message, onBack = ::navigateBack) + Box(Modifier.fillMaxSize()) { + val currentLibraryState = when (val current = state) { + is AppState.Library -> current + is AppState.Reader, is AppState.BookInfo -> libraryBackState + else -> null + } + currentLibraryState?.let { libraryState -> + LibraryScreen( + state = libraryState, + activeScan = activeScan, + itemRefreshRequest = libraryItemRefreshRequest, + hiddenFileIds = hiddenDeletedFileIds + pendingDelete?.request?.fileId?.let(::setOf).orEmpty(), + onStateChange = { next -> + val currentState = state + if (next is AppState.Reader && currentState is AppState.Library) { + libraryBackState = currentState + } + state = next + }, + onNavigateToScan = { + libraryBackState = null + state = AppState.Scan(libraryState.items, libraryState.scanPath, libraryState.message) + }, + onStartScan = ::startScan, + onDeleteRequested = ::requestDelete, + ) + } + + when (val current = state) { + AppState.LoadingStartup -> LoadingScreen(strings.loadingOpeningBook) + is AppState.Library -> Unit + is AppState.Scan -> ScanScreen( + state = current, + activeScan = activeScan, + onStateChange = { state = it }, + onStartScan = { path -> + startScan(path) + state = AppState.Library(current.items, path, strings.scanning) + }, + ) + is AppState.Reader -> BookView( + fileId = current.fileId, + book = current.book, + onImageOpen = { imageViewer = it }, + onThemeToggle = onThemeToggle, + onBookChanged = ::refreshLibraryItem, + onBookInfo = { + state = AppState.BookInfo( + fileId = current.fileId, + book = current.book, + libraryItems = current.libraryItems, + scanPath = current.scanPath, + message = current.message, + ) + }, + onDeleted = { message -> + state = libraryBackState?.copy(message = message) + ?: AppState.Library(emptyList(), current.scanPath, message) + libraryBackState = null + }, + onDeleteRequested = ::requestDelete, + onBack = ::navigateBack, + ) + is AppState.BookInfo -> BookInfoScreen( + fileId = current.fileId, + book = current.book, + onImageOpen = { imageViewer = it }, + onBack = ::navigateBack, + ) + is AppState.Error -> ErrorScreen(current.message, onBack = ::navigateBack) + } } imageViewer?.let { image -> diff --git a/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryScreen.kt b/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryScreen.kt index 0b0bd53..cce757f 100644 --- a/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryScreen.kt +++ b/composeApp/src/commonMain/kotlin/net/sergeych/toread/LibraryScreen.kt @@ -82,6 +82,7 @@ import kotlinx.coroutines.launch internal fun LibraryScreen( state: AppState.Library, activeScan: LibraryScanProgress?, + itemRefreshRequest: LibraryItemRefreshRequest?, hiddenFileIds: Set, onStateChange: (AppState) -> Unit, onNavigateToScan: () -> Unit, @@ -147,6 +148,13 @@ internal fun LibraryScreen( recentlyAddedItems = loadRecentlyAddedLibraryItems(since, RecentlyAddedLimit) } + suspend fun refreshLibraryItem(fileId: String) { + val updatedItem = loadLibraryItem(fileId) ?: return + items = items.replaceLibraryItem(updatedItem) + searchResults = searchResults.replaceLibraryItem(updatedItem) + recentlyAddedItems = recentlyAddedItems.replaceLibraryItem(updatedItem) + } + fun refresh(nextMessage: String? = message) { message = nextMessage scope.launch { @@ -247,6 +255,10 @@ internal fun LibraryScreen( } } + LaunchedEffect(itemRefreshRequest) { + itemRefreshRequest?.let { refreshLibraryItem(it.fileId) } + } + LaunchedEffect(searchText) { val query = searchText if (query.isBlank()) { diff --git a/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderContent.kt b/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderContent.kt index 2917118..9fa5bf3 100644 --- a/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderContent.kt +++ b/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderContent.kt @@ -79,6 +79,7 @@ import androidx.compose.ui.text.style.LineBreak import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.isSpecified import androidx.compose.ui.unit.sp @@ -96,7 +97,6 @@ import net.sergeych.toread.fb2.Fb2TextSpan import net.sergeych.toread.fb2.Fb2TextStyle import net.sergeych.toread.text.HyphenationRegistry import net.sergeych.toread.text.SoftHyphen -import kotlin.math.min import kotlinx.coroutines.launch import kotlin.math.max import kotlin.math.min @@ -117,7 +117,7 @@ internal fun ContinuousBookReader( val hyphenation = remember { HyphenationRegistry() } val scope = rememberCoroutineScope() val textLineMetricsByItem = remember(contentPlan) { mutableStateMapOf() } - val contentPadding = PaddingValues(6.dp) + val contentPadding = PaddingValues(top=6.dp, bottom = 6.dp, start = 0.dp, end = 6.dp) val userScrollConnection = remember(onUserScroll) { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { @@ -945,6 +945,13 @@ private val isDesktopPlatform: Boolean by lazy { getPlatform().name.startsWith("Java") } +private val MinimumBookImageMaxDimension = 800.dp + +private fun minimumBookImageMaxDimension(availableWidth: Dp): Dp { + val relativeMinimum = availableWidth * 0.55f + return if (MinimumBookImageMaxDimension < relativeMinimum) MinimumBookImageMaxDimension else relativeMinimum +} + private fun TextLayoutResult.endsAtSoftHyphen(text: String, line: Int): Boolean { val end = getLineEnd(line, visibleEnd = false) return text.getOrNull(end - 1) == SoftHyphen || text.getOrNull(end) == SoftHyphen @@ -996,8 +1003,17 @@ private fun BookImage( val imageAspectRatio = bitmap.width.toFloat() / bitmap.height.coerceAtLeast(1).toFloat() val imageModifier = if (fitBackgroundToBitmapBounds) { val bitmapWidth = with(density) { bitmap.width.toDp() } + val bitmapHeight = with(density) { bitmap.height.toDp() } + val bitmapMaxDimension = if (bitmapWidth > bitmapHeight) bitmapWidth else bitmapHeight + val minimumMaxDimension = minimumBookImageMaxDimension(maxWidth) + val displayWidth = when { + bitmapMaxDimension < minimumMaxDimension -> + bitmapWidth * (minimumMaxDimension.value / bitmapMaxDimension.value) + bitmapWidth < maxWidth -> bitmapWidth + else -> maxWidth + } Modifier - .width(if (bitmapWidth < maxWidth) bitmapWidth else maxWidth) + .width(displayWidth) .aspectRatio(imageAspectRatio) } else { Modifier.fillMaxSize() diff --git a/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderScreen.kt b/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderScreen.kt index ada3f1b..2682a01 100644 --- a/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderScreen.kt +++ b/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderScreen.kt @@ -66,6 +66,7 @@ internal fun BookView( book: Fb2Book, onImageOpen: (ViewedBookImage) -> Unit, onThemeToggle: () -> Unit, + onBookChanged: (String) -> Unit, onBookInfo: () -> Unit, onDeleted: (String?) -> Unit, onDeleteRequested: ( @@ -110,6 +111,7 @@ internal fun BookView( scope.launch { if (markLibraryReadingStatus(fileId, status)) { libraryItem = loadLibraryItem(fileId) ?: libraryItem?.copy(readingStatus = status) + onBookChanged(fileId) if (status == BookReadingStatus.READ) markedRead = true if (status == BookReadingStatus.NEW) markedRead = false if (status == BookReadingStatus.NOT_INTERESTED) { @@ -134,6 +136,7 @@ internal fun BookView( markedRead = item?.readingStatus == BookReadingStatus.READ if (item?.readingStatus?.shouldBecomeReadingOnOpen() == true && markLibraryReadingStatus(fileId, BookReadingStatus.READING)) { libraryItem = loadLibraryItem(fileId) ?: item.copy(readingStatus = BookReadingStatus.READING) + onBookChanged(fileId) } } @@ -182,6 +185,7 @@ internal fun BookView( markedRead = true if (markLibraryReadingStatus(fileId, BookReadingStatus.READ)) { libraryItem = loadLibraryItem(fileId) ?: libraryItem?.copy(readingStatus = BookReadingStatus.READ) + onBookChanged(fileId) } } } @@ -230,6 +234,7 @@ internal fun BookView( scope.launch { if (markLibraryFavorite(fileId, favorite)) { libraryItem = loadLibraryItem(fileId) ?: libraryItem?.copy(favorite = favorite) + onBookChanged(fileId) showMessage(if (favorite) strings.addedToFavorites() else strings.removedFromFavorites()) } else { showMessage(strings.couldNotUpdateBook)