This commit is contained in:
@@ -223,6 +223,16 @@ class ReaderActivity : BaseActivity() {
|
|||||||
ReaderViewModel.Event.ReloadViewerChapters -> {
|
ReaderViewModel.Event.ReloadViewerChapters -> {
|
||||||
viewModel.state.value.viewerChapters?.let(::setChapters)
|
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 -> {
|
ReaderViewModel.Event.PageChanged -> {
|
||||||
displayRefreshHost.flash()
|
displayRefreshHost.flash()
|
||||||
}
|
}
|
||||||
@@ -254,7 +264,8 @@ class ReaderActivity : BaseActivity() {
|
|||||||
readerState = viewModel.state,
|
readerState = viewModel.state,
|
||||||
onChangeReadingMode = viewModel::setMangaReadingMode,
|
onChangeReadingMode = viewModel::setMangaReadingMode,
|
||||||
onChangeOrientation = viewModel::setMangaOrientationType,
|
onChangeOrientation = viewModel::setMangaOrientationType,
|
||||||
onTextSettingsChanged = viewModel::reloadTextReaderStyle,
|
onTextSettingsPreview = viewModel::previewTextReaderStyle,
|
||||||
|
onTextSettingsCommit = viewModel::commitTextReaderStyle,
|
||||||
preferences = readerPreferences,
|
preferences = readerPreferences,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import eu.kanade.tachiyomi.source.model.Page
|
|||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader
|
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader
|
||||||
import eu.kanade.tachiyomi.ui.reader.loader.DownloadPageLoader
|
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.InsertPage
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
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 eu.kanade.tachiyomi.util.storage.cacheImageDir
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
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.image.LocalCoverManager
|
||||||
import tachiyomi.source.local.isLocal
|
import tachiyomi.source.local.isLocal
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
|
|
||||||
@@ -824,41 +825,36 @@ class ReaderViewModel(
|
|||||||
mutableState.update { it.copy(dialog = null) }
|
mutableState.update { it.copy(dialog = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private val textReaderStyleReloading = AtomicBoolean(false)
|
fun previewTextReaderStyle() {
|
||||||
private val textReaderStyleReloadPending = AtomicBoolean(false)
|
|
||||||
|
|
||||||
fun reloadTextReaderStyle() {
|
|
||||||
val chapter = state.value.currentChapter ?: return
|
val chapter = state.value.currentChapter ?: return
|
||||||
if (chapter.pageLoader?.usesTextReaderStyle != true) return
|
if (chapter.pageLoader?.usesTextReaderStyle != true) return
|
||||||
val chapterLoader = loader ?: return
|
val style = readerPreferences.textReaderStyle()
|
||||||
|
val textPages = chapter.pages?.filterIsInstance<TextReaderPage>().orEmpty()
|
||||||
|
if (textPages.isEmpty()) return
|
||||||
|
|
||||||
if (!textReaderStyleReloading.compareAndSet(false, true)) {
|
textPages.forEach { it.updateStyle(style) }
|
||||||
textReaderStyleReloadPending.set(true)
|
eventChannel.trySend(Event.RefreshCurrentTextPage)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
val requestedPage = (state.value.currentPage - 1).coerceAtLeast(0)
|
val requestedPage = (state.value.currentPage - 1).coerceAtLeast(0)
|
||||||
val oldLastIndex = (chapter.pages?.lastIndex ?: 0).coerceAtLeast(0)
|
val oldLastIndex = (chapter.pages?.lastIndex ?: 0).coerceAtLeast(0)
|
||||||
val readingProgress = if (oldLastIndex == 0) 0f else requestedPage.toFloat() / oldLastIndex
|
val readingProgress = if (oldLastIndex == 0) 0f else requestedPage.toFloat() / oldLastIndex
|
||||||
|
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
try {
|
try {
|
||||||
chapter.pageLoader?.recycle()
|
val oldLoader = chapterLoader.reloadTextChapter(chapter, readingProgress) ?: return@launchIO
|
||||||
chapter.pageLoader = null
|
eventChannel.send(Event.ReloadTextViewerChapters)
|
||||||
chapter.state = ReaderChapter.State.Wait
|
// Give old visible holders time to detach before closing resources such as EpubReader.
|
||||||
chapter.requestedPage = requestedPage
|
delay(2000)
|
||||||
chapterLoader.loadChapter(chapter)
|
oldLoader.recycle()
|
||||||
val newLastIndex = (chapter.pages?.lastIndex ?: 0).coerceAtLeast(0)
|
|
||||||
chapter.requestedPage = (readingProgress * newLastIndex).roundToInt().coerceIn(0, newLastIndex)
|
|
||||||
eventChannel.send(Event.ReloadViewerChapters)
|
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
logcat(LogPriority.ERROR, e) { "Failed to reload text reader style" }
|
logcat(LogPriority.ERROR, e) { "Failed to repaginate text reader style" }
|
||||||
} finally {
|
|
||||||
textReaderStyleReloading.set(false)
|
|
||||||
if (textReaderStyleReloadPending.getAndSet(false)) {
|
|
||||||
reloadTextReaderStyle()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1052,6 +1048,8 @@ class ReaderViewModel(
|
|||||||
|
|
||||||
sealed interface Event {
|
sealed interface Event {
|
||||||
data object ReloadViewerChapters : Event
|
data object ReloadViewerChapters : Event
|
||||||
|
data object RefreshCurrentTextPage : Event
|
||||||
|
data object ReloadTextViewerChapters : Event
|
||||||
data object PageChanged : Event
|
data object PageChanged : Event
|
||||||
data class SetOrientation(val orientation: Int) : Event
|
data class SetOrientation(val orientation: Int) : Event
|
||||||
data class SetCoverResult(val result: SetAsCoverResult) : Event
|
data class SetCoverResult(val result: SetAsCoverResult) : Event
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import tachiyomi.domain.source.model.StubSource
|
|||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.source.local.LocalSource
|
import tachiyomi.source.local.LocalSource
|
||||||
import tachiyomi.source.local.io.Format
|
import tachiyomi.source.local.io.Format
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loader used to retrieve the [PageLoader] for a given chapter.
|
* 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.
|
* Checks [chapter] to be loaded based on present pages and loader in addition to state.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -26,10 +26,7 @@ internal class EpubPageLoader(
|
|||||||
|
|
||||||
if (textPages.isNotEmpty()) {
|
if (textPages.isNotEmpty()) {
|
||||||
return textPages.mapIndexed { index, text ->
|
return textPages.mapIndexed { index, text ->
|
||||||
ReaderPage(index).apply {
|
TextReaderPage(index, text, style)
|
||||||
stream = { TextPageRenderer.render(text, style) }
|
|
||||||
status = Page.State.Ready
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.ui.reader.loader
|
package eu.kanade.tachiyomi.ui.reader.loader
|
||||||
|
|
||||||
import com.hippo.unifile.UniFile
|
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.model.ReaderPage
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.TextReaderStyle
|
import eu.kanade.tachiyomi.ui.reader.setting.TextReaderStyle
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
@@ -22,10 +21,7 @@ internal class TextPageLoader(
|
|||||||
val bytes = file.openInputStream().use { it.readBytes() }
|
val bytes = file.openInputStream().use { it.readBytes() }
|
||||||
val text = decodeText(bytes)
|
val text = decodeText(bytes)
|
||||||
return TextPageRenderer.paginate(text, style).mapIndexed { index, pageText ->
|
return TextPageRenderer.paginate(text, style).mapIndexed { index, pageText ->
|
||||||
ReaderPage(index).apply {
|
TextReaderPage(index, pageText, style)
|
||||||
stream = { TextPageRenderer.render(pageText, style) }
|
|
||||||
status = Page.State.Ready
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,8 @@ class ReaderSettingsViewModel(
|
|||||||
readerState: StateFlow<ReaderViewModel.State>,
|
readerState: StateFlow<ReaderViewModel.State>,
|
||||||
val onChangeReadingMode: (ReadingMode) -> Unit,
|
val onChangeReadingMode: (ReadingMode) -> Unit,
|
||||||
val onChangeOrientation: (ReaderOrientation) -> Unit,
|
val onChangeOrientation: (ReaderOrientation) -> Unit,
|
||||||
private val onTextSettingsChanged: () -> Unit,
|
private val onTextSettingsPreview: () -> Unit,
|
||||||
|
private val onTextSettingsCommit: () -> Unit,
|
||||||
val preferences: ReaderPreferences,
|
val preferences: ReaderPreferences,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
@@ -31,20 +32,24 @@ class ReaderSettingsViewModel(
|
|||||||
.stateIn(viewModelScope, SharingStarted.Lazily, null)
|
.stateIn(viewModelScope, SharingStarted.Lazily, null)
|
||||||
|
|
||||||
private var textSettingsReloadJob: Job? = null
|
private var textSettingsReloadJob: Job? = null
|
||||||
|
private var textSettingsDirty = false
|
||||||
|
|
||||||
fun requestTextSettingsReload() {
|
fun requestTextSettingsReload() {
|
||||||
|
textSettingsDirty = true
|
||||||
textSettingsReloadJob?.cancel()
|
textSettingsReloadJob?.cancel()
|
||||||
textSettingsReloadJob = viewModelScope.launch {
|
textSettingsReloadJob = viewModelScope.launch {
|
||||||
delay(150)
|
delay(100)
|
||||||
onTextSettingsChanged()
|
onTextSettingsPreview()
|
||||||
textSettingsReloadJob = null
|
textSettingsReloadJob = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun flushTextSettingsReload() {
|
fun flushTextSettingsReload() {
|
||||||
if (textSettingsReloadJob == null) return
|
if (!textSettingsDirty) return
|
||||||
textSettingsReloadJob?.cancel()
|
textSettingsReloadJob?.cancel()
|
||||||
textSettingsReloadJob = null
|
textSettingsReloadJob = null
|
||||||
onTextSettingsChanged()
|
onTextSettingsPreview()
|
||||||
|
textSettingsDirty = false
|
||||||
|
onTextSettingsCommit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ interface Viewer {
|
|||||||
*/
|
*/
|
||||||
fun setChapters(chapters: ViewerChapters)
|
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].
|
* Tells this viewer to move to the given [page].
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -380,6 +380,17 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
|
|||||||
pager.setCurrentItem(currentItem, false)
|
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
|
* Called from the containing activity when a key [event] is received. It should return true
|
||||||
* if the event was handled, false otherwise.
|
* if the event was handled, false otherwise.
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
|
|||||||
* Updates this adapter with the given [chapters]. It handles setting a few pages of the
|
* Updates this adapter with the given [chapters]. It handles setting a few pages of the
|
||||||
* next/previous chapter to allow seamless transitions.
|
* next/previous chapter to allow seamless transitions.
|
||||||
*/
|
*/
|
||||||
fun setChapters(chapters: ViewerChapters, forceTransition: Boolean) {
|
fun setChapters(chapters: ViewerChapters, forceTransition: Boolean, useDiff: Boolean = true) {
|
||||||
val newItems = mutableListOf<Any>()
|
val newItems = mutableListOf<Any>()
|
||||||
|
|
||||||
// Forces chapter transition if there is missing chapters
|
// Forces chapter transition if there is missing chapters
|
||||||
@@ -65,13 +65,18 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
|
|||||||
|
|
||||||
chapters.nextChapter?.pages?.let(newItems::addAll)
|
chapters.nextChapter?.pages?.let(newItems::addAll)
|
||||||
|
|
||||||
updateItems(newItems)
|
updateItems(newItems, useDiff)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateItems(newItems: List<Any>) {
|
private fun updateItems(newItems: List<Any>, useDiff: Boolean) {
|
||||||
|
if (useDiff) {
|
||||||
val result = DiffUtil.calculateDiff(Callback(items, newItems))
|
val result = DiffUtil.calculateDiff(Callback(items, newItems))
|
||||||
items = newItems
|
items = newItems
|
||||||
result.dispatchUpdatesTo(this)
|
result.dispatchUpdatesTo(this)
|
||||||
|
} else {
|
||||||
|
items = newItems
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refresh() {
|
fun refresh() {
|
||||||
|
|||||||
@@ -353,6 +353,19 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
|||||||
min(position + 3, adapter.itemCount - 1),
|
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
|
// Double the cache size to reduce rebinds/recycles incurred by the extra layout space on scroll direction changes
|
||||||
|
|||||||
Reference in New Issue
Block a user