better images and correct library re-sorting

This commit is contained in:
Sergey Chernov 2026-05-24 18:42:52 +03:00
parent 89b1115169
commit 17c885b3f5
4 changed files with 125 additions and 51 deletions

View File

@ -46,6 +46,11 @@ private data class PendingLibraryDelete(
val restore: () -> Unit, val restore: () -> Unit,
) )
internal data class LibraryItemRefreshRequest(
val id: Long,
val fileId: String,
)
@Composable @Composable
@Preview @Preview
fun App() { fun App() {
@ -165,15 +170,23 @@ private fun BookReaderApp(
) -> Unit, ) -> Unit,
) { ) {
var state by remember { mutableStateOf<AppState>(AppState.LoadingStartup) } var state by remember { mutableStateOf<AppState>(AppState.LoadingStartup) }
var libraryBackState by remember { mutableStateOf<AppState.Library?>(null) }
var activeScan by remember { mutableStateOf<LibraryScanProgress?>(null) } var activeScan by remember { mutableStateOf<LibraryScanProgress?>(null) }
var scanJob by remember { mutableStateOf<Job?>(null) } var scanJob by remember { mutableStateOf<Job?>(null) }
var pendingDelete by remember { mutableStateOf<PendingLibraryDelete?>(null) } var pendingDelete by remember { mutableStateOf<PendingLibraryDelete?>(null) }
var pendingDeleteJob by remember { mutableStateOf<Job?>(null) } var pendingDeleteJob by remember { mutableStateOf<Job?>(null) }
var hiddenDeletedFileIds by remember { mutableStateOf<Set<String>>(emptySet()) } var hiddenDeletedFileIds by remember { mutableStateOf<Set<String>>(emptySet()) }
var nextDeleteId by remember { mutableStateOf(0L) } var nextDeleteId by remember { mutableStateOf(0L) }
var nextLibraryItemRefreshId by remember { mutableStateOf(0L) }
var libraryItemRefreshRequest by remember { mutableStateOf<LibraryItemRefreshRequest?>(null) }
var imageViewer by remember { mutableStateOf<ViewedBookImage?>(null) } var imageViewer by remember { mutableStateOf<ViewedBookImage?>(null) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
fun refreshLibraryItem(fileId: String) {
nextLibraryItemRefreshId += 1
libraryItemRefreshRequest = LibraryItemRefreshRequest(nextLibraryItemRefreshId, fileId)
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
state = loadStartupState() state = loadStartupState()
} }
@ -279,7 +292,11 @@ private fun BookReaderApp(
) )
is AppState.Reader -> { is AppState.Reader -> {
scope.launch { saveActiveReadingFileId(null) } 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.Scan -> AppState.Library(current.items, current.scanPath, current.message)
is AppState.Error -> AppState.LoadingStartup is AppState.Error -> AppState.LoadingStartup
@ -340,17 +357,37 @@ private fun BookReaderApp(
} }
} }
when (val current = state) { Box(Modifier.fillMaxSize()) {
AppState.LoadingStartup -> LoadingScreen(strings.loadingOpeningBook) val currentLibraryState = when (val current = state) {
is AppState.Library -> LibraryScreen( is AppState.Library -> current
state = current, is AppState.Reader, is AppState.BookInfo -> libraryBackState
else -> null
}
currentLibraryState?.let { libraryState ->
LibraryScreen(
state = libraryState,
activeScan = activeScan, activeScan = activeScan,
itemRefreshRequest = libraryItemRefreshRequest,
hiddenFileIds = hiddenDeletedFileIds + pendingDelete?.request?.fileId?.let(::setOf).orEmpty(), hiddenFileIds = hiddenDeletedFileIds + pendingDelete?.request?.fileId?.let(::setOf).orEmpty(),
onStateChange = { state = it }, onStateChange = { next ->
onNavigateToScan = { state = AppState.Scan(current.items, current.scanPath, current.message) }, 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, onStartScan = ::startScan,
onDeleteRequested = ::requestDelete, onDeleteRequested = ::requestDelete,
) )
}
when (val current = state) {
AppState.LoadingStartup -> LoadingScreen(strings.loadingOpeningBook)
is AppState.Library -> Unit
is AppState.Scan -> ScanScreen( is AppState.Scan -> ScanScreen(
state = current, state = current,
activeScan = activeScan, activeScan = activeScan,
@ -365,6 +402,7 @@ private fun BookReaderApp(
book = current.book, book = current.book,
onImageOpen = { imageViewer = it }, onImageOpen = { imageViewer = it },
onThemeToggle = onThemeToggle, onThemeToggle = onThemeToggle,
onBookChanged = ::refreshLibraryItem,
onBookInfo = { onBookInfo = {
state = AppState.BookInfo( state = AppState.BookInfo(
fileId = current.fileId, fileId = current.fileId,
@ -375,7 +413,9 @@ private fun BookReaderApp(
) )
}, },
onDeleted = { message -> onDeleted = { message ->
state = AppState.Library(emptyList(), current.scanPath, message) state = libraryBackState?.copy(message = message)
?: AppState.Library(emptyList(), current.scanPath, message)
libraryBackState = null
}, },
onDeleteRequested = ::requestDelete, onDeleteRequested = ::requestDelete,
onBack = ::navigateBack, onBack = ::navigateBack,
@ -388,6 +428,7 @@ private fun BookReaderApp(
) )
is AppState.Error -> ErrorScreen(current.message, onBack = ::navigateBack) is AppState.Error -> ErrorScreen(current.message, onBack = ::navigateBack)
} }
}
imageViewer?.let { image -> imageViewer?.let { image ->
ImageViewer( ImageViewer(

View File

@ -82,6 +82,7 @@ import kotlinx.coroutines.launch
internal fun LibraryScreen( internal fun LibraryScreen(
state: AppState.Library, state: AppState.Library,
activeScan: LibraryScanProgress?, activeScan: LibraryScanProgress?,
itemRefreshRequest: LibraryItemRefreshRequest?,
hiddenFileIds: Set<String>, hiddenFileIds: Set<String>,
onStateChange: (AppState) -> Unit, onStateChange: (AppState) -> Unit,
onNavigateToScan: () -> Unit, onNavigateToScan: () -> Unit,
@ -147,6 +148,13 @@ internal fun LibraryScreen(
recentlyAddedItems = loadRecentlyAddedLibraryItems(since, RecentlyAddedLimit) 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) { fun refresh(nextMessage: String? = message) {
message = nextMessage message = nextMessage
scope.launch { scope.launch {
@ -247,6 +255,10 @@ internal fun LibraryScreen(
} }
} }
LaunchedEffect(itemRefreshRequest) {
itemRefreshRequest?.let { refreshLibraryItem(it.fileId) }
}
LaunchedEffect(searchText) { LaunchedEffect(searchText) {
val query = searchText val query = searchText
if (query.isBlank()) { if (query.isBlank()) {

View File

@ -79,6 +79,7 @@ import androidx.compose.ui.text.style.LineBreak
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isSpecified import androidx.compose.ui.unit.isSpecified
import androidx.compose.ui.unit.sp 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.fb2.Fb2TextStyle
import net.sergeych.toread.text.HyphenationRegistry import net.sergeych.toread.text.HyphenationRegistry
import net.sergeych.toread.text.SoftHyphen import net.sergeych.toread.text.SoftHyphen
import kotlin.math.min
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -117,7 +117,7 @@ internal fun ContinuousBookReader(
val hyphenation = remember { HyphenationRegistry() } val hyphenation = remember { HyphenationRegistry() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val textLineMetricsByItem = remember(contentPlan) { mutableStateMapOf<Int, TextLineMetrics>() } val textLineMetricsByItem = remember(contentPlan) { mutableStateMapOf<Int, TextLineMetrics>() }
val contentPadding = PaddingValues(6.dp) val contentPadding = PaddingValues(top=6.dp, bottom = 6.dp, start = 0.dp, end = 6.dp)
val userScrollConnection = remember(onUserScroll) { val userScrollConnection = remember(onUserScroll) {
object : NestedScrollConnection { object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
@ -945,6 +945,13 @@ private val isDesktopPlatform: Boolean by lazy {
getPlatform().name.startsWith("Java") 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 { private fun TextLayoutResult.endsAtSoftHyphen(text: String, line: Int): Boolean {
val end = getLineEnd(line, visibleEnd = false) val end = getLineEnd(line, visibleEnd = false)
return text.getOrNull(end - 1) == SoftHyphen || text.getOrNull(end) == SoftHyphen 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 imageAspectRatio = bitmap.width.toFloat() / bitmap.height.coerceAtLeast(1).toFloat()
val imageModifier = if (fitBackgroundToBitmapBounds) { val imageModifier = if (fitBackgroundToBitmapBounds) {
val bitmapWidth = with(density) { bitmap.width.toDp() } 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 Modifier
.width(if (bitmapWidth < maxWidth) bitmapWidth else maxWidth) .width(displayWidth)
.aspectRatio(imageAspectRatio) .aspectRatio(imageAspectRatio)
} else { } else {
Modifier.fillMaxSize() Modifier.fillMaxSize()

View File

@ -66,6 +66,7 @@ internal fun BookView(
book: Fb2Book, book: Fb2Book,
onImageOpen: (ViewedBookImage) -> Unit, onImageOpen: (ViewedBookImage) -> Unit,
onThemeToggle: () -> Unit, onThemeToggle: () -> Unit,
onBookChanged: (String) -> Unit,
onBookInfo: () -> Unit, onBookInfo: () -> Unit,
onDeleted: (String?) -> Unit, onDeleted: (String?) -> Unit,
onDeleteRequested: ( onDeleteRequested: (
@ -110,6 +111,7 @@ internal fun BookView(
scope.launch { scope.launch {
if (markLibraryReadingStatus(fileId, status)) { if (markLibraryReadingStatus(fileId, status)) {
libraryItem = loadLibraryItem(fileId) ?: libraryItem?.copy(readingStatus = status) libraryItem = loadLibraryItem(fileId) ?: libraryItem?.copy(readingStatus = status)
onBookChanged(fileId)
if (status == BookReadingStatus.READ) markedRead = true if (status == BookReadingStatus.READ) markedRead = true
if (status == BookReadingStatus.NEW) markedRead = false if (status == BookReadingStatus.NEW) markedRead = false
if (status == BookReadingStatus.NOT_INTERESTED) { if (status == BookReadingStatus.NOT_INTERESTED) {
@ -134,6 +136,7 @@ internal fun BookView(
markedRead = item?.readingStatus == BookReadingStatus.READ markedRead = item?.readingStatus == BookReadingStatus.READ
if (item?.readingStatus?.shouldBecomeReadingOnOpen() == true && markLibraryReadingStatus(fileId, BookReadingStatus.READING)) { if (item?.readingStatus?.shouldBecomeReadingOnOpen() == true && markLibraryReadingStatus(fileId, BookReadingStatus.READING)) {
libraryItem = loadLibraryItem(fileId) ?: item.copy(readingStatus = BookReadingStatus.READING) libraryItem = loadLibraryItem(fileId) ?: item.copy(readingStatus = BookReadingStatus.READING)
onBookChanged(fileId)
} }
} }
@ -182,6 +185,7 @@ internal fun BookView(
markedRead = true markedRead = true
if (markLibraryReadingStatus(fileId, BookReadingStatus.READ)) { if (markLibraryReadingStatus(fileId, BookReadingStatus.READ)) {
libraryItem = loadLibraryItem(fileId) ?: libraryItem?.copy(readingStatus = BookReadingStatus.READ) libraryItem = loadLibraryItem(fileId) ?: libraryItem?.copy(readingStatus = BookReadingStatus.READ)
onBookChanged(fileId)
} }
} }
} }
@ -230,6 +234,7 @@ internal fun BookView(
scope.launch { scope.launch {
if (markLibraryFavorite(fileId, favorite)) { if (markLibraryFavorite(fileId, favorite)) {
libraryItem = loadLibraryItem(fileId) ?: libraryItem?.copy(favorite = favorite) libraryItem = loadLibraryItem(fileId) ?: libraryItem?.copy(favorite = favorite)
onBookChanged(fileId)
showMessage(if (favorite) strings.addedToFavorites() else strings.removedFromFavorites()) showMessage(if (favorite) strings.addedToFavorites() else strings.removedFromFavorites())
} else { } else {
showMessage(strings.couldNotUpdateBook) showMessage(strings.couldNotUpdateBook)