diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 8af3700ca..c573c8363 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -223,6 +223,16 @@ class ReaderActivity : BaseActivity() { ReaderViewModel.Event.ReloadViewerChapters -> { viewModel.state.value.viewerChapters?.let(::setChapters) } + ReaderViewModel.Event.RefreshCurrentTextPage -> { + viewModel.state.value.viewer?.refreshCurrentPage() + } + ReaderViewModel.Event.ReloadTextViewerChapters -> { + val state = viewModel.state.value + val chapters = state.viewerChapters + if (chapters != null) { + state.viewer?.refreshTextChapters(chapters) + } + } ReaderViewModel.Event.PageChanged -> { displayRefreshHost.flash() } @@ -254,7 +264,8 @@ class ReaderActivity : BaseActivity() { readerState = viewModel.state, onChangeReadingMode = viewModel::setMangaReadingMode, onChangeOrientation = viewModel::setMangaOrientationType, - onTextSettingsChanged = viewModel::reloadTextReaderStyle, + onTextSettingsPreview = viewModel::previewTextReaderStyle, + onTextSettingsCommit = viewModel::commitTextReaderStyle, preferences = readerPreferences, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index 34d9c64dd..5149301f9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -40,6 +40,7 @@ import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader import eu.kanade.tachiyomi.ui.reader.loader.DownloadPageLoader +import eu.kanade.tachiyomi.ui.reader.loader.TextReaderPage import eu.kanade.tachiyomi.ui.reader.model.InsertPage import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage @@ -56,6 +57,7 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.cacheImageDir import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -90,7 +92,6 @@ import tachiyomi.domain.source.service.SourceManager import tachiyomi.source.local.image.LocalCoverManager import tachiyomi.source.local.isLocal import java.util.Date -import java.util.concurrent.atomic.AtomicBoolean import kotlin.math.roundToInt import kotlin.time.Clock @@ -824,41 +825,36 @@ class ReaderViewModel( mutableState.update { it.copy(dialog = null) } } - private val textReaderStyleReloading = AtomicBoolean(false) - private val textReaderStyleReloadPending = AtomicBoolean(false) - - fun reloadTextReaderStyle() { + fun previewTextReaderStyle() { val chapter = state.value.currentChapter ?: return if (chapter.pageLoader?.usesTextReaderStyle != true) return + val style = readerPreferences.textReaderStyle() + val textPages = chapter.pages?.filterIsInstance().orEmpty() + if (textPages.isEmpty()) return + + textPages.forEach { it.updateStyle(style) } + eventChannel.trySend(Event.RefreshCurrentTextPage) + } + + fun commitTextReaderStyle() { + val chapter = state.value.currentChapter ?: return + if (chapter.pageLoader?.usesTextReaderStyle != true) return + if (chapter.pages?.any { it is TextReaderPage } != true) return val chapterLoader = loader ?: return - - if (!textReaderStyleReloading.compareAndSet(false, true)) { - textReaderStyleReloadPending.set(true) - return - } - val requestedPage = (state.value.currentPage - 1).coerceAtLeast(0) val oldLastIndex = (chapter.pages?.lastIndex ?: 0).coerceAtLeast(0) val readingProgress = if (oldLastIndex == 0) 0f else requestedPage.toFloat() / oldLastIndex viewModelScope.launchIO { try { - chapter.pageLoader?.recycle() - chapter.pageLoader = null - chapter.state = ReaderChapter.State.Wait - chapter.requestedPage = requestedPage - chapterLoader.loadChapter(chapter) - val newLastIndex = (chapter.pages?.lastIndex ?: 0).coerceAtLeast(0) - chapter.requestedPage = (readingProgress * newLastIndex).roundToInt().coerceIn(0, newLastIndex) - eventChannel.send(Event.ReloadViewerChapters) + val oldLoader = chapterLoader.reloadTextChapter(chapter, readingProgress) ?: return@launchIO + eventChannel.send(Event.ReloadTextViewerChapters) + // Give old visible holders time to detach before closing resources such as EpubReader. + delay(2000) + oldLoader.recycle() } catch (e: Throwable) { if (e is CancellationException) throw e - logcat(LogPriority.ERROR, e) { "Failed to reload text reader style" } - } finally { - textReaderStyleReloading.set(false) - if (textReaderStyleReloadPending.getAndSet(false)) { - reloadTextReaderStyle() - } + logcat(LogPriority.ERROR, e) { "Failed to repaginate text reader style" } } } } @@ -1052,6 +1048,8 @@ class ReaderViewModel( sealed interface Event { data object ReloadViewerChapters : Event + data object RefreshCurrentTextPage : Event + data object ReloadTextViewerChapters : Event data object PageChanged : Event data class SetOrientation(val orientation: Int) : Event data class SetCoverResult(val result: SetAsCoverResult) : Event diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt index 210e200cd..b824be5d8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt @@ -22,6 +22,7 @@ import tachiyomi.domain.source.model.StubSource import tachiyomi.i18n.MR import tachiyomi.source.local.LocalSource import tachiyomi.source.local.io.Format +import kotlin.math.roundToInt /** * Loader used to retrieve the [PageLoader] for a given chapter. @@ -73,6 +74,36 @@ class ChapterLoader( } } + + suspend fun reloadTextChapter(chapter: ReaderChapter, readingProgress: Float): PageLoader? { + return withIOContext { + val newLoader = getPageLoader(chapter) + if (!newLoader.usesTextReaderStyle) { + newLoader.recycle() + return@withIOContext null + } + + try { + val pages = newLoader.getPages() + .onEach { it.chapter = chapter } + if (pages.isEmpty()) { + throw Exception(context.stringResource(MR.strings.page_list_empty_error)) + } + + val oldLoader = chapter.pageLoader + chapter.pageLoader = newLoader + chapter.requestedPage = (readingProgress * pages.lastIndex) + .roundToInt() + .coerceIn(0, pages.lastIndex) + chapter.state = ReaderChapter.State.Loaded(pages) + oldLoader + } catch (e: Throwable) { + newLoader.recycle() + throw e + } + } + } + /** * Checks [chapter] to be loaded based on present pages and loader in addition to state. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt index 0e7d12a92..87f0c2028 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt @@ -26,10 +26,7 @@ internal class EpubPageLoader( if (textPages.isNotEmpty()) { return textPages.mapIndexed { index, text -> - ReaderPage(index).apply { - stream = { TextPageRenderer.render(text, style) } - status = Page.State.Ready - } + TextReaderPage(index, text, style) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/TextPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/TextPageLoader.kt index 37e0dca6d..ec162c821 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/TextPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/TextPageLoader.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.ui.reader.loader import com.hippo.unifile.UniFile -import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.setting.TextReaderStyle import java.nio.ByteBuffer @@ -22,10 +21,7 @@ internal class TextPageLoader( val bytes = file.openInputStream().use { it.readBytes() } val text = decodeText(bytes) return TextPageRenderer.paginate(text, style).mapIndexed { index, pageText -> - ReaderPage(index).apply { - stream = { TextPageRenderer.render(pageText, style) } - status = Page.State.Ready - } + TextReaderPage(index, pageText, style) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/TextReaderPage.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/TextReaderPage.kt new file mode 100644 index 000000000..dbf552bd3 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/TextReaderPage.kt @@ -0,0 +1,29 @@ +package eu.kanade.tachiyomi.ui.reader.loader + +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.ui.reader.model.ReaderPage +import eu.kanade.tachiyomi.ui.reader.setting.TextReaderStyle + +/** + * A rendered text page whose style can be changed without rebuilding the chapter. + * Page boundaries remain stable during the live preview; a full repagination is done + * once when the settings dialog is dismissed. + */ +internal class TextReaderPage( + index: Int, + private val pageText: String, + initialStyle: TextReaderStyle, +) : ReaderPage(index) { + + @Volatile + private var style: TextReaderStyle = initialStyle + + init { + stream = { TextPageRenderer.render(pageText, style) } + status = Page.State.Ready + } + + fun updateStyle(newStyle: TextReaderStyle) { + style = newStyle + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsViewModel.kt index be4ccb8a5..6f907a921 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsViewModel.kt @@ -16,7 +16,8 @@ class ReaderSettingsViewModel( readerState: StateFlow, val onChangeReadingMode: (ReadingMode) -> Unit, val onChangeOrientation: (ReaderOrientation) -> Unit, - private val onTextSettingsChanged: () -> Unit, + private val onTextSettingsPreview: () -> Unit, + private val onTextSettingsCommit: () -> Unit, val preferences: ReaderPreferences, ) : ViewModel() { @@ -31,20 +32,24 @@ class ReaderSettingsViewModel( .stateIn(viewModelScope, SharingStarted.Lazily, null) private var textSettingsReloadJob: Job? = null + private var textSettingsDirty = false fun requestTextSettingsReload() { + textSettingsDirty = true textSettingsReloadJob?.cancel() textSettingsReloadJob = viewModelScope.launch { - delay(150) - onTextSettingsChanged() + delay(100) + onTextSettingsPreview() textSettingsReloadJob = null } } fun flushTextSettingsReload() { - if (textSettingsReloadJob == null) return + if (!textSettingsDirty) return textSettingsReloadJob?.cancel() textSettingsReloadJob = null - onTextSettingsChanged() + onTextSettingsPreview() + textSettingsDirty = false + onTextSettingsCommit() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/Viewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/Viewer.kt index 00834563c..56bfd051b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/Viewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/Viewer.kt @@ -26,6 +26,14 @@ interface Viewer { */ fun setChapters(chapters: ViewerChapters) + /** Rebinds only the currently visible page(s), used for lightweight text-style preview. */ + fun refreshCurrentPage() {} + + /** Replaces repaginated text pages without expensive per-item diffing where possible. */ + fun refreshTextChapters(chapters: ViewerChapters) { + setChapters(chapters) + } + /** * Tells this viewer to move to the given [page]. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt index 3bd7feaef..6475da1de 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt @@ -380,6 +380,17 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer { pager.setCurrentItem(currentItem, false) } + override fun refreshCurrentPage() { + refreshAdapter() + } + + override fun refreshTextChapters(chapters: ViewerChapters) { + setChaptersInternal(chapters) + val pages = chapters.currChapter.pages ?: return + val page = pages[min(chapters.currChapter.requestedPage, pages.lastIndex)] + moveToPage(page) + } + /** * Called from the containing activity when a key [event] is received. It should return true * if the event was handled, false otherwise. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt index 29727f696..283abf4af 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt @@ -35,7 +35,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter() // Forces chapter transition if there is missing chapters @@ -65,13 +65,18 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter) { - val result = DiffUtil.calculateDiff(Callback(items, newItems)) - items = newItems - result.dispatchUpdatesTo(this) + private fun updateItems(newItems: List, useDiff: Boolean) { + if (useDiff) { + val result = DiffUtil.calculateDiff(Callback(items, newItems)) + items = newItems + result.dispatchUpdatesTo(this) + } else { + items = newItems + notifyDataSetChanged() + } } fun refresh() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt index 228145a66..58db7adb6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt @@ -353,6 +353,19 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr min(position + 3, adapter.itemCount - 1), ) } + + override fun refreshCurrentPage() { + refreshAdapter() + } + + override fun refreshTextChapters(chapters: ViewerChapters) { + val forceTransition = config.alwaysShowChapterTransition || currentPage is ChapterTransition + adapter.setChapters(chapters, forceTransition, useDiff = false) + val pages = chapters.currChapter.pages ?: return + val page = pages[min(chapters.currChapter.requestedPage, pages.lastIndex)] + moveToPage(page) + currentPage = page + } } // Double the cache size to reduce rebinds/recycles incurred by the extra layout space on scroll direction changes