mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 10:44:42 +00:00
Convert ReaderPresenter to ReaderViewModel
Also fixed certain sources without a contenttype not loading Also remove HTTP 103 interceptor Also upgrade some serialization libraries Look a lot of stuff got taken from upstream if you're reading this commit and you work on main your code is probably is in this commit Co-Authored-By: arkon <4098258+arkon@users.noreply.github.com>
This commit is contained in:
parent
98319daba4
commit
d443da4dcc
36 changed files with 958 additions and 1056 deletions
|
@ -164,7 +164,7 @@ dependencies {
|
|||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
||||
implementation("com.squareup.okio:okio:3.0.0")
|
||||
implementation("com.squareup.okio:okio:3.3.0")
|
||||
|
||||
// Chucker
|
||||
val chuckerVersion = "3.5.2"
|
||||
|
@ -175,9 +175,10 @@ dependencies {
|
|||
implementation(kotlin("reflect", version = AndroidVersions.kotlin))
|
||||
|
||||
// JSON
|
||||
val kotlinSerialization = "1.3.3"
|
||||
val kotlinSerialization = "1.4.0"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:${kotlinSerialization}")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:${kotlinSerialization}")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:${kotlinSerialization}")
|
||||
|
||||
// JavaScript engine
|
||||
implementation("app.cash.quickjs:quickjs-android:0.9.2")
|
||||
|
@ -188,7 +189,7 @@ dependencies {
|
|||
implementation("com.github.junrar:junrar:7.5.0")
|
||||
|
||||
// HTML parser
|
||||
implementation("org.jsoup:jsoup:1.14.3")
|
||||
implementation("org.jsoup:jsoup:1.15.3")
|
||||
|
||||
// Job scheduling
|
||||
implementation("androidx.work:work-runtime-ktx:2.6.0")
|
||||
|
|
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.download
|
|||
import android.content.Context
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
|
@ -13,7 +14,6 @@ import eu.kanade.tachiyomi.source.model.Page
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import rx.Observable
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
|
@ -177,32 +177,21 @@ class DownloadManager(val context: Context) {
|
|||
* @param source the source of the chapter.
|
||||
* @param manga the manga of the chapter.
|
||||
* @param chapter the downloaded chapter.
|
||||
* @return an observable containing the list of pages from the chapter.
|
||||
* @return the list of pages from the chapter.
|
||||
*/
|
||||
fun buildPageList(source: Source, manga: Manga, chapter: Chapter): Observable<List<Page>> {
|
||||
return buildPageList(provider.findChapterDir(chapter, manga, source))
|
||||
}
|
||||
fun buildPageList(source: Source, manga: Manga, chapter: Chapter): List<Page> {
|
||||
val chapterDir = provider.findChapterDir(chapter, manga, source)
|
||||
val files = chapterDir?.listFiles().orEmpty()
|
||||
.filter { "image" in it.type.orEmpty() }
|
||||
|
||||
/**
|
||||
* Builds the page list of a downloaded chapter.
|
||||
*
|
||||
* @param chapterDir the file where the chapter is downloaded.
|
||||
* @return an observable containing the list of pages from the chapter.
|
||||
*/
|
||||
private fun buildPageList(chapterDir: UniFile?): Observable<List<Page>> {
|
||||
return Observable.fromCallable {
|
||||
val files = chapterDir?.listFiles().orEmpty()
|
||||
.filter { "image" in it.type.orEmpty() }
|
||||
|
||||
if (files.isEmpty()) {
|
||||
throw Exception("Page list is empty")
|
||||
}
|
||||
|
||||
files.sortedBy { it.name }
|
||||
.mapIndexed { i, file ->
|
||||
Page(i, uri = file.uri).apply { status = Page.READY }
|
||||
}
|
||||
if (files.isEmpty()) {
|
||||
throw Exception(context.getString(R.string.no_pages_found))
|
||||
}
|
||||
|
||||
return files.sortedBy { it.name }
|
||||
.mapIndexed { i, file ->
|
||||
Page(i, uri = file.uri).apply { status = Page.State.READY }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -346,11 +346,14 @@ class Downloader(
|
|||
val pageListObservable = if (download.pages == null) {
|
||||
// Pull page list from network and add them to download object
|
||||
download.source.fetchPageList(download.chapter)
|
||||
.doOnNext { pages ->
|
||||
.map { pages ->
|
||||
if (pages.isEmpty()) {
|
||||
throw Exception(context.getString(R.string.no_pages_found))
|
||||
}
|
||||
download.pages = pages
|
||||
// Don't trust index from source
|
||||
val reIndexedPages = pages.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
|
||||
download.pages = reIndexedPages
|
||||
reIndexedPages
|
||||
}
|
||||
} else {
|
||||
// Or if the page list already exists, start from the file
|
||||
|
@ -434,13 +437,13 @@ class Downloader(
|
|||
page.uri = file.uri
|
||||
page.progress = 100
|
||||
download.downloadedImages++
|
||||
page.status = Page.READY
|
||||
page.status = Page.State.READY
|
||||
}
|
||||
.map { page }
|
||||
// Mark this page as error and allow to download the remaining
|
||||
.onErrorReturn {
|
||||
page.progress = 0
|
||||
page.status = Page.ERROR
|
||||
page.status = Page.State.ERROR
|
||||
notifier.onError(it.message, download.chapter.name, download.manga.title)
|
||||
page
|
||||
}
|
||||
|
@ -460,13 +463,13 @@ class Downloader(
|
|||
tmpDir: UniFile,
|
||||
filename: String,
|
||||
): Observable<UniFile> {
|
||||
page.status = Page.DOWNLOAD_IMAGE
|
||||
page.status = Page.State.DOWNLOAD_IMAGE
|
||||
page.progress = 0
|
||||
return source.fetchImage(page)
|
||||
.map { response ->
|
||||
val file = tmpDir.createFile("$filename.tmp")
|
||||
try {
|
||||
response.body!!.source().saveTo(file.openOutputStream())
|
||||
response.body.source().saveTo(file.openOutputStream())
|
||||
val extension = getImageExtension(response, file)
|
||||
file.renameTo("$filename.$extension")
|
||||
} catch (e: Exception) {
|
||||
|
@ -515,7 +518,7 @@ class Downloader(
|
|||
*/
|
||||
private fun getImageExtension(response: Response, file: UniFile): String {
|
||||
// Read content type if available.
|
||||
val mime = response.body?.contentType()?.let { ct -> "${ct.type}/${ct.subtype}" }
|
||||
val mime = response.body.contentType()?.let { ct -> "${ct.type}/${ct.subtype}" }
|
||||
// Else guess from the uri.
|
||||
?: context.contentResolver.getType(file.uri)
|
||||
// Else read magic numbers.
|
||||
|
|
|
@ -4,7 +4,9 @@ import com.jakewharton.rxrelay.PublishRelay
|
|||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.DownloadStore
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import rx.subjects.PublishSubject
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
|
@ -20,6 +22,8 @@ class DownloadQueue(
|
|||
|
||||
private val downloadListeners = mutableListOf<DownloadListener>()
|
||||
|
||||
private var scope = MainScope()
|
||||
|
||||
fun addAll(downloads: List<Download>) {
|
||||
downloads.forEach { download ->
|
||||
download.setStatusSubject(statusSubject)
|
||||
|
@ -79,13 +83,15 @@ class DownloadQueue(
|
|||
if (download.status == Download.State.DOWNLOADING) {
|
||||
if (download.pages != null) {
|
||||
for (page in download.pages!!)
|
||||
page.setStatusCallback {
|
||||
callListeners(download)
|
||||
scope.launch {
|
||||
page.statusFlow.collectLatest {
|
||||
callListeners(download)
|
||||
}
|
||||
}
|
||||
}
|
||||
callListeners(download)
|
||||
} else if (download.status == Download.State.DOWNLOADED || download.status == Download.State.ERROR) {
|
||||
setPagesSubject(download.pages, null)
|
||||
// setPagesSubject(download.pages, null)
|
||||
if (download.status == Download.State.ERROR) {
|
||||
callListeners(download)
|
||||
}
|
||||
|
@ -98,13 +104,13 @@ class DownloadQueue(
|
|||
downloadListeners.forEach { it.updateDownload(download) }
|
||||
}
|
||||
|
||||
private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) {
|
||||
if (pages != null) {
|
||||
for (page in pages) {
|
||||
page.setStatusSubject(subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
// private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) {
|
||||
// if (pages != null) {
|
||||
// for (page in pages) {
|
||||
// page.setStatusSubject(subject)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
fun addListener(listener: DownloadListener) {
|
||||
downloadListeners.add(listener)
|
||||
|
|
|
@ -18,7 +18,7 @@ import eu.kanade.tachiyomi.network.GET
|
|||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.ProgressListener
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.newCallWithProgress
|
||||
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||
|
@ -141,7 +141,7 @@ class AppUpdateService : Service() {
|
|||
|
||||
try {
|
||||
// Download the new update.
|
||||
val call = network.client.newCallWithProgress(GET(url), progressListener)
|
||||
val call = network.client.newCachelessCallWithProgress(GET(url), progressListener)
|
||||
runningCall = call
|
||||
val response = call.await()
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ import com.chuckerteam.chucker.api.ChuckerInterceptor
|
|||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
||||
import eu.kanade.tachiyomi.network.interceptor.Http103Interceptor
|
||||
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
|
@ -25,7 +24,6 @@ class NetworkHelper(val context: Context) {
|
|||
val cookieManager = AndroidCookieJar()
|
||||
|
||||
private val userAgentInterceptor by lazy { UserAgentInterceptor() }
|
||||
private val http103Interceptor by lazy { Http103Interceptor(context) }
|
||||
private val cloudflareInterceptor by lazy { CloudflareInterceptor(context) }
|
||||
|
||||
private val baseClientBuilder: OkHttpClient.Builder
|
||||
|
@ -36,7 +34,6 @@ class NetworkHelper(val context: Context) {
|
|||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.callTimeout(2, TimeUnit.MINUTES)
|
||||
.addInterceptor(userAgentInterceptor)
|
||||
.addNetworkInterceptor(http103Interceptor)
|
||||
.apply {
|
||||
if (BuildConfig.DEBUG) {
|
||||
addInterceptor(
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import coil.network.HttpException
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.DeserializationStrategy
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.okio.decodeFromBufferedSource
|
||||
import kotlinx.serialization.serializer
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
|
@ -14,10 +17,9 @@ import rx.Observable
|
|||
import rx.Producer
|
||||
import rx.Subscription
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.fullType
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
val jsonMime = "application/json; charset=utf-8".toMediaType()
|
||||
|
@ -38,9 +40,9 @@ fun Call.asObservable(): Observable<Response> {
|
|||
subscriber.onNext(response)
|
||||
subscriber.onCompleted()
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
} catch (e: Exception) {
|
||||
if (!subscriber.isUnsubscribed) {
|
||||
subscriber.onError(error)
|
||||
subscriber.onError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +62,7 @@ fun Call.asObservable(): Observable<Response> {
|
|||
}
|
||||
|
||||
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
val callback =
|
||||
|
@ -109,18 +112,18 @@ fun Call.asObservableSuccess(): Observable<Response> {
|
|||
return asObservable().doOnNext { response ->
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
throw Exception("HTTP error ${response.code}")
|
||||
throw HttpException(response.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||
fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||
val progressClient = newBuilder()
|
||||
.cache(null)
|
||||
.addNetworkInterceptor { chain ->
|
||||
val originalResponse = chain.proceed(chain.request())
|
||||
originalResponse.newBuilder()
|
||||
.body(ProgressResponseBody(originalResponse.body!!, listener))
|
||||
.body(ProgressResponseBody(originalResponse.body, listener))
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
|
@ -129,11 +132,13 @@ fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListene
|
|||
}
|
||||
|
||||
inline fun <reified T> Response.parseAs(): T {
|
||||
// Avoiding Injekt.get<Json>() due to compiler issues
|
||||
val json = Injekt.getInstance<Json>(fullType<Json>().type)
|
||||
this.use {
|
||||
val responseBody = it.body?.string().orEmpty()
|
||||
return json.decodeFromString(responseBody)
|
||||
return decodeFromJsonResponse(serializer(), this)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
fun <T> decodeFromJsonResponse(deserializer: DeserializationStrategy<T>, response: Response): T {
|
||||
return response.body.source().use {
|
||||
Injekt.get<Json>().decodeFromBufferedSource(deserializer, it)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,8 +15,8 @@ class ProgressResponseBody(private val responseBody: ResponseBody, private val p
|
|||
source(responseBody.source()).buffer()
|
||||
}
|
||||
|
||||
override fun contentType(): MediaType {
|
||||
return responseBody.contentType()!!
|
||||
override fun contentType(): MediaType? {
|
||||
return responseBody.contentType()
|
||||
}
|
||||
|
||||
override fun contentLength(): Long {
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebView
|
||||
import androidx.core.content.ContextCompat
|
||||
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Protocol
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
// TODO: Remove when OkHttp can handle HTTP 103 responses
|
||||
class Http103Interceptor(context: Context) : WebViewInterceptor(context) {
|
||||
|
||||
private val executor = ContextCompat.getMainExecutor(context)
|
||||
|
||||
override fun shouldIntercept(response: Response): Boolean {
|
||||
return response.code == 103
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response {
|
||||
Timber.d("Proceeding with WebView for request $request")
|
||||
try {
|
||||
return proceedWithWebView(request, response)
|
||||
} catch (e: Exception) {
|
||||
throw IOException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
|
||||
private fun proceedWithWebView(originalRequest: Request, originalResponse: Response): Response {
|
||||
// We need to lock this thread until the WebView loads the page, because
|
||||
// OkHttp doesn't support asynchronous interceptors.
|
||||
val latch = CountDownLatch(1)
|
||||
|
||||
val jsInterface = JsInterface(latch)
|
||||
|
||||
var webview: WebView? = null
|
||||
|
||||
var exception: Exception? = null
|
||||
|
||||
val requestUrl = originalRequest.url.toString()
|
||||
val headers = parseHeaders(originalRequest.headers)
|
||||
|
||||
executor.execute {
|
||||
webview = createWebView(originalRequest)
|
||||
webview?.addJavascriptInterface(jsInterface, "android")
|
||||
|
||||
webview?.webViewClient = object : WebViewClientCompat() {
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
view.evaluateJavascript(jsScript) {}
|
||||
}
|
||||
|
||||
override fun onReceivedErrorCompat(
|
||||
view: WebView,
|
||||
errorCode: Int,
|
||||
description: String?,
|
||||
failingUrl: String,
|
||||
isMainFrame: Boolean,
|
||||
) {
|
||||
if (isMainFrame) {
|
||||
exception = Exception("Error $errorCode - $description")
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
webview?.loadUrl(requestUrl, headers)
|
||||
}
|
||||
|
||||
latch.awaitFor30Seconds()
|
||||
|
||||
executor.execute {
|
||||
webview?.run {
|
||||
stopLoading()
|
||||
destroy()
|
||||
}
|
||||
}
|
||||
|
||||
exception?.let { throw it }
|
||||
|
||||
val responseHtml = jsInterface.responseHtml ?: throw Exception("Couldn't fetch site through webview")
|
||||
|
||||
return originalResponse.newBuilder()
|
||||
.code(200)
|
||||
.protocol(Protocol.HTTP_1_1)
|
||||
.message("OK")
|
||||
.body(responseHtml.toResponseBody(htmlMediaType))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
internal class JsInterface(private val latch: CountDownLatch, var responseHtml: String? = null) {
|
||||
@Suppress("UNUSED")
|
||||
@JavascriptInterface
|
||||
fun passPayload(passedPayload: String) {
|
||||
responseHtml = passedPayload
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
private const val jsScript = "window.android.passPayload(document.querySelector('html').outerHTML)"
|
||||
private val htmlMediaType = "text/html".toMediaType()
|
|
@ -121,7 +121,7 @@ open class SourceManager(private val context: Context) {
|
|||
return name
|
||||
}
|
||||
|
||||
private fun getSourceNotInstalledException(): Exception {
|
||||
fun getSourceNotInstalledException(): Exception {
|
||||
return SourceNotFoundException(
|
||||
context.getString(
|
||||
R.string.source_not_installed_,
|
||||
|
|
|
@ -2,8 +2,12 @@ package eu.kanade.tachiyomi.source.model
|
|||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.ProgressListener
|
||||
import rx.subjects.Subject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
|
||||
@Serializable
|
||||
open class Page(
|
||||
val index: Int,
|
||||
val url: String = "",
|
||||
|
@ -14,25 +18,28 @@ open class Page(
|
|||
val number: Int
|
||||
get() = index + 1
|
||||
|
||||
@Transient @Volatile
|
||||
var status: Int = 0
|
||||
@Transient
|
||||
private val _statusFlow = MutableStateFlow(State.QUEUE)
|
||||
|
||||
@Transient
|
||||
val statusFlow = _statusFlow.asStateFlow()
|
||||
var status: State
|
||||
get() = _statusFlow.value
|
||||
set(value) {
|
||||
field = value
|
||||
statusSubject?.onNext(value)
|
||||
statusCallback?.invoke(this)
|
||||
_statusFlow.value = value
|
||||
}
|
||||
|
||||
@Transient @Volatile
|
||||
var progress: Int = 0
|
||||
@Transient
|
||||
private val _progressFlow = MutableStateFlow(0)
|
||||
|
||||
@Transient
|
||||
val progressFlow = _progressFlow.asStateFlow()
|
||||
var progress: Int
|
||||
get() = _progressFlow.value
|
||||
set(value) {
|
||||
field = value
|
||||
statusCallback?.invoke(this)
|
||||
_progressFlow.value = value
|
||||
}
|
||||
|
||||
@Transient private var statusSubject: Subject<Int, Int>? = null
|
||||
|
||||
@Transient private var statusCallback: ((Page) -> Unit)? = null
|
||||
|
||||
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
|
||||
progress = if (contentLength > 0) {
|
||||
(100 * bytesRead / contentLength).toInt()
|
||||
|
@ -41,19 +48,11 @@ open class Page(
|
|||
}
|
||||
}
|
||||
|
||||
fun setStatusSubject(subject: Subject<Int, Int>?) {
|
||||
this.statusSubject = subject
|
||||
}
|
||||
|
||||
fun setStatusCallback(f: ((Page) -> Unit)?) {
|
||||
statusCallback = f
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val QUEUE = 0
|
||||
const val LOAD_PAGE = 1
|
||||
const val DOWNLOAD_IMAGE = 2
|
||||
const val READY = 3
|
||||
const val ERROR = 4
|
||||
enum class State {
|
||||
QUEUE,
|
||||
LOAD_PAGE,
|
||||
DOWNLOAD_IMAGE,
|
||||
READY,
|
||||
ERROR,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@ import eu.kanade.tachiyomi.extension.model.Extension
|
|||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.newCallWithProgress
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
|
@ -305,10 +306,21 @@ abstract class HttpSource : CatalogueSource {
|
|||
* @param page the page whose source image has to be downloaded.
|
||||
*/
|
||||
fun fetchImage(page: Page): Observable<Response> {
|
||||
return client.newCallWithProgress(imageRequest(page), page)
|
||||
return client.newCachelessCallWithProgress(imageRequest(page), page)
|
||||
.asObservableSuccess()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the response of the source image.
|
||||
*
|
||||
* @param page the page whose source image has to be downloaded.
|
||||
*/
|
||||
suspend fun getImage(page: Page): Response {
|
||||
// images will be cached or saved manually, so don't take up network cache
|
||||
return client.newCachelessCallWithProgress(imageRequest(page), page)
|
||||
.awaitSuccess()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for getting the source image. Override only if it's needed to override
|
||||
* the url, send different headers or request method like POST.
|
||||
|
|
|
@ -4,9 +4,9 @@ import eu.kanade.tachiyomi.source.model.Page
|
|||
import rx.Observable
|
||||
|
||||
fun HttpSource.getImageUrl(page: Page): Observable<Page> {
|
||||
page.status = Page.LOAD_PAGE
|
||||
page.status = Page.State.LOAD_PAGE
|
||||
return fetchImageUrl(page)
|
||||
.doOnError { page.status = Page.ERROR }
|
||||
.doOnError { page.status = Page.State.ERROR }
|
||||
.onErrorReturn { null }
|
||||
.doOnNext { page.imageUrl = it }
|
||||
.map { page }
|
||||
|
|
|
@ -30,6 +30,7 @@ import android.view.WindowManager
|
|||
import android.view.animation.AnimationUtils
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
|
@ -64,18 +65,17 @@ import eu.kanade.tachiyomi.BuildConfig
|
|||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlowIn
|
||||
import eu.kanade.tachiyomi.data.preference.toggle
|
||||
import eu.kanade.tachiyomi.databinding.ReaderActivityBinding
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.base.MaterialMenuSheet
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.main.SearchActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.AddToLibraryFirst
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Error
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Success
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderViewModel.SetAsCoverResult.AddToLibraryFirst
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderViewModel.SetAsCoverResult.Error
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderViewModel.SetAsCoverResult.Success
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||
|
@ -104,12 +104,14 @@ import eu.kanade.tachiyomi.util.system.isInNightMode
|
|||
import eu.kanade.tachiyomi.util.system.isLTR
|
||||
import eu.kanade.tachiyomi.util.system.isTablet
|
||||
import eu.kanade.tachiyomi.util.system.launchIO
|
||||
import eu.kanade.tachiyomi.util.system.launchNonCancellable
|
||||
import eu.kanade.tachiyomi.util.system.launchUI
|
||||
import eu.kanade.tachiyomi.util.system.materialAlertDialog
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import eu.kanade.tachiyomi.util.system.rootWindowInsetsCompat
|
||||
import eu.kanade.tachiyomi.util.system.spToPx
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.system.withUIContext
|
||||
import eu.kanade.tachiyomi.util.view.collapse
|
||||
import eu.kanade.tachiyomi.util.view.compatToolTipText
|
||||
import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsetsCompat
|
||||
|
@ -123,17 +125,17 @@ import eu.kanade.tachiyomi.widget.doOnStart
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import nucleus.factory.RequiresPresenter
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
|
@ -144,17 +146,13 @@ import kotlin.math.roundToInt
|
|||
|
||||
/**
|
||||
* Activity containing the reader of Tachiyomi. This activity is mostly a container of the
|
||||
* viewers, to which calls from the presenter or UI events are delegated.
|
||||
* viewers, to which calls from the view model or UI events are delegated.
|
||||
*/
|
||||
@RequiresPresenter(ReaderPresenter::class)
|
||||
class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
class ReaderActivity : BaseActivity<ReaderActivityBinding>() {
|
||||
|
||||
lateinit var binding: ReaderActivityBinding
|
||||
val viewModel by viewModels<ReaderViewModel>()
|
||||
|
||||
/**
|
||||
* Preferences helper.
|
||||
*/
|
||||
private val preferences by injectLazy<PreferencesHelper>()
|
||||
val scope = lifecycleScope
|
||||
|
||||
/**
|
||||
* Viewer used to display the pages (pager, webtoon, ...).
|
||||
|
@ -263,7 +261,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Called when the activity is created. Initializes the presenter and configuration.
|
||||
* Called when the activity is created. Initializes the view model and configuration.
|
||||
*/
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Setup shared element transitions
|
||||
|
@ -300,7 +298,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
)
|
||||
|
||||
backPressedCallback = onBackPressedDispatcher.addCallback { backCallback() }
|
||||
if (presenter.needsInit()) {
|
||||
if (viewModel.needsInit()) {
|
||||
fromUrl = handleIntentAction(intent)
|
||||
if (!fromUrl) {
|
||||
val manga = intent.extras!!.getLong("manga", -1)
|
||||
|
@ -309,7 +307,15 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
finish()
|
||||
return
|
||||
}
|
||||
presenter.init(manga, chapter)
|
||||
lifecycleScope.launchNonCancellable {
|
||||
val initResult = viewModel.init(manga, chapter)
|
||||
if (!initResult.getOrDefault(false)) {
|
||||
val exception = initResult.exceptionOrNull() ?: IllegalStateException("Unknown err")
|
||||
withUIContext {
|
||||
setInitialChapterError(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.pleaseWait.isVisible = true
|
||||
}
|
||||
|
@ -334,6 +340,58 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
SecureActivityDelegate.setSecure(this)
|
||||
}
|
||||
reEnableBackPressedCallBack()
|
||||
|
||||
viewModel.state
|
||||
.map { it.isLoadingAdjacentChapter }
|
||||
.distinctUntilChanged()
|
||||
.onEach(::setProgressDialog)
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
viewModel.state
|
||||
.map { it.manga }
|
||||
.distinctUntilChanged()
|
||||
.filterNotNull()
|
||||
.onEach(::setManga)
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
val viewerChapters = viewModel.state.value.viewerChapters
|
||||
viewModel.state
|
||||
.map { it.viewerChapters }
|
||||
.distinctUntilChanged()
|
||||
// .drop(if (viewerChapters != null) 1 else 0)
|
||||
.filterNotNull()
|
||||
.onEach(::setChapters)
|
||||
.launchIn(lifecycleScope)
|
||||
viewerChapters?.currChapter?.let { currChapter ->
|
||||
currChapter.requestedPage = currChapter.chapter.last_page_read
|
||||
}
|
||||
|
||||
viewModel.eventFlow
|
||||
.onEach { event ->
|
||||
when (event) {
|
||||
ReaderViewModel.Event.ReloadMangaAndChapters -> {
|
||||
viewModel.manga?.let(::setManga)
|
||||
viewModel.state.value.viewerChapters?.let(::setChapters)
|
||||
}
|
||||
ReaderViewModel.Event.ReloadViewerChapters -> {
|
||||
viewModel.state.value.viewerChapters?.let(::setChapters)
|
||||
}
|
||||
is ReaderViewModel.Event.SetOrientation -> {
|
||||
setOrientation(event.orientation)
|
||||
}
|
||||
is ReaderViewModel.Event.SavedImage -> {
|
||||
onSaveImageResult(event.result)
|
||||
}
|
||||
is ReaderViewModel.Event.ShareImage -> {
|
||||
onShareImageResult(event.file, event.page)
|
||||
}
|
||||
is ReaderViewModel.Event.SetCoverResult -> {
|
||||
onSetAsCoverResult(event.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
lifecycleScope.launchUI {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
WindowInfoTracker.getOrCreate(this@ReaderActivity).windowLayoutInfo(this@ReaderActivity)
|
||||
|
@ -397,7 +455,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
}
|
||||
}
|
||||
if (!isChangingConfigurations) {
|
||||
presenter.onSaveInstanceStateNonConfigurationChange()
|
||||
viewModel.onSaveInstanceStateNonConfigurationChange()
|
||||
} else {
|
||||
viewModel.onSave()
|
||||
}
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
@ -541,7 +601,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
private fun shiftDoublePages() {
|
||||
(viewer as? PagerViewer)?.config?.let { config ->
|
||||
config.shiftDoublePage = !config.shiftDoublePage
|
||||
presenter.viewerChapters?.let {
|
||||
viewModel.state.value.viewerChapters?.let {
|
||||
(viewer as? PagerViewer)?.updateShifting()
|
||||
(viewer as? PagerViewer)?.setChaptersDoubleShift(it)
|
||||
invalidateOptionsMenu()
|
||||
|
@ -570,13 +630,13 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
if (didTransistionFromChapter && visibleChapterRange.isNotEmpty() && MainActivity.chapterIdToExitTo !in visibleChapterRange) {
|
||||
finish()
|
||||
} else {
|
||||
presenter.onBackPressed()
|
||||
viewModel.onBackPressed()
|
||||
super.finishAfterTransition()
|
||||
}
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
presenter.onBackPressed()
|
||||
viewModel.onBackPressed()
|
||||
super.finish()
|
||||
}
|
||||
|
||||
|
@ -670,7 +730,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
}
|
||||
|
||||
binding.toolbar.setOnClickListener {
|
||||
presenter.manga?.id?.let { id ->
|
||||
viewModel.manga?.id?.let { id ->
|
||||
val intent = SearchActivity.openMangaIntent(this, id)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
@ -712,12 +772,12 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
setOnClickListener {
|
||||
popupMenu(
|
||||
items = OrientationType.values().map { it.flagValue to it.stringRes },
|
||||
selectedItemId = presenter.manga?.orientationType
|
||||
selectedItemId = viewModel.manga?.orientationType
|
||||
?: preferences.defaultOrientationType().get(),
|
||||
) {
|
||||
val newOrientation = OrientationType.fromPreference(itemId)
|
||||
|
||||
presenter.setMangaOrientationType(newOrientation.flagValue)
|
||||
viewModel.setMangaOrientationType(newOrientation.flagValue)
|
||||
|
||||
updateOrientationShortcut(newOrientation.flagValue)
|
||||
}
|
||||
|
@ -740,9 +800,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
readingMode.setOnClickListener { readingMode ->
|
||||
readingMode.popupMenu(
|
||||
items = ReadingModeType.values().map { it.flagValue to it.stringRes },
|
||||
selectedItemId = presenter.manga?.readingModeType,
|
||||
selectedItemId = viewModel.manga?.readingModeType,
|
||||
) {
|
||||
presenter.setMangaReadingMode(itemId)
|
||||
viewModel.setMangaReadingMode(itemId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -757,53 +817,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
binding.chaptersSheet.shiftPageButton.setOnClickListener {
|
||||
shiftDoublePages()
|
||||
}
|
||||
binding.readerNav.leftChapter.setOnClickListener {
|
||||
if (isLoading) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
isScrollingThroughPagesOrChapters = true
|
||||
val result = if (viewer is R2LPagerViewer) {
|
||||
presenter.loadNextChapter()
|
||||
} else {
|
||||
presenter.loadPreviousChapter()
|
||||
}
|
||||
if (result) {
|
||||
binding.readerNav.leftChapter.isInvisible = true
|
||||
binding.readerNav.leftProgress.isVisible = true
|
||||
} else {
|
||||
toast(
|
||||
if (viewer is R2LPagerViewer) {
|
||||
R.string.theres_no_next_chapter
|
||||
} else {
|
||||
R.string.theres_no_previous_chapter
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
binding.readerNav.rightChapter.setOnClickListener {
|
||||
if (isLoading) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
isScrollingThroughPagesOrChapters = true
|
||||
val result = if (viewer !is R2LPagerViewer) {
|
||||
presenter.loadNextChapter()
|
||||
} else {
|
||||
presenter.loadPreviousChapter()
|
||||
}
|
||||
if (result) {
|
||||
binding.readerNav.rightChapter.isInvisible = true
|
||||
binding.readerNav.rightProgress.isVisible = true
|
||||
} else {
|
||||
toast(
|
||||
if (viewer !is R2LPagerViewer) {
|
||||
R.string.theres_no_next_chapter
|
||||
} else {
|
||||
R.string.theres_no_previous_chapter
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
binding.readerNav.leftChapter.setOnClickListener { loadAdjacentChapter(false) }
|
||||
binding.readerNav.rightChapter.setOnClickListener { loadAdjacentChapter(true) }
|
||||
|
||||
binding.touchView.setOnTouchListener { _, event ->
|
||||
if (event.action == MotionEvent.ACTION_DOWN) {
|
||||
|
@ -874,7 +890,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
val pageNumber = (value + 1).roundToInt()
|
||||
(viewer as? PagerViewer)?.let {
|
||||
if (it.config.doublePages || it.config.splitPages) {
|
||||
if (it.hasExtraPage(value.roundToInt(), presenter.getCurrentChapter())) {
|
||||
if (it.hasExtraPage(value.roundToInt(), viewModel.getCurrentChapter())) {
|
||||
val invertDoublePage = (viewer as? PagerViewer)?.config?.invertDoublePages ?: false
|
||||
return@setLabelFormatter if (!binding.readerNav.pageSeekbar.isRTL.xor(invertDoublePage)) {
|
||||
"$pageNumber-${pageNumber + 1}"
|
||||
|
@ -951,6 +967,47 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun loadAdjacentChapter(rightButton: Boolean) {
|
||||
if (isLoading) {
|
||||
return
|
||||
}
|
||||
isScrollingThroughPagesOrChapters = true
|
||||
lifecycleScope.launch {
|
||||
val getNextChapter = (viewer is R2LPagerViewer).xor(rightButton)
|
||||
val adjChapter = viewModel.adjacentChapter(getNextChapter)
|
||||
if (adjChapter != null) {
|
||||
if (rightButton) {
|
||||
binding.readerNav.rightChapter.isInvisible = true
|
||||
binding.readerNav.rightProgress.isVisible = true
|
||||
} else {
|
||||
binding.readerNav.leftChapter.isInvisible = true
|
||||
binding.readerNav.leftProgress.isVisible = true
|
||||
}
|
||||
loadChapter(adjChapter)
|
||||
} else {
|
||||
toast(
|
||||
if (getNextChapter) {
|
||||
R.string.theres_no_next_chapter
|
||||
} else {
|
||||
R.string.theres_no_previous_chapter
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadChapter(chapter: Chapter) {
|
||||
loadChapter(ReaderChapter(chapter))
|
||||
}
|
||||
|
||||
private suspend fun loadChapter(chapter: ReaderChapter) {
|
||||
val lastPage = viewModel.loadChapter(chapter) ?: return
|
||||
launchUI {
|
||||
moveToPageIndex(lastPage, false, chapterChange = true)
|
||||
}
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
fun setNavColor(insets: WindowInsetsCompat) {
|
||||
sheetManageNavColor = when {
|
||||
isSplitScreen -> {
|
||||
|
@ -1079,13 +1136,13 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when a manga is ready. Used to instantiate the appropriate viewer
|
||||
* Called from the view model when a manga is ready. Used to instantiate the appropriate viewer
|
||||
* and the binding.toolbar title.
|
||||
*/
|
||||
fun setManga(manga: Manga) {
|
||||
val prevViewer = viewer
|
||||
val noDefault = manga.viewer_flags == -1
|
||||
val mangaViewer = presenter.getMangaReadingMode()
|
||||
val mangaViewer = viewModel.getMangaReadingMode()
|
||||
val newViewer = when (mangaViewer) {
|
||||
ReadingModeType.LEFT_TO_RIGHT.flagValue -> L2RPagerViewer(this)
|
||||
ReadingModeType.VERTICAL.flagValue -> VerticalPagerViewer(this)
|
||||
|
@ -1094,8 +1151,8 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
else -> R2LPagerViewer(this)
|
||||
}
|
||||
|
||||
if (noDefault && presenter.manga?.readingModeType!! > 0 &&
|
||||
presenter.manga?.readingModeType!! != preferences.defaultReadingMode()
|
||||
if (noDefault && viewModel.manga?.readingModeType!! > 0 &&
|
||||
viewModel.manga?.readingModeType!! != preferences.defaultReadingMode()
|
||||
) {
|
||||
snackbar = binding.readerLayout.snack(
|
||||
getString(
|
||||
|
@ -1112,7 +1169,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
4000,
|
||||
) {
|
||||
setAction(R.string.use_default) {
|
||||
presenter.setMangaReadingMode(0)
|
||||
viewModel.setMangaReadingMode(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1122,10 +1179,10 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
) {
|
||||
// Wait until transition is complete to avoid crash on API 26
|
||||
window.sharedElementEnterTransition.addListener(
|
||||
onEnd = { setOrientation(presenter.getMangaOrientationType()) },
|
||||
onEnd = { setOrientation(viewModel.getMangaOrientationType()) },
|
||||
)
|
||||
} else {
|
||||
setOrientation(presenter.getMangaOrientationType())
|
||||
setOrientation(viewModel.getMangaOrientationType())
|
||||
}
|
||||
|
||||
// Destroy previous viewer if there was one
|
||||
|
@ -1161,7 +1218,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
},
|
||||
)
|
||||
|
||||
binding.toolbar.title = manga.title
|
||||
supportActionBar?.title = manga.title
|
||||
|
||||
binding.readerNav.pageSeekbar.isRTL = newViewer is R2LPagerViewer
|
||||
|
||||
|
@ -1170,19 +1227,19 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
invalidateOptionsMenu()
|
||||
updateCropBordersShortcut()
|
||||
updateBottomShortcuts()
|
||||
val viewerMode = ReadingModeType.fromPreference(presenter?.manga?.readingModeType ?: 0)
|
||||
val viewerMode = ReadingModeType.fromPreference(viewModel.state.value.manga?.readingModeType ?: 0)
|
||||
binding.chaptersSheet.readingMode.setImageResource(viewerMode.iconRes)
|
||||
startPostponedEnterTransition()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
presenter.saveCurrentChapterReadingProgress()
|
||||
viewModel.saveCurrentChapterReadingProgress()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
presenter.setReadStartTime()
|
||||
viewModel.setReadStartTime()
|
||||
}
|
||||
|
||||
fun reloadChapters(doublePages: Boolean, force: Boolean = false) {
|
||||
|
@ -1196,7 +1253,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
pViewer.config.splitPages = preferences.automaticSplitsPage().get() && !pViewer.config.doublePages
|
||||
}
|
||||
}
|
||||
val currentChapter = presenter.getCurrentChapter()
|
||||
val currentChapter = viewModel.getCurrentChapter()
|
||||
if (doublePages) {
|
||||
// If we're moving from singe to double, we want the current page to be the first page
|
||||
pViewer.config.shiftDoublePage = (
|
||||
|
@ -1207,14 +1264,14 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
)
|
||||
) % 2 != 0
|
||||
}
|
||||
presenter.viewerChapters?.let {
|
||||
viewModel.state.value.viewerChapters?.let {
|
||||
pViewer.setChaptersDoubleShift(it)
|
||||
}
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter whenever a new [viewerChapters] have been set. It delegates the
|
||||
* Called from the view model whenever a new [viewerChapters] have been set. It delegates the
|
||||
* method to the current viewer, but also set the subtitle on the binding.toolbar.
|
||||
*/
|
||||
fun setChapters(viewerChapters: ViewerChapters) {
|
||||
|
@ -1246,7 +1303,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
viewer?.setChapters(viewerChapters)
|
||||
intentPageNumber?.let { moveToPageIndex(it) }
|
||||
intentPageNumber = null
|
||||
binding.toolbar.subtitle = if (presenter.manga!!.hideChapterTitle(preferences)) {
|
||||
binding.toolbar.subtitle = if (viewModel.manga!!.hideChapterTitle(preferences)) {
|
||||
val number = decimalFormat.format(viewerChapters.currChapter.chapter.chapter_number.toDouble())
|
||||
getString(R.string.chapter_, number)
|
||||
} else {
|
||||
|
@ -1268,7 +1325,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter if the initial load couldn't load the pages of the chapter. In
|
||||
* Called from the view model if the initial load couldn't load the pages of the chapter. In
|
||||
* this case the activity is closed and a toast is shown to the user.
|
||||
*/
|
||||
fun setInitialChapterError(error: Throwable) {
|
||||
|
@ -1278,7 +1335,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter whenever it's loading the next or previous chapter. It shows or
|
||||
* Called from the view model whenever it's loading the next or previous chapter. It shows or
|
||||
* dismisses a non-cancellable dialog to prevent user interaction according to the value of
|
||||
* [show]. This is only used when the next/previous buttons on the binding.toolbar are clicked; the
|
||||
* other cases are handled with chapter transitions on the viewers and chapter preloading.
|
||||
|
@ -1308,7 +1365,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
*/
|
||||
fun moveToPageIndex(index: Int, animated: Boolean = true, chapterChange: Boolean = false) {
|
||||
val viewer = viewer ?: return
|
||||
val currentChapter = presenter.getCurrentChapter() ?: return
|
||||
val currentChapter = viewModel.getCurrentChapter() ?: return
|
||||
val page = currentChapter.pages?.getOrNull(index) ?: return
|
||||
viewer.moveToPage(page, animated)
|
||||
if (chapterChange) {
|
||||
|
@ -1322,11 +1379,11 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
|
||||
/**
|
||||
* Called from the viewer whenever a [page] is marked as active. It updates the values of the
|
||||
* bottom menu and delegates the change to the presenter.
|
||||
* bottom menu and delegates the change to the view model.
|
||||
*/
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun onPageSelected(page: ReaderPage, hasExtraPage: Boolean) {
|
||||
presenter.onPageSelected(page, hasExtraPage)
|
||||
viewModel.onPageSelected(page, hasExtraPage)
|
||||
val pages = page.chapter.pages ?: return
|
||||
|
||||
val currentPage = if (hasExtraPage) {
|
||||
|
@ -1449,9 +1506,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
Color.BLACK
|
||||
}
|
||||
if (item == 6) {
|
||||
presenter.shareImages(page, secondPage, isLTR, bg)
|
||||
viewModel.shareImages(page, secondPage, isLTR, bg)
|
||||
} else {
|
||||
presenter.saveImages(page, secondPage, isLTR, bg)
|
||||
viewModel.saveImages(page, secondPage, isLTR, bg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1468,7 +1525,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
* the viewer is reaching the beginning or end of a chapter or the transition page is active.
|
||||
*/
|
||||
fun requestPreloadChapter(chapter: ReaderChapter) {
|
||||
presenter.preloadChapter(chapter)
|
||||
lifecycleScope.launch {
|
||||
viewModel.preloadChapter(chapter)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1489,15 +1548,15 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Called from the page sheet. It delegates the call to the presenter to do some IO, which
|
||||
* Called from the page sheet. It delegates the call to the view model to do some IO, which
|
||||
* will call [onShareImageResult] with the path the image was saved on when it's ready.
|
||||
*/
|
||||
private fun shareImage(page: ReaderPage) {
|
||||
presenter.shareImage(page)
|
||||
viewModel.shareImage(page)
|
||||
}
|
||||
|
||||
private fun showSetCoverPrompt(page: ReaderPage) {
|
||||
if (page.status != Page.READY) return
|
||||
if (page.status != Page.State.READY) return
|
||||
|
||||
materialAlertDialog()
|
||||
.setMessage(R.string.use_image_as_cover)
|
||||
|
@ -1509,11 +1568,11 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when a page is ready to be shared. It shows Android's default
|
||||
* Called from the view model when a page is ready to be shared. It shows Android's default
|
||||
* sharing tool.
|
||||
*/
|
||||
fun onShareImageResult(file: File, page: ReaderPage, secondPage: ReaderPage? = null) {
|
||||
val manga = presenter.manga ?: return
|
||||
val manga = viewModel.manga ?: return
|
||||
val chapter = page.chapter.chapter
|
||||
|
||||
val decimalFormat =
|
||||
|
@ -1544,28 +1603,28 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
|
||||
override fun onProvideAssistContent(outContent: AssistContent) {
|
||||
super.onProvideAssistContent(outContent)
|
||||
val chapterUrl = presenter.getChapterUrl() ?: return
|
||||
val chapterUrl = viewModel.getChapterUrl() ?: return
|
||||
outContent.webUri = Uri.parse(chapterUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the page sheet. It delegates saving the image of the given [page] on external
|
||||
* storage to the presenter.
|
||||
* storage to the viewModel.
|
||||
*/
|
||||
private fun saveImage(page: ReaderPage) {
|
||||
presenter.saveImage(page)
|
||||
viewModel.saveImage(page)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when a page is saved or fails. It shows a message or logs the
|
||||
* Called from the view model when a page is saved or fails. It shows a message or logs the
|
||||
* event depending on the [result].
|
||||
*/
|
||||
fun onSaveImageResult(result: ReaderPresenter.SaveImageResult) {
|
||||
fun onSaveImageResult(result: ReaderViewModel.SaveImageResult) {
|
||||
when (result) {
|
||||
is ReaderPresenter.SaveImageResult.Success -> {
|
||||
is ReaderViewModel.SaveImageResult.Success -> {
|
||||
toast(R.string.picture_saved)
|
||||
}
|
||||
is ReaderPresenter.SaveImageResult.Error -> {
|
||||
is ReaderViewModel.SaveImageResult.Error -> {
|
||||
Timber.e(result.error)
|
||||
}
|
||||
}
|
||||
|
@ -1573,17 +1632,17 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
|
||||
/**
|
||||
* Called from the page sheet. It delegates setting the image of the given [page] as the
|
||||
* cover to the presenter.
|
||||
* cover to the viewModel.
|
||||
*/
|
||||
private fun setAsCover(page: ReaderPage) {
|
||||
presenter.setAsCover(page)
|
||||
viewModel.setAsCover(page)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when a page is set as cover or fails. It shows a different message
|
||||
* Called from the view model when a page is set as cover or fails. It shows a different message
|
||||
* depending on the [result].
|
||||
*/
|
||||
fun onSetAsCoverResult(result: ReaderPresenter.SetAsCoverResult) {
|
||||
fun onSetAsCoverResult(result: ReaderViewModel.SetAsCoverResult) {
|
||||
toast(
|
||||
when (result) {
|
||||
Success -> R.string.cover_updated
|
||||
|
@ -1662,7 +1721,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
|
||||
private fun handleIntentAction(intent: Intent): Boolean {
|
||||
val uri = intent.data ?: return false
|
||||
if (!presenter.canLoadUrl(uri)) {
|
||||
if (!viewModel.canLoadUrl(uri)) {
|
||||
openInBrowser(intent.data!!.toString(), true)
|
||||
finishAfterTransition()
|
||||
return true
|
||||
|
@ -1670,8 +1729,8 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
setMenuVisibility(visible = false, animate = true)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
intentPageNumber = presenter.intentPageNumber(uri)
|
||||
presenter.loadChapterURL(uri)
|
||||
intentPageNumber = viewModel.intentPageNumber(uri)
|
||||
viewModel.loadChapterURL(uri)
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
setInitialChapterError(e)
|
||||
|
@ -1682,14 +1741,14 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
}
|
||||
|
||||
private fun openMangaInBrowser() {
|
||||
val source = presenter.getSource() ?: return
|
||||
val chapterUrl = presenter.getChapterUrl() ?: return
|
||||
val source = viewModel.getSource() ?: return
|
||||
val chapterUrl = viewModel.getChapterUrl() ?: return
|
||||
|
||||
val intent = WebViewActivity.newIntent(
|
||||
applicationContext,
|
||||
source.id,
|
||||
chapterUrl,
|
||||
presenter.manga!!.title,
|
||||
viewModel.manga!!.title,
|
||||
)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
@ -1719,7 +1778,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
.drop(1)
|
||||
.onEach {
|
||||
delay(250)
|
||||
setOrientation(presenter.getMangaOrientationType())
|
||||
setOrientation(viewModel.getMangaOrientationType())
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
|
|
|
@ -3,10 +3,11 @@ package eu.kanade.tachiyomi.ui.reader
|
|||
import android.app.Application
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import androidx.annotation.ColorInt
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
|
@ -14,6 +15,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
|||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
|
@ -23,7 +25,6 @@ import eu.kanade.tachiyomi.source.Source
|
|||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem
|
||||
import eu.kanade.tachiyomi.ui.reader.chapter.ReaderChapterItem
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader
|
||||
|
@ -43,43 +44,61 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
|
|||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import eu.kanade.tachiyomi.util.system.executeOnIO
|
||||
import eu.kanade.tachiyomi.util.system.launchIO
|
||||
import eu.kanade.tachiyomi.util.system.launchUI
|
||||
import eu.kanade.tachiyomi.util.system.launchNonCancellable
|
||||
import eu.kanade.tachiyomi.util.system.localeContext
|
||||
import eu.kanade.tachiyomi.util.system.withIOContext
|
||||
import eu.kanade.tachiyomi.util.system.withUIContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.CancellationException
|
||||
|
||||
/**
|
||||
* Presenter used by the activity to perform background operations.
|
||||
*/
|
||||
class ReaderPresenter(
|
||||
class ReaderViewModel(
|
||||
private val savedState: SavedStateHandle = SavedStateHandle(),
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get(),
|
||||
private val coverCache: CoverCache = Injekt.get(),
|
||||
private val preferences: PreferencesHelper = Injekt.get(),
|
||||
private val chapterFilter: ChapterFilter = Injekt.get(),
|
||||
) : BasePresenter<ReaderActivity>() {
|
||||
) : ViewModel() {
|
||||
|
||||
private val mutableState = MutableStateFlow(State())
|
||||
val state = mutableState.asStateFlow()
|
||||
|
||||
private val downloadProvider = DownloadProvider(preferences.context)
|
||||
|
||||
private val eventChannel = Channel<Event>()
|
||||
val eventFlow = eventChannel.receiveAsFlow()
|
||||
|
||||
/**
|
||||
* The manga loaded in the reader. It can be null when instantiated for a short time.
|
||||
*/
|
||||
var manga: Manga? = null
|
||||
private set
|
||||
val manga: Manga?
|
||||
get() = state.value.manga
|
||||
|
||||
val source: Source?
|
||||
get() = manga?.source?.let { sourceManager.getOrStub(it) }
|
||||
|
@ -87,7 +106,11 @@ class ReaderPresenter(
|
|||
/**
|
||||
* The chapter id of the currently loaded chapter. Used to restore from process kill.
|
||||
*/
|
||||
private var chapterId = -1L
|
||||
private var chapterId = savedState.get<Long>("chapter_id") ?: -1L
|
||||
set(value) {
|
||||
savedState["chapter_id"] = value
|
||||
field = value
|
||||
}
|
||||
|
||||
/**
|
||||
* The chapter loader for the loaded manga. It'll be null until [manga] is set.
|
||||
|
@ -99,25 +122,11 @@ class ReaderPresenter(
|
|||
*/
|
||||
private var chapterReadStartTime: Long? = null
|
||||
|
||||
/**
|
||||
* Subscription to prevent setting chapters as active from multiple threads.
|
||||
*/
|
||||
private var activeChapterSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Relay for currently active viewer chapters.
|
||||
*/
|
||||
private val viewerChaptersRelay = BehaviorRelay.create<ViewerChapters>()
|
||||
|
||||
val viewerChapters: ViewerChapters?
|
||||
get() = viewerChaptersRelay.value
|
||||
|
||||
/**
|
||||
* Relay used when loading prev/next chapter needed to lock the UI (with a dialog).
|
||||
*/
|
||||
private val isLoadingAdjacentChapterRelay = BehaviorRelay.create<Boolean>()
|
||||
private var finished = false
|
||||
private var chapterDownload: Download? = null
|
||||
private var chapterToDownload: Download? = null
|
||||
|
||||
/**
|
||||
* Chapter list for the active manga. It's retrieved lazily and should be accessed for the first
|
||||
|
@ -147,27 +156,26 @@ class ReaderPresenter(
|
|||
hasTrackers = tracks.size > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the presenter is created. It retrieves the saved active chapter if the process
|
||||
* was restored.
|
||||
*/
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
if (savedState != null) {
|
||||
chapterId = savedState.getLong(::chapterId.name, -1)
|
||||
}
|
||||
init {
|
||||
// To save state
|
||||
state.map { it.viewerChapters?.currChapter }
|
||||
.distinctUntilChanged()
|
||||
.filterNotNull()
|
||||
.onEach { currentChapter ->
|
||||
chapterId = currentChapter.chapter.id!!
|
||||
currentChapter.requestedPage = currentChapter.chapter.last_page_read
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the presenter instance is being saved. It saves the currently active chapter
|
||||
* id and the last page read.
|
||||
*/
|
||||
override fun onSave(state: Bundle) {
|
||||
super.onSave(state)
|
||||
fun onSave() {
|
||||
val currentChapter = getCurrentChapter()
|
||||
if (currentChapter != null) {
|
||||
currentChapter.requestedPage = currentChapter.chapter.last_page_read
|
||||
state.putLong(::chapterId.name, currentChapter.chapter.id!!)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,11 +187,11 @@ class ReaderPresenter(
|
|||
if (finished) return
|
||||
finished = true
|
||||
deletePendingChapters()
|
||||
val currentChapters = viewerChaptersRelay.value
|
||||
val currentChapters = state.value.viewerChapters
|
||||
if (currentChapters != null) {
|
||||
currentChapters.unref()
|
||||
saveReadingProgress(currentChapters.currChapter)
|
||||
chapterDownload?.let {
|
||||
chapterToDownload?.let {
|
||||
downloadManager.addDownloadsToStartOfQueue(listOf(it))
|
||||
}
|
||||
}
|
||||
|
@ -209,19 +217,40 @@ class ReaderPresenter(
|
|||
* Initializes this presenter with the given [mangaId] and [initialChapterId]. This method will
|
||||
* fetch the manga from the database and initialize the initial chapter.
|
||||
*/
|
||||
fun init(mangaId: Long, initialChapterId: Long) {
|
||||
if (!needsInit()) return
|
||||
suspend fun init(mangaId: Long, initialChapterId: Long): Result<Boolean> {
|
||||
if (!needsInit()) return Result.success(true)
|
||||
return withIOContext {
|
||||
try {
|
||||
val manga = db.getManga(mangaId).executeAsBlocking()
|
||||
if (manga != null) {
|
||||
mutableState.update { it.copy(manga = manga) }
|
||||
if (chapterId == -1L) chapterId = initialChapterId
|
||||
|
||||
db.getManga(mangaId).asRxObservable()
|
||||
.first()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { init(it, initialChapterId) }
|
||||
.subscribeFirst(
|
||||
{ _, _ ->
|
||||
// Ignore onNext event
|
||||
},
|
||||
ReaderActivity::setInitialChapterError,
|
||||
)
|
||||
checkTrackers(manga)
|
||||
|
||||
NotificationReceiver.dismissNotification(
|
||||
preferences.context,
|
||||
manga.id!!.hashCode(),
|
||||
Notifications.ID_NEW_CHAPTERS,
|
||||
)
|
||||
|
||||
val source = sourceManager.getOrStub(manga.source)
|
||||
val context = Injekt.get<Application>()
|
||||
loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source)
|
||||
|
||||
loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id })
|
||||
Result.success(true)
|
||||
} else {
|
||||
// Unlikely but okay
|
||||
Result.success(false)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) {
|
||||
throw e
|
||||
}
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getChapters(): List<ReaderChapterItem> {
|
||||
|
@ -237,7 +266,7 @@ class ReaderPresenter(
|
|||
ReaderChapterItem(
|
||||
it,
|
||||
manga,
|
||||
it.id == getCurrentChapter()?.chapter?.id ?: chapterId,
|
||||
it.id == (getCurrentChapter()?.chapter?.id ?: chapterId),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -245,82 +274,6 @@ class ReaderPresenter(
|
|||
return chapterItems
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this presenter with the given [manga] and [initialChapterId]. This method will
|
||||
* set the chapter loader, view subscriptions and trigger an initial load.
|
||||
*/
|
||||
private fun init(manga: Manga, initialChapterId: Long) {
|
||||
if (!needsInit()) return
|
||||
|
||||
this.manga = manga
|
||||
if (chapterId == -1L) chapterId = initialChapterId
|
||||
|
||||
checkTrackers(manga)
|
||||
|
||||
NotificationReceiver.dismissNotification(
|
||||
preferences.context,
|
||||
manga.id!!.hashCode(),
|
||||
Notifications.ID_NEW_CHAPTERS,
|
||||
)
|
||||
|
||||
val source = sourceManager.getOrStub(manga.source)
|
||||
loader = ChapterLoader(downloadManager, manga, source)
|
||||
|
||||
Observable.just(manga).subscribeLatestCache(ReaderActivity::setManga)
|
||||
viewerChaptersRelay.subscribeLatestCache(ReaderActivity::setChapters)
|
||||
isLoadingAdjacentChapterRelay.subscribeLatestCache(ReaderActivity::setProgressDialog)
|
||||
|
||||
// Read chapterList from an io thread because it's retrieved lazily and would block main.
|
||||
activeChapterSubscription?.unsubscribe()
|
||||
activeChapterSubscription = Observable
|
||||
.fromCallable { chapterList.first { chapterId == it.chapter.id } }
|
||||
.flatMap { getLoadObservable(loader!!, it) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst(
|
||||
{ _, _ ->
|
||||
// Ignore onNext event
|
||||
},
|
||||
ReaderActivity::setInitialChapterError,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable that loads the given [chapter] with this [loader]. This observable
|
||||
* handles main thread synchronization and updating the currently active chapters on
|
||||
* [viewerChaptersRelay], however callers must ensure there won't be more than one
|
||||
* subscription active by unsubscribing any existing [activeChapterSubscription] before.
|
||||
* Callers must also handle the onError event.
|
||||
*/
|
||||
private fun getLoadObservable(
|
||||
loader: ChapterLoader,
|
||||
chapter: ReaderChapter,
|
||||
): Observable<ViewerChapters> {
|
||||
return loader.loadChapter(chapter)
|
||||
.andThen(
|
||||
Observable.fromCallable {
|
||||
val chapterPos = chapterList.indexOf(chapter)
|
||||
|
||||
ViewerChapters(
|
||||
chapter,
|
||||
chapterList.getOrNull(chapterPos - 1),
|
||||
chapterList.getOrNull(chapterPos + 1),
|
||||
)
|
||||
},
|
||||
)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { newChapters ->
|
||||
val oldChapters = viewerChaptersRelay.value
|
||||
|
||||
// Add new references first to avoid unnecessary recycling
|
||||
newChapters.ref()
|
||||
oldChapters?.unref()
|
||||
|
||||
chapterDownload = deleteChapterFromDownloadQueue(newChapters.currChapter)
|
||||
viewerChaptersRelay.call(newChapters)
|
||||
}
|
||||
}
|
||||
|
||||
fun canLoadUrl(uri: Uri): Boolean {
|
||||
val host = uri.host ?: return false
|
||||
val delegatedSource = sourceManager.getDelegatedSource(host) ?: return false
|
||||
|
@ -338,7 +291,7 @@ class ReaderPresenter(
|
|||
@Suppress("DEPRECATION")
|
||||
suspend fun loadChapterURL(url: Uri) {
|
||||
val host = url.host ?: return
|
||||
val context = view ?: preferences.context
|
||||
val context = Injekt.get<Application>()
|
||||
val delegatedSource = sourceManager.getDelegatedSource(host) ?: error(
|
||||
context.getString(R.string.source_not_installed),
|
||||
)
|
||||
|
@ -357,14 +310,8 @@ class ReaderPresenter(
|
|||
httpSource?.baseUrl?.contains(domainName) == true
|
||||
}
|
||||
}
|
||||
if (dbChapter?.manga_id != null) {
|
||||
val dbManga = db.getManga(dbChapter.manga_id!!).executeOnIO()
|
||||
if (dbManga != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
init(dbManga, dbChapter.id!!)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (dbChapter?.manga_id?.let { init(it, dbChapter.id!!).isSuccess } == true) {
|
||||
return
|
||||
}
|
||||
}
|
||||
val info = delegatedSource.fetchMangaFromChapterUrl(url)
|
||||
|
@ -377,7 +324,7 @@ class ReaderPresenter(
|
|||
db.getChapters(manga).executeOnIO().find { it.url == chapter.url }?.id
|
||||
if (matchingChapterId != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
this@ReaderPresenter.init(manga, matchingChapterId)
|
||||
this@ReaderViewModel.init(manga.id!!, matchingChapterId)
|
||||
}
|
||||
} else {
|
||||
val chapterId: Long
|
||||
|
@ -397,7 +344,7 @@ class ReaderPresenter(
|
|||
)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
init(manga, chapterId)
|
||||
init(manga.id!!, chapterId)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -409,42 +356,86 @@ class ReaderPresenter(
|
|||
* Called when the user changed to the given [chapter] when changing pages from the viewer.
|
||||
* It's used only to set this chapter as active.
|
||||
*/
|
||||
private fun loadNewChapter(chapter: ReaderChapter) {
|
||||
private suspend fun loadNewChapter(chapter: ReaderChapter) {
|
||||
val loader = loader ?: return
|
||||
|
||||
Timber.d("Loading ${chapter.chapter.url}")
|
||||
|
||||
activeChapterSubscription?.unsubscribe()
|
||||
activeChapterSubscription = getLoadObservable(loader, chapter)
|
||||
.toCompletable()
|
||||
.onErrorComplete()
|
||||
.subscribe()
|
||||
.also(::add)
|
||||
// activeChapterSubscription?.unsubscribe()
|
||||
// activeChapterSubscription = getLoadObservable(loader, chapter)
|
||||
// .toCompletable()
|
||||
// .onErrorComplete()
|
||||
// .subscribe()
|
||||
// .also(::add)
|
||||
withIOContext {
|
||||
try {
|
||||
loadChapter(loader, chapter)
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) {
|
||||
throw e
|
||||
}
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadChapter(chapter: Chapter) {
|
||||
val loader = loader ?: return
|
||||
/**
|
||||
* Loads the given [chapter] with this [loader] and updates the currently active chapters.
|
||||
* Callers must handle errors.
|
||||
*/
|
||||
private suspend fun loadChapter(
|
||||
loader: ChapterLoader,
|
||||
chapter: ReaderChapter,
|
||||
): ViewerChapters {
|
||||
loader.loadChapter(chapter)
|
||||
|
||||
viewerChaptersRelay.value?.currChapter?.let(::saveReadingProgress)
|
||||
val chapterPos = chapterList.indexOf(chapter)
|
||||
// chapter.requestedPage = chapter.chapter.last_page_read
|
||||
val newChapters = ViewerChapters(
|
||||
chapter,
|
||||
chapterList.getOrNull(chapterPos - 1),
|
||||
chapterList.getOrNull(chapterPos + 1),
|
||||
)
|
||||
|
||||
Timber.d("Loading ${chapter.url}")
|
||||
withUIContext {
|
||||
mutableState.update {
|
||||
// Add new references first to avoid unnecessary recycling
|
||||
newChapters.ref()
|
||||
it.viewerChapters?.unref()
|
||||
|
||||
activeChapterSubscription?.unsubscribe()
|
||||
val lastPage = if (chapter.pages_left <= 1) 0 else chapter.last_page_read
|
||||
activeChapterSubscription = getLoadObservable(loader, ReaderChapter(chapter))
|
||||
.doOnSubscribe { isLoadingAdjacentChapterRelay.call(true) }
|
||||
.doOnUnsubscribe { isLoadingAdjacentChapterRelay.call(false) }
|
||||
.subscribeFirst(
|
||||
{ view, _ ->
|
||||
scope.launchUI {
|
||||
view.moveToPageIndex(lastPage, false, chapterChange = true)
|
||||
}
|
||||
view.refreshChapters()
|
||||
},
|
||||
{ _, _ ->
|
||||
// Ignore onError event, viewers handle that state
|
||||
},
|
||||
)
|
||||
chapterToDownload = deleteChapterFromDownloadQueue(newChapters.currChapter)
|
||||
it.copy(viewerChapters = newChapters)
|
||||
}
|
||||
}
|
||||
return newChapters
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user is going to load the prev/next chapter through the menu button.
|
||||
*/
|
||||
suspend fun loadChapter(chapter: ReaderChapter): Int? {
|
||||
val loader = loader ?: return -1
|
||||
|
||||
Timber.d("Loading adjacent ${chapter.chapter.url}")
|
||||
var lastPage: Int? = null
|
||||
mutableState.update { it.copy(isLoadingAdjacentChapter = true) }
|
||||
try {
|
||||
withIOContext {
|
||||
loadChapter(loader, chapter)
|
||||
lastPage =
|
||||
if (chapter.chapter.pages_left <= 1) 0 else chapter.chapter.last_page_read
|
||||
withUIContext {
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) {
|
||||
throw e
|
||||
}
|
||||
Timber.e(e)
|
||||
} finally {
|
||||
mutableState.update { it.copy(isLoadingAdjacentChapter = false) }
|
||||
}
|
||||
return lastPage
|
||||
}
|
||||
|
||||
fun toggleBookmark(chapter: Chapter) {
|
||||
|
@ -456,7 +447,7 @@ class ReaderPresenter(
|
|||
* Called when the viewers decide it's a good time to preload a [chapter] and improve the UX so
|
||||
* that the user doesn't have to wait too long to continue reading.
|
||||
*/
|
||||
private fun preload(chapter: ReaderChapter) {
|
||||
private suspend fun preload(chapter: ReaderChapter) {
|
||||
if (chapter.pageLoader is HttpPageLoader) {
|
||||
val manga = manga ?: return
|
||||
val isDownloaded = downloadManager.isChapterDownloaded(chapter.chapter, manga)
|
||||
|
@ -472,32 +463,22 @@ class ReaderPresenter(
|
|||
Timber.d("Preloading ${chapter.chapter.url}")
|
||||
|
||||
val loader = loader ?: return
|
||||
|
||||
loader.loadChapter(chapter)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
// Update current chapters whenever a chapter is preloaded
|
||||
.doOnCompleted { viewerChaptersRelay.value?.let(viewerChaptersRelay::call) }
|
||||
.onErrorComplete()
|
||||
.subscribe()
|
||||
.also(::add)
|
||||
withIOContext {
|
||||
try {
|
||||
loader.loadChapter(chapter)
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) {
|
||||
throw e
|
||||
}
|
||||
return@withIOContext
|
||||
}
|
||||
eventChannel.trySend(Event.ReloadViewerChapters)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the activity to load and set the next chapter as active.
|
||||
*/
|
||||
fun loadNextChapter(): Boolean {
|
||||
val nextChapter = viewerChaptersRelay.value?.nextChapter ?: return false
|
||||
loadChapter(nextChapter.chapter)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the activity to load and set the previous chapter as active.
|
||||
*/
|
||||
fun loadPreviousChapter(): Boolean {
|
||||
val prevChapter = viewerChaptersRelay.value?.prevChapter ?: return false
|
||||
loadChapter(prevChapter.chapter)
|
||||
return true
|
||||
fun adjacentChapter(next: Boolean): ReaderChapter? {
|
||||
val chapters = state.value.viewerChapters
|
||||
return if (next) chapters?.nextChapter else chapters?.prevChapter
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -506,7 +487,7 @@ class ReaderPresenter(
|
|||
* [page]'s chapter is different from the currently active.
|
||||
*/
|
||||
fun onPageSelected(page: ReaderPage, hasExtraPage: Boolean) {
|
||||
val currentChapters = viewerChaptersRelay.value ?: return
|
||||
val currentChapters = state.value.viewerChapters ?: return
|
||||
|
||||
val selectedChapter = page.chapter
|
||||
|
||||
|
@ -531,7 +512,7 @@ class ReaderPresenter(
|
|||
Timber.d("Setting ${selectedChapter.chapter.url} as active")
|
||||
saveReadingProgress(currentChapters.currChapter)
|
||||
setReadStartTime()
|
||||
loadNewChapter(selectedChapter)
|
||||
scope.launch { loadNewChapter(selectedChapter) }
|
||||
}
|
||||
val pages = page.chapter.pages ?: return
|
||||
val inDownloadRange = page.number.toDouble() / pages.size > 0.2
|
||||
|
@ -543,7 +524,7 @@ class ReaderPresenter(
|
|||
private fun downloadNextChapters() {
|
||||
val manga = manga ?: return
|
||||
if (getCurrentChapter()?.pageLoader !is DownloadPageLoader) return
|
||||
val nextChapter = viewerChaptersRelay.value?.nextChapter?.chapter ?: return
|
||||
val nextChapter = state.value.viewerChapters?.nextChapter?.chapter ?: return
|
||||
val chaptersNumberToDownload = preferences.autoDownloadWhileReading().get()
|
||||
if (chaptersNumberToDownload == 0 || !manga.favorite) return
|
||||
val isNextChapterDownloaded = downloadManager.isChapterDownloaded(nextChapter, manga)
|
||||
|
@ -596,10 +577,10 @@ class ReaderPresenter(
|
|||
val removeAfterReadSlots = preferences.removeAfterReadSlots()
|
||||
val chapterToDelete = chapterList.getOrNull(currentChapterPosition - removeAfterReadSlots)
|
||||
|
||||
if (removeAfterReadSlots != 0 && chapterDownload != null) {
|
||||
downloadManager.addDownloadsToStartOfQueue(listOf(chapterDownload!!))
|
||||
if (removeAfterReadSlots != 0 && chapterToDownload != null) {
|
||||
downloadManager.addDownloadsToStartOfQueue(listOf(chapterToDownload!!))
|
||||
} else {
|
||||
chapterDownload = null
|
||||
chapterToDownload = null
|
||||
}
|
||||
// Check if deleting option is enabled and chapter exists
|
||||
if (removeAfterReadSlots != -1 && chapterToDelete != null) {
|
||||
|
@ -654,7 +635,7 @@ class ReaderPresenter(
|
|||
/**
|
||||
* Called from the activity to preload the given [chapter].
|
||||
*/
|
||||
fun preloadChapter(chapter: ReaderChapter) {
|
||||
suspend fun preloadChapter(chapter: ReaderChapter) {
|
||||
preload(chapter)
|
||||
}
|
||||
|
||||
|
@ -662,7 +643,7 @@ class ReaderPresenter(
|
|||
* Returns the currently active chapter.
|
||||
*/
|
||||
fun getCurrentChapter(): ReaderChapter? {
|
||||
return viewerChaptersRelay.value?.currChapter
|
||||
return state.value.viewerChapters?.currChapter
|
||||
}
|
||||
|
||||
fun getChapterUrl(): String? {
|
||||
|
@ -702,22 +683,25 @@ class ReaderPresenter(
|
|||
*/
|
||||
fun setMangaReadingMode(readingModeType: Int) {
|
||||
val manga = manga ?: return
|
||||
manga.readingModeType = readingModeType
|
||||
db.updateViewerFlags(manga).executeAsBlocking()
|
||||
|
||||
Observable.timer(250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, _ ->
|
||||
val currChapters = viewerChaptersRelay.value
|
||||
if (currChapters != null) {
|
||||
// Save current page
|
||||
val currChapter = currChapters.currChapter
|
||||
currChapter.requestedPage = currChapter.chapter.last_page_read
|
||||
runBlocking(Dispatchers.IO) {
|
||||
manga.readingModeType = readingModeType
|
||||
db.updateViewerFlags(manga).executeAsBlocking()
|
||||
val currChapters = state.value.viewerChapters
|
||||
if (currChapters != null) {
|
||||
// Save current page
|
||||
val currChapter = currChapters.currChapter
|
||||
currChapter.requestedPage = currChapter.chapter.last_page_read
|
||||
|
||||
// Emit manga and chapters to the new viewer
|
||||
view.setManga(manga)
|
||||
view.setChapters(currChapters)
|
||||
mutableState.update {
|
||||
it.copy(
|
||||
manga = db.getManga(manga.id!!).executeAsBlocking(),
|
||||
viewerChapters = currChapters,
|
||||
)
|
||||
}
|
||||
},)
|
||||
eventChannel.send(Event.ReloadMangaAndChapters)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -736,18 +720,26 @@ class ReaderPresenter(
|
|||
*/
|
||||
fun setMangaOrientationType(rotationType: Int) {
|
||||
val manga = manga ?: return
|
||||
manga.orientationType = rotationType
|
||||
this.manga?.orientationType = rotationType
|
||||
db.updateViewerFlags(manga).executeAsBlocking()
|
||||
|
||||
Timber.i("Manga orientation is ${manga.orientationType}")
|
||||
|
||||
Observable.timer(250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, _ ->
|
||||
val currChapters = viewerChaptersRelay.value
|
||||
if (currChapters != null) {
|
||||
view.setOrientation(getMangaOrientationType())
|
||||
viewModelScope.launchIO {
|
||||
// delay(250)
|
||||
db.updateViewerFlags(manga).executeAsBlocking()
|
||||
val currChapters = state.value.viewerChapters
|
||||
if (currChapters != null) {
|
||||
mutableState.update {
|
||||
it.copy(
|
||||
manga = db.getManga(manga.id!!).executeAsBlocking(),
|
||||
viewerChapters = currChapters,
|
||||
)
|
||||
}
|
||||
},)
|
||||
eventChannel.send(Event.SetOrientation(getMangaOrientationType()))
|
||||
eventChannel.send(Event.ReloadViewerChapters)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -776,7 +768,7 @@ class ReaderPresenter(
|
|||
}
|
||||
|
||||
/**
|
||||
* Saves the image of this [page] in the given [directory] and returns the file location.
|
||||
* Saves the image of [page1] and [page2] in the given [directory] and returns the file location.
|
||||
*/
|
||||
private fun saveImages(page1: ReaderPage, page2: ReaderPage, isLTR: Boolean, @ColorInt bg: Int, directory: File, manga: Manga): File {
|
||||
val stream1 = page1.stream!!
|
||||
|
@ -814,7 +806,7 @@ class ReaderPresenter(
|
|||
* There's also a notification to allow sharing the image somewhere else or deleting it.
|
||||
*/
|
||||
fun saveImage(page: ReaderPage) {
|
||||
if (page.status != Page.READY) return
|
||||
if (page.status != Page.State.READY) return
|
||||
val manga = manga ?: return
|
||||
val context = Injekt.get<Application>()
|
||||
|
||||
|
@ -832,24 +824,23 @@ class ReaderPresenter(
|
|||
}
|
||||
|
||||
// Copy file in background.
|
||||
Observable.fromCallable { saveImage(page, destDir, manga) }
|
||||
.doOnNext { file ->
|
||||
viewModelScope.launchNonCancellable {
|
||||
try {
|
||||
val file = saveImage(page, destDir, manga)
|
||||
DiskUtil.scanMedia(context, file)
|
||||
notifier.onComplete(file)
|
||||
eventChannel.send(Event.SavedImage(SaveImageResult.Success(file)))
|
||||
} catch (e: Exception) {
|
||||
notifier.onError(e.message)
|
||||
eventChannel.send(Event.SavedImage(SaveImageResult.Error(e)))
|
||||
}
|
||||
.doOnError { notifier.onError(it.message) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst(
|
||||
{ view, file -> view.onSaveImageResult(SaveImageResult.Success(file)) },
|
||||
{ view, error -> view.onSaveImageResult(SaveImageResult.Error(error)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun saveImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) {
|
||||
scope.launch {
|
||||
if (firstPage.status != Page.READY) return@launch
|
||||
if (secondPage.status != Page.READY) return@launch
|
||||
if (firstPage.status != Page.State.READY) return@launch
|
||||
if (secondPage.status != Page.State.READY) return@launch
|
||||
val manga = manga ?: return@launch
|
||||
val context = Injekt.get<Application>()
|
||||
|
||||
|
@ -870,9 +861,9 @@ class ReaderPresenter(
|
|||
val file = saveImages(firstPage, secondPage, isLTR, bg, destDir, manga)
|
||||
DiskUtil.scanMedia(context, file)
|
||||
notifier.onComplete(file)
|
||||
withUIContext { view?.onSaveImageResult(SaveImageResult.Success(file)) }
|
||||
eventChannel.send(Event.SavedImage(SaveImageResult.Success(file)))
|
||||
} catch (e: Exception) {
|
||||
withUIContext { view?.onSaveImageResult(SaveImageResult.Error(e)) }
|
||||
eventChannel.send(Event.SavedImage(SaveImageResult.Error(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -885,26 +876,23 @@ class ReaderPresenter(
|
|||
* image will be kept so it won't be taking lots of internal disk space.
|
||||
*/
|
||||
fun shareImage(page: ReaderPage) {
|
||||
if (page.status != Page.READY) return
|
||||
if (page.status != Page.State.READY) return
|
||||
val manga = manga ?: return
|
||||
val context = Injekt.get<Application>()
|
||||
|
||||
val destDir = File(context.cacheDir, "shared_image")
|
||||
|
||||
Observable.fromCallable { destDir.deleteRecursively() } // Keep only the last shared file
|
||||
.map { saveImage(page, destDir, manga) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst(
|
||||
{ view, file -> view.onShareImageResult(file, page) },
|
||||
{ _, _ -> },
|
||||
)
|
||||
viewModelScope.launchNonCancellable {
|
||||
destDir.deleteRecursively() // Keep only the last shared file
|
||||
val file = saveImage(page, destDir, manga)
|
||||
eventChannel.send(Event.ShareImage(file, page))
|
||||
}
|
||||
}
|
||||
|
||||
fun shareImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) {
|
||||
scope.launch {
|
||||
if (firstPage.status != Page.READY) return@launch
|
||||
if (secondPage.status != Page.READY) return@launch
|
||||
if (firstPage.status != Page.State.READY) return@launch
|
||||
if (secondPage.status != Page.State.READY) return@launch
|
||||
val manga = manga ?: return@launch
|
||||
val context = Injekt.get<Application>()
|
||||
|
||||
|
@ -912,10 +900,8 @@ class ReaderPresenter(
|
|||
destDir.deleteRecursively()
|
||||
try {
|
||||
val file = saveImages(firstPage, secondPage, isLTR, bg, destDir, manga)
|
||||
withUIContext {
|
||||
view?.onShareImageResult(file, firstPage, secondPage)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
eventChannel.send(Event.ShareImage(file, firstPage, secondPage))
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -924,12 +910,12 @@ class ReaderPresenter(
|
|||
* Sets the image of this [page] as cover and notifies the UI of the result.
|
||||
*/
|
||||
fun setAsCover(page: ReaderPage) {
|
||||
if (page.status != Page.READY) return
|
||||
if (page.status != Page.State.READY) return
|
||||
val manga = manga ?: return
|
||||
val stream = page.stream ?: return
|
||||
|
||||
Observable
|
||||
.fromCallable {
|
||||
viewModelScope.launchNonCancellable {
|
||||
val result = try {
|
||||
if (manga.isLocal()) {
|
||||
val context = Injekt.get<Application>()
|
||||
coverCache.deleteFromCache(manga)
|
||||
|
@ -944,13 +930,11 @@ class ReaderPresenter(
|
|||
SetAsCoverResult.AddToLibraryFirst
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
SetAsCoverResult.Error
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst(
|
||||
{ view, result -> view.onSetAsCoverResult(result) },
|
||||
{ view, _ -> view.onSetAsCoverResult(SetAsCoverResult.Error) },
|
||||
)
|
||||
eventChannel.send(Event.SetCoverResult(result))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1008,4 +992,21 @@ class ReaderPresenter(
|
|||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
data class State(
|
||||
val manga: Manga? = null,
|
||||
val viewerChapters: ViewerChapters? = null,
|
||||
val isLoadingAdjacentChapter: Boolean = false,
|
||||
val lastPage: Int? = null,
|
||||
)
|
||||
|
||||
sealed class Event {
|
||||
object ReloadViewerChapters : Event()
|
||||
object ReloadMangaAndChapters : Event()
|
||||
data class SetOrientation(val orientation: Int) : Event()
|
||||
data class SetCoverResult(val result: SetAsCoverResult) : Event()
|
||||
|
||||
data class SavedImage(val result: SaveImageResult) : Event()
|
||||
data class ShareImage(val file: File, val page: ReaderPage, val extraPage: ReaderPage? = null) : Event()
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ import android.widget.LinearLayout
|
|||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
|
@ -19,7 +20,7 @@ import com.mikepenz.fastadapter.listeners.ClickEventHook
|
|||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.ReaderChaptersSheetBinding
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderViewModel
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.system.isInNightMode
|
||||
|
@ -28,6 +29,7 @@ import eu.kanade.tachiyomi.util.view.collapse
|
|||
import eu.kanade.tachiyomi.util.view.expand
|
||||
import eu.kanade.tachiyomi.util.view.isCollapsed
|
||||
import eu.kanade.tachiyomi.util.view.isExpanded
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
@ -37,7 +39,7 @@ class ReaderChapterSheet @JvmOverloads constructor(context: Context, attrs: Attr
|
|||
LinearLayout(context, attrs) {
|
||||
|
||||
var sheetBehavior: BottomSheetBehavior<View>? = null
|
||||
lateinit var presenter: ReaderPresenter
|
||||
lateinit var viewModel: ReaderViewModel
|
||||
var adapter: FastAdapter<ReaderChapterItem>? = null
|
||||
private val itemAdapter = ItemAdapter<ReaderChapterItem>()
|
||||
var selectedChapterId = -1L
|
||||
|
@ -51,7 +53,7 @@ class ReaderChapterSheet @JvmOverloads constructor(context: Context, attrs: Attr
|
|||
}
|
||||
|
||||
fun setup(activity: ReaderActivity) {
|
||||
presenter = activity.presenter
|
||||
viewModel = activity.viewModel
|
||||
val fullPrimary = activity.getResourceColor(R.attr.colorSurface)
|
||||
|
||||
val primary = ColorUtils.setAlphaComponent(fullPrimary, 200)
|
||||
|
@ -78,7 +80,7 @@ class ReaderChapterSheet @JvmOverloads constructor(context: Context, attrs: Attr
|
|||
binding.chapterRecycler.alpha = if (sheetBehavior.isExpanded()) 1f else 0f
|
||||
binding.chapterRecycler.isClickable = sheetBehavior.isExpanded()
|
||||
binding.chapterRecycler.isFocusable = sheetBehavior.isExpanded()
|
||||
val canShowNav = presenter.getCurrentChapter()?.pages?.size ?: 1 > 1
|
||||
val canShowNav = viewModel.getCurrentChapter()?.pages?.size ?: 1 > 1
|
||||
if (canShowNav) {
|
||||
activity.binding.readerNav.root.isVisible = sheetBehavior.isCollapsed()
|
||||
}
|
||||
|
@ -87,6 +89,7 @@ class ReaderChapterSheet @JvmOverloads constructor(context: Context, attrs: Attr
|
|||
sheetBehavior?.addBottomSheetCallback(
|
||||
object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onSlide(bottomSheet: View, progress: Float) {
|
||||
binding.root.isVisible = true
|
||||
binding.pill.alpha = (1 - max(0f, progress)) * 0.25f
|
||||
val trueProgress = max(progress, 0f)
|
||||
activity.binding.readerNav.root.alpha = (1 - abs(progress)).coerceIn(0f, 1f)
|
||||
|
@ -100,11 +103,11 @@ class ReaderChapterSheet @JvmOverloads constructor(context: Context, attrs: Attr
|
|||
}
|
||||
|
||||
override fun onStateChanged(p0: View, state: Int) {
|
||||
val canShowNav = (presenter.getCurrentChapter()?.pages?.size ?: 1) > 1
|
||||
val canShowNav = (viewModel.getCurrentChapter()?.pages?.size ?: 1) > 1
|
||||
if (state == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||
sheetBehavior?.isHideable = false
|
||||
(binding.chapterRecycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(
|
||||
adapter?.getPosition(presenter.getCurrentChapter()?.chapter?.id ?: 0L) ?: 0,
|
||||
adapter?.getPosition(viewModel.getCurrentChapter()?.chapter?.id ?: 0L) ?: 0,
|
||||
binding.chapterRecycler.height / 2 - 30.dpToPx,
|
||||
)
|
||||
if (canShowNav) {
|
||||
|
@ -130,6 +133,9 @@ class ReaderChapterSheet @JvmOverloads constructor(context: Context, attrs: Attr
|
|||
if (canShowNav) {
|
||||
activity.binding.readerNav.root.isInvisible = true
|
||||
}
|
||||
binding.root.isInvisible = true
|
||||
} else if (binding.root.isVisible) {
|
||||
binding.root.isVisible = true
|
||||
}
|
||||
binding.chapterRecycler.isClickable = state == BottomSheetBehavior.STATE_EXPANDED
|
||||
binding.chapterRecycler.isFocusable = state == BottomSheetBehavior.STATE_EXPANDED
|
||||
|
@ -144,16 +150,18 @@ class ReaderChapterSheet @JvmOverloads constructor(context: Context, attrs: Attr
|
|||
if (!sheetBehavior.isExpanded() || activity.isLoading) {
|
||||
false
|
||||
} else {
|
||||
if (item.chapter.id != presenter.getCurrentChapter()?.chapter?.id) {
|
||||
if (item.chapter.id != viewModel.getCurrentChapter()?.chapter?.id) {
|
||||
activity.binding.readerNav.leftChapter.isInvisible = true
|
||||
activity.binding.readerNav.rightChapter.isInvisible = true
|
||||
activity.isScrollingThroughPagesOrChapters = true
|
||||
|
||||
presenter.loadChapter(item.chapter)
|
||||
loadingPos = position
|
||||
val itemView = (binding.chapterRecycler.findViewHolderForAdapterPosition(position) as? ReaderChapterItem.ViewHolder)?.binding
|
||||
itemView?.bookmarkImage?.isVisible = false
|
||||
itemView?.progress?.isVisible = true
|
||||
activity.lifecycleScope.launch {
|
||||
activity.loadChapter(item.chapter)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
@ -175,7 +183,7 @@ class ReaderChapterSheet @JvmOverloads constructor(context: Context, attrs: Attr
|
|||
item: ReaderChapterItem,
|
||||
) {
|
||||
if (!activity.isLoading && sheetBehavior.isExpanded()) {
|
||||
presenter.toggleBookmark(item.chapter)
|
||||
viewModel.toggleBookmark(item.chapter)
|
||||
refreshList()
|
||||
}
|
||||
}
|
||||
|
@ -217,14 +225,14 @@ class ReaderChapterSheet @JvmOverloads constructor(context: Context, attrs: Attr
|
|||
|
||||
fun refreshList() {
|
||||
launchUI {
|
||||
val chapters = presenter.getChapters()
|
||||
val chapters = viewModel.getChapters()
|
||||
|
||||
selectedChapterId = chapters.find { it.isCurrent }?.chapter?.id ?: -1L
|
||||
itemAdapter.clear()
|
||||
itemAdapter.add(chapters)
|
||||
|
||||
(binding.chapterRecycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(
|
||||
adapter?.getPosition(presenter.getCurrentChapter()?.chapter?.id ?: 0L) ?: 0,
|
||||
adapter?.getPosition(viewModel.getCurrentChapter()?.chapter?.id ?: 0L) ?: 0,
|
||||
binding.chapterRecycler.height / 2 - 30.dpToPx,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,66 +1,64 @@
|
|||
package eu.kanade.tachiyomi.ui.reader.loader
|
||||
|
||||
import android.content.Context
|
||||
import com.github.junrar.exception.UnsupportedRarV5Exception
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import eu.kanade.tachiyomi.util.system.withIOContext
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Loader used to retrieve the [PageLoader] for a given chapter.
|
||||
*/
|
||||
class ChapterLoader(
|
||||
private val context: Context,
|
||||
private val downloadManager: DownloadManager,
|
||||
private val downloadProvider: DownloadProvider,
|
||||
private val manga: Manga,
|
||||
private val source: Source,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Returns a completable that assigns the page loader and loads the its pages. It just
|
||||
* completes if the chapter is already loaded.
|
||||
* Assigns the chapter's page loader and loads the its pages. Returns immediately if the chapter
|
||||
* is already loaded.
|
||||
*/
|
||||
fun loadChapter(chapter: ReaderChapter): Completable {
|
||||
suspend fun loadChapter(chapter: ReaderChapter) {
|
||||
if (chapterIsReady(chapter)) {
|
||||
return Completable.complete()
|
||||
return
|
||||
}
|
||||
|
||||
return Observable.just(chapter)
|
||||
.doOnNext { chapter.state = ReaderChapter.State.Loading }
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMap { readerChapter ->
|
||||
Timber.d("Loading pages for ${chapter.chapter.name}")
|
||||
|
||||
val loader = getPageLoader(readerChapter)
|
||||
chapter.state = ReaderChapter.State.Loading
|
||||
withIOContext {
|
||||
Timber.d("Loading pages for ${chapter.chapter.name}")
|
||||
try {
|
||||
val loader = getPageLoader(chapter)
|
||||
chapter.pageLoader = loader
|
||||
|
||||
loader.getPages().take(1).doOnNext { pages ->
|
||||
pages.forEach { it.chapter = chapter }
|
||||
}
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { pages ->
|
||||
if (pages.isEmpty()) {
|
||||
throw Exception(downloadManager.context.getString(R.string.no_pages_found))
|
||||
}
|
||||
val pages = loader.getPages()
|
||||
.onEach { it.chapter = chapter }
|
||||
|
||||
chapter.state = ReaderChapter.State.Loaded(pages)
|
||||
if (pages.isEmpty()) {
|
||||
throw Exception(context.getString(R.string.no_pages_found))
|
||||
}
|
||||
|
||||
// If the chapter is partially read, set the starting page to the last the user read
|
||||
// otherwise use the requested page.
|
||||
if (!chapter.chapter.read) {
|
||||
chapter.requestedPage = chapter.chapter.last_page_read
|
||||
}
|
||||
|
||||
chapter.state = ReaderChapter.State.Loaded(pages)
|
||||
} catch (e: Throwable) {
|
||||
chapter.state = ReaderChapter.State.Error(e)
|
||||
throw e
|
||||
}
|
||||
.toCompletable()
|
||||
.doOnError { chapter.state = ReaderChapter.State.Error(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -74,9 +72,10 @@ class ChapterLoader(
|
|||
* Returns the page loader to use for this [chapter].
|
||||
*/
|
||||
private fun getPageLoader(chapter: ReaderChapter): PageLoader {
|
||||
val isDownloaded = downloadManager.isChapterDownloaded(chapter.chapter, manga, true)
|
||||
val dbChapter = chapter.chapter
|
||||
val isDownloaded = downloadManager.isChapterDownloaded(dbChapter, manga, skipCache = true)
|
||||
return when {
|
||||
isDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager)
|
||||
isDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager, downloadProvider)
|
||||
source is HttpSource -> HttpPageLoader(chapter, source)
|
||||
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
|
||||
when (format) {
|
||||
|
@ -85,12 +84,12 @@ class ChapterLoader(
|
|||
is LocalSource.Format.Rar -> try {
|
||||
RarPageLoader(format.file)
|
||||
} catch (e: UnsupportedRarV5Exception) {
|
||||
error(downloadManager.context.getString(R.string.loader_rar5_error))
|
||||
error(context.getString(R.string.loader_rar5_error))
|
||||
}
|
||||
is LocalSource.Format.Epub -> EpubPageLoader(format.file)
|
||||
}
|
||||
}
|
||||
else -> error(downloadManager.context.getString(R.string.source_not_installed))
|
||||
else -> error(context.getString(R.string.source_not_installed))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import eu.kanade.tachiyomi.source.model.Page
|
|||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import rx.Observable
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
|
||||
|
@ -14,27 +13,23 @@ import java.io.FileInputStream
|
|||
class DirectoryPageLoader(val file: File) : PageLoader() {
|
||||
|
||||
/**
|
||||
* Returns an observable containing the pages found on this directory ordered with a natural
|
||||
* comparator.
|
||||
* Returns the pages found on this directory ordered with a natural comparator.
|
||||
*/
|
||||
override fun getPages(): Observable<List<ReaderPage>> {
|
||||
override suspend fun getPages(): List<ReaderPage> {
|
||||
return file.listFiles()
|
||||
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||
.sortedWith(Comparator<File> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) })
|
||||
.mapIndexed { i, file ->
|
||||
?.filter { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
?.mapIndexed { i, file ->
|
||||
val streamFn = { FileInputStream(file) }
|
||||
ReaderPage(i).apply {
|
||||
stream = streamFn
|
||||
status = Page.READY
|
||||
status = Page.State.READY
|
||||
}
|
||||
}
|
||||
.let { Observable.just(it) }
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable that emits a ready state.
|
||||
* No additional action required to load the page
|
||||
*/
|
||||
override fun getPage(page: ReaderPage): Observable<Int> {
|
||||
return Observable.just(Page.READY)
|
||||
}
|
||||
override suspend fun loadPage(page: ReaderPage) {}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.reader.loader
|
|||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
||||
|
@ -9,12 +10,8 @@ import eu.kanade.tachiyomi.source.Source
|
|||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
/**
|
||||
* Loader used to load a chapter from the downloaded chapters.
|
||||
|
@ -24,69 +21,49 @@ class DownloadPageLoader(
|
|||
private val manga: Manga,
|
||||
private val source: Source,
|
||||
private val downloadManager: DownloadManager,
|
||||
private val downloadProvider: DownloadProvider,
|
||||
) : PageLoader() {
|
||||
|
||||
/**
|
||||
* The application context. Needed to open input streams.
|
||||
*/
|
||||
private val context by injectLazy<Application>()
|
||||
private val downloadProvider by lazy { DownloadProvider(context) }
|
||||
// Needed to open input streams
|
||||
private val context: Application by injectLazy()
|
||||
|
||||
private var zipPageLoader: ZipPageLoader? = null
|
||||
|
||||
override fun recycle() {
|
||||
super.recycle()
|
||||
zipPageLoader?.recycle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable containing the pages found on this downloaded chapter.
|
||||
* Returns the pages found on this downloaded chapter.
|
||||
*/
|
||||
override fun getPages(): Observable<List<ReaderPage>> {
|
||||
val chapterPath = downloadProvider.findChapterDir(chapter.chapter, manga, source)
|
||||
if (chapterPath?.isFile == true) {
|
||||
val zip = if (!File(chapterPath.filePath!!).canRead()) {
|
||||
val tmpFile = File.createTempFile(chapterPath.name!!.replace(".cbz", ""), ".cbz")
|
||||
val buffer = ByteArray(1024)
|
||||
chapterPath.openInputStream().use { input ->
|
||||
tmpFile.outputStream().use { fileOut ->
|
||||
while (true) {
|
||||
val length = input.read(buffer)
|
||||
if (length <= 0) break
|
||||
fileOut.write(buffer, 0, length)
|
||||
}
|
||||
fileOut.flush()
|
||||
}
|
||||
}
|
||||
ZipFile(tmpFile.absolutePath)
|
||||
} else {
|
||||
ZipFile(chapterPath.filePath)
|
||||
}
|
||||
return zip.entries().toList()
|
||||
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.mapIndexed { i, entry ->
|
||||
val streamFn = { zip.getInputStream(entry) }
|
||||
ReaderPage(i).apply {
|
||||
stream = streamFn
|
||||
status = Page.READY
|
||||
}
|
||||
}
|
||||
.let { Observable.just(it) }
|
||||
override suspend fun getPages(): List<ReaderPage> {
|
||||
val dbChapter = chapter.chapter
|
||||
val chapterPath = downloadProvider.findChapterDir(dbChapter, manga, source)
|
||||
return if (chapterPath?.isFile == true) {
|
||||
getPagesFromArchive(chapterPath)
|
||||
} else {
|
||||
return downloadManager.buildPageList(source, manga, chapter.chapter)
|
||||
.map { pages ->
|
||||
pages.map { page ->
|
||||
ReaderPage(
|
||||
page.index,
|
||||
page.url,
|
||||
page.imageUrl,
|
||||
|
||||
{
|
||||
context.contentResolver.openInputStream(page.uri ?: Uri.EMPTY)!!
|
||||
},
|
||||
).apply {
|
||||
status = Page.READY
|
||||
}
|
||||
}
|
||||
}
|
||||
getPagesFromDirectory()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPage(page: ReaderPage): Observable<Int> {
|
||||
return Observable.just(Page.READY) // TODO maybe check if file still exists?
|
||||
private suspend fun getPagesFromArchive(chapterPath: UniFile): List<ReaderPage> {
|
||||
val loader = ZipPageLoader(File(chapterPath.filePath!!)).also { zipPageLoader = it }
|
||||
return loader.getPages()
|
||||
}
|
||||
|
||||
private fun getPagesFromDirectory(): List<ReaderPage> {
|
||||
val pages = downloadManager.buildPageList(source, manga, chapter.chapter)
|
||||
return pages.map { page ->
|
||||
ReaderPage(page.index, page.url, page.imageUrl, stream = {
|
||||
context.contentResolver.openInputStream(page.uri ?: Uri.EMPTY)!!
|
||||
},).apply {
|
||||
status = Page.State.READY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun loadPage(page: ReaderPage) {
|
||||
zipPageLoader?.loadPage(page)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ 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.util.storage.EpubFile
|
||||
import rx.Observable
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
|
@ -25,31 +24,23 @@ class EpubPageLoader(file: File) : PageLoader() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns an observable containing the pages found on this zip archive ordered with a natural
|
||||
* comparator.
|
||||
* Returns the pages found on this zip archive ordered with a natural comparator.
|
||||
*/
|
||||
override fun getPages(): Observable<List<ReaderPage>> {
|
||||
override suspend fun getPages(): List<ReaderPage> {
|
||||
return epub.getImagesFromPages()
|
||||
.mapIndexed { i, path ->
|
||||
val streamFn = { epub.getInputStream(epub.getEntry(path)!!) }
|
||||
ReaderPage(i).apply {
|
||||
stream = streamFn
|
||||
status = Page.READY
|
||||
status = Page.State.READY
|
||||
}
|
||||
}
|
||||
.let { Observable.just(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable that emits a ready state unless the loader was recycled.
|
||||
* No additional action required to load the page
|
||||
*/
|
||||
override fun getPage(page: ReaderPage): Observable<Int> {
|
||||
return Observable.just(
|
||||
if (isRecycled) {
|
||||
Page.ERROR
|
||||
} else {
|
||||
Page.READY
|
||||
},
|
||||
)
|
||||
override suspend fun loadPage(page: ReaderPage) {
|
||||
check(!isRecycled)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,24 @@
|
|||
package eu.kanade.tachiyomi.ui.reader.loader
|
||||
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||
import eu.kanade.tachiyomi.util.system.awaitSingle
|
||||
import eu.kanade.tachiyomi.util.system.launchIO
|
||||
import eu.kanade.tachiyomi.util.system.withIOContext
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import rx.schedulers.Schedulers
|
||||
import rx.subjects.PublishSubject
|
||||
import rx.subjects.SerializedSubject
|
||||
import rx.subscriptions.CompositeSubscription
|
||||
import timber.log.Timber
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.concurrent.PriorityBlockingQueue
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.math.min
|
||||
|
@ -36,38 +32,27 @@ class HttpPageLoader(
|
|||
private val chapterCache: ChapterCache = Injekt.get(),
|
||||
) : PageLoader() {
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
/**
|
||||
* A queue used to manage requests one by one while allowing priorities.
|
||||
*/
|
||||
private val queue = PriorityBlockingQueue<PriorityPage>()
|
||||
|
||||
private val subscriptions = CompositeSubscription()
|
||||
private val preloadSize = 4
|
||||
|
||||
private val preferences by injectLazy<PreferencesHelper>()
|
||||
private var preloadSize = preferences.preloadSize().get()
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
init {
|
||||
// Adding flow since we can reach reader settings after this is created
|
||||
preferences.preloadSize().asFlow()
|
||||
.onEach {
|
||||
preloadSize = it
|
||||
scope.launchIO {
|
||||
flow {
|
||||
while (true) {
|
||||
emit(runInterruptible { queue.take() }.page)
|
||||
}
|
||||
}
|
||||
.launchIn(scope)
|
||||
subscriptions += Observable.defer { Observable.just(queue.take().page) }
|
||||
.filter { it.status == Page.QUEUE }
|
||||
.concatMap { source.fetchImageFromCacheThenNet(it) }
|
||||
.repeat()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(
|
||||
{
|
||||
},
|
||||
{ error ->
|
||||
if (error !is InterruptedException) {
|
||||
Timber.e(error)
|
||||
}
|
||||
},
|
||||
)
|
||||
.filter { it.status == Page.State.QUEUE }
|
||||
.collect {
|
||||
_loadPage(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -76,77 +61,77 @@ class HttpPageLoader(
|
|||
override fun recycle() {
|
||||
super.recycle()
|
||||
scope.cancel()
|
||||
subscriptions.unsubscribe()
|
||||
queue.clear()
|
||||
|
||||
// Cache current page list progress for online chapters to allow a faster reopen
|
||||
val pages = chapter.pages
|
||||
if (pages != null) {
|
||||
Completable
|
||||
.fromAction {
|
||||
launchIO {
|
||||
try {
|
||||
// Convert to pages without reader information
|
||||
val pagesToSave = pages.map { Page(it.index, it.url, it.imageUrl) }
|
||||
chapterCache.putPageListToCache(chapter.chapter, pagesToSave)
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable with the page list for a chapter. It tries to return the page list from
|
||||
* the local cache, otherwise fallbacks to network.
|
||||
* Returns the page list for a chapter. It tries to return the page list from the local cache,
|
||||
* otherwise fallbacks to network.
|
||||
*/
|
||||
override fun getPages(): Observable<List<ReaderPage>> {
|
||||
return Observable.fromCallable { chapterCache.getPageListFromCache(chapter.chapter) }
|
||||
.onErrorResumeNext { source.fetchPageList(chapter.chapter) }
|
||||
.map { pages ->
|
||||
pages.mapIndexed { index, page ->
|
||||
// Don't trust sources and use our own indexing
|
||||
ReaderPage(index, page.url, page.imageUrl)
|
||||
}
|
||||
override suspend fun getPages(): List<ReaderPage> {
|
||||
val pages = try {
|
||||
chapterCache.getPageListFromCache(chapter.chapter)
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) {
|
||||
throw e
|
||||
}
|
||||
source.getPageList(chapter.chapter)
|
||||
}
|
||||
return pages.mapIndexed { index, page ->
|
||||
// Don't trust sources and use our own indexing
|
||||
ReaderPage(index, page.url, page.imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable that loads a page through the queue and listens to its result to
|
||||
* emit new states. It handles re-enqueueing pages if they were evicted from the cache.
|
||||
* Loads a page through the queue. Handles re-enqueueing pages if they were evicted from the cache.
|
||||
*/
|
||||
override fun getPage(page: ReaderPage): Observable<Int> {
|
||||
return Observable.defer {
|
||||
override suspend fun loadPage(page: ReaderPage) {
|
||||
withIOContext {
|
||||
val imageUrl = page.imageUrl
|
||||
|
||||
// Check if the image has been deleted
|
||||
if (page.status == Page.READY && imageUrl != null && !chapterCache.isImageInCache(imageUrl)) {
|
||||
page.status = Page.QUEUE
|
||||
if (page.status == Page.State.READY && imageUrl != null && !chapterCache.isImageInCache(imageUrl)) {
|
||||
page.status = Page.State.QUEUE
|
||||
}
|
||||
|
||||
// Automatically retry failed pages when subscribed to this page
|
||||
if (page.status == Page.ERROR) {
|
||||
page.status = Page.QUEUE
|
||||
if (page.status == Page.State.ERROR) {
|
||||
page.status = Page.State.QUEUE
|
||||
}
|
||||
|
||||
val statusSubject = SerializedSubject(PublishSubject.create<Int>())
|
||||
page.setStatusSubject(statusSubject)
|
||||
|
||||
val queuedPages = mutableListOf<PriorityPage>()
|
||||
if (page.status == Page.QUEUE) {
|
||||
if (page.status == Page.State.QUEUE) {
|
||||
queuedPages += PriorityPage(page, 1).also { queue.offer(it) }
|
||||
}
|
||||
queuedPages += preloadNextPages(page, preloadSize)
|
||||
|
||||
statusSubject.startWith(page.status)
|
||||
.doOnUnsubscribe {
|
||||
suspendCancellableCoroutine<Nothing> { continuation ->
|
||||
continuation.invokeOnCancellation {
|
||||
queuedPages.forEach {
|
||||
if (it.page.status == Page.QUEUE) {
|
||||
if (it.page.status == Page.State.QUEUE) {
|
||||
queue.remove(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.unsubscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -161,7 +146,7 @@ class HttpPageLoader(
|
|||
return pages
|
||||
.subList(pageIndex + 1, min(pageIndex + 1 + amount, pages.size))
|
||||
.mapNotNull {
|
||||
if (it.status == Page.QUEUE) {
|
||||
if (it.status == Page.State.QUEUE) {
|
||||
PriorityPage(it, 0).apply { queue.offer(this) }
|
||||
} else {
|
||||
null
|
||||
|
@ -173,8 +158,8 @@ class HttpPageLoader(
|
|||
* Retries a page. This method is only called from user interaction on the viewer.
|
||||
*/
|
||||
override fun retryPage(page: ReaderPage) {
|
||||
if (page.status == Page.ERROR) {
|
||||
page.status = Page.QUEUE
|
||||
if (page.status == Page.State.ERROR) {
|
||||
page.status = Page.State.QUEUE
|
||||
}
|
||||
queue.offer(PriorityPage(page, 2))
|
||||
}
|
||||
|
@ -186,7 +171,6 @@ class HttpPageLoader(
|
|||
val page: ReaderPage,
|
||||
val priority: Int,
|
||||
) : Comparable<PriorityPage> {
|
||||
|
||||
companion object {
|
||||
private val idGenerator = AtomicInteger()
|
||||
}
|
||||
|
@ -200,61 +184,32 @@ class HttpPageLoader(
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns an observable of the page with the downloaded image.
|
||||
* Loads the page, retrieving the image URL and downloading the image if necessary.
|
||||
* Downloaded images are stored in the chapter cache.
|
||||
*
|
||||
* @param page the page whose source image has to be downloaded.
|
||||
*/
|
||||
private fun HttpSource.fetchImageFromCacheThenNet(page: ReaderPage): Observable<ReaderPage> {
|
||||
return if (page.imageUrl.isNullOrEmpty()) {
|
||||
getImageUrl(page).flatMap { getCachedImage(it) }
|
||||
} else {
|
||||
getCachedImage(page)
|
||||
private suspend fun _loadPage(page: ReaderPage) {
|
||||
try {
|
||||
if (page.imageUrl.isNullOrEmpty()) {
|
||||
page.status = Page.State.LOAD_PAGE
|
||||
page.imageUrl = source.fetchImageUrl(page).awaitSingle()
|
||||
}
|
||||
val imageUrl = page.imageUrl!!
|
||||
|
||||
if (!chapterCache.isImageInCache(imageUrl)) {
|
||||
page.status = Page.State.DOWNLOAD_IMAGE
|
||||
val imageResponse = source.getImage(page)
|
||||
chapterCache.putImageToCache(imageUrl, imageResponse)
|
||||
}
|
||||
|
||||
page.stream = { chapterCache.getImageFile(imageUrl).inputStream() }
|
||||
page.status = Page.State.READY
|
||||
} catch (e: Throwable) {
|
||||
page.status = Page.State.ERROR
|
||||
if (e is CancellationException) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun HttpSource.getImageUrl(page: ReaderPage): Observable<ReaderPage> {
|
||||
page.status = Page.LOAD_PAGE
|
||||
return fetchImageUrl(page)
|
||||
.doOnError { page.status = Page.ERROR }
|
||||
.onErrorReturn { null }
|
||||
.doOnNext { page.imageUrl = it }
|
||||
.map { page }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable of the page that gets the image from the chapter or fallbacks to
|
||||
* network and copies it to the cache calling [cacheImage].
|
||||
*
|
||||
* @param page the page.
|
||||
*/
|
||||
private fun HttpSource.getCachedImage(page: ReaderPage): Observable<ReaderPage> {
|
||||
val imageUrl = page.imageUrl ?: return Observable.just(page)
|
||||
|
||||
return Observable.just(page)
|
||||
.flatMap {
|
||||
if (!chapterCache.isImageInCache(imageUrl)) {
|
||||
cacheImage(page)
|
||||
} else {
|
||||
Observable.just(page)
|
||||
}
|
||||
}
|
||||
.doOnNext {
|
||||
page.stream = { chapterCache.getImageFile(imageUrl).inputStream() }
|
||||
page.status = Page.READY
|
||||
}
|
||||
.doOnError { page.status = Page.ERROR }
|
||||
.onErrorReturn { page }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable of the page that downloads the image to [ChapterCache].
|
||||
*
|
||||
* @param page the page.
|
||||
*/
|
||||
private fun HttpSource.cacheImage(page: ReaderPage): Observable<ReaderPage> {
|
||||
page.status = Page.DOWNLOAD_IMAGE
|
||||
return fetchImage(page)
|
||||
.doOnNext { chapterCache.putImageToCache(page.imageUrl!!, it) }
|
||||
.map { page }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.reader.loader
|
|||
|
||||
import androidx.annotation.CallSuper
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import rx.Observable
|
||||
|
||||
/**
|
||||
* A loader used to load pages into the reader. Any open resources must be cleaned up when the
|
||||
|
@ -26,16 +25,16 @@ abstract class PageLoader {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns an observable containing the list of pages of a chapter. Only the first emission
|
||||
* will be used.
|
||||
* Returns the list of pages of a chapter.
|
||||
*/
|
||||
abstract fun getPages(): Observable<List<ReaderPage>>
|
||||
abstract suspend fun getPages(): List<ReaderPage>
|
||||
|
||||
/**
|
||||
* Returns an observable that should inform of the progress of the page (see the Page class
|
||||
* for the available states)
|
||||
* Loads the page. May also preload other pages.
|
||||
* Progress of the page loading should be followed via [page.statusFlow].
|
||||
* [loadPage] is not currently guaranteed to complete, so it should be launched asynchronously.
|
||||
*/
|
||||
abstract fun getPage(page: ReaderPage): Observable<Int>
|
||||
abstract suspend fun loadPage(page: ReaderPage)
|
||||
|
||||
/**
|
||||
* Retries the given [page] in case it failed to load. This method only makes sense when an
|
||||
|
|
|
@ -6,7 +6,6 @@ import eu.kanade.tachiyomi.source.model.Page
|
|||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import rx.Observable
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.PipedInputStream
|
||||
|
@ -38,33 +37,27 @@ class RarPageLoader(file: File) : PageLoader() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns an observable containing the pages found on this rar archive ordered with a natural
|
||||
* Returns an RxJava Single containing the pages found on this rar archive ordered with a natural
|
||||
* comparator.
|
||||
*/
|
||||
override fun getPages(): Observable<List<ReaderPage>> {
|
||||
override suspend fun getPages(): List<ReaderPage> {
|
||||
return archive.fileHeaders.asSequence()
|
||||
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||
.mapIndexed { i, header ->
|
||||
ReaderPage(i).apply {
|
||||
stream = { getStream(header) }
|
||||
status = Page.READY
|
||||
status = Page.State.READY
|
||||
}
|
||||
}
|
||||
.let { Observable.just(it.toList()) }
|
||||
.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable that emits a ready state unless the loader was recycled.
|
||||
* No additional action required to load the page
|
||||
*/
|
||||
override fun getPage(page: ReaderPage): Observable<Int> {
|
||||
return Observable.just(
|
||||
if (isRecycled) {
|
||||
Page.ERROR
|
||||
} else {
|
||||
Page.READY
|
||||
},
|
||||
)
|
||||
override suspend fun loadPage(page: ReaderPage) {
|
||||
check(!isRecycled)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,7 +5,6 @@ import eu.kanade.tachiyomi.source.model.Page
|
|||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import rx.Observable
|
||||
import java.io.File
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.zip.ZipFile
|
||||
|
@ -33,32 +32,25 @@ class ZipPageLoader(file: File) : PageLoader() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns an observable containing the pages found on this zip archive ordered with a natural
|
||||
* comparator.
|
||||
* Returns the pages found on this zip archive ordered with a natural comparator.
|
||||
*/
|
||||
override fun getPages(): Observable<List<ReaderPage>> {
|
||||
override suspend fun getPages(): List<ReaderPage> {
|
||||
return zip.entries().asSequence()
|
||||
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.mapIndexed { i, entry ->
|
||||
ReaderPage(i).apply {
|
||||
stream = { zip.getInputStream(entry) }
|
||||
status = Page.READY
|
||||
status = Page.State.READY
|
||||
}
|
||||
}
|
||||
.let { Observable.just(it.toList()) }
|
||||
.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable that emits a ready state unless the loader was recycled.
|
||||
* No additional action required to load the page
|
||||
*/
|
||||
override fun getPage(page: ReaderPage): Observable<Int> {
|
||||
return Observable.just(
|
||||
if (isRecycled) {
|
||||
Page.ERROR
|
||||
} else {
|
||||
Page.READY
|
||||
},
|
||||
)
|
||||
override suspend fun loadPage(page: ReaderPage) {
|
||||
check(!isRecycled)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,6 @@ class InsertPage(parent: ReaderPage) : ReaderPage(
|
|||
fullPage = true
|
||||
firstHalf = false
|
||||
stream = parent.stream
|
||||
status = READY
|
||||
status = State.READY
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,9 +15,9 @@ class ReaderGeneralView @JvmOverloads constructor(context: Context, attrs: Attri
|
|||
override fun initGeneralPreferences() {
|
||||
binding.viewerSeries.onItemSelectedListener = { position ->
|
||||
val readingModeType = ReadingModeType.fromSpinner(position)
|
||||
(context as ReaderActivity).presenter.setMangaReadingMode(readingModeType.flagValue)
|
||||
(context as ReaderActivity).viewModel.setMangaReadingMode(readingModeType.flagValue)
|
||||
|
||||
val mangaViewer = activity.presenter.getMangaReadingMode()
|
||||
val mangaViewer = activity.viewModel.getMangaReadingMode()
|
||||
if (mangaViewer == ReadingModeType.WEBTOON.flagValue || mangaViewer == ReadingModeType.CONTINUOUS_VERTICAL.flagValue) {
|
||||
initWebtoonPreferences()
|
||||
} else {
|
||||
|
@ -25,16 +25,16 @@ class ReaderGeneralView @JvmOverloads constructor(context: Context, attrs: Attri
|
|||
}
|
||||
}
|
||||
binding.viewerSeries.setSelection(
|
||||
(context as? ReaderActivity)?.presenter?.manga?.readingModeType?.let {
|
||||
(context as? ReaderActivity)?.viewModel?.state?.value?.manga?.readingModeType?.let {
|
||||
ReadingModeType.fromPreference(it).prefValue
|
||||
} ?: 0,
|
||||
)
|
||||
binding.rotationMode.onItemSelectedListener = { position ->
|
||||
val rotationType = OrientationType.fromSpinner(position)
|
||||
(context as ReaderActivity).presenter.setMangaOrientationType(rotationType.flagValue)
|
||||
(context as ReaderActivity).viewModel.setMangaOrientationType(rotationType.flagValue)
|
||||
}
|
||||
binding.rotationMode.setSelection(
|
||||
(context as ReaderActivity).presenter.manga?.orientationType?.let {
|
||||
(context as ReaderActivity).viewModel.manga?.orientationType?.let {
|
||||
OrientationType.fromPreference(it).prefValue
|
||||
} ?: 0,
|
||||
)
|
||||
|
|
|
@ -19,7 +19,7 @@ class ReaderPagedView @JvmOverloads constructor(context: Context, attrs: Attribu
|
|||
override fun initGeneralPreferences() {
|
||||
with(binding) {
|
||||
scaleType.bindToPreference(preferences.imageScaleType(), 1) {
|
||||
val mangaViewer = (context as? ReaderActivity)?.presenter?.getMangaReadingMode() ?: 0
|
||||
val mangaViewer = (context as? ReaderActivity)?.viewModel?.getMangaReadingMode() ?: 0
|
||||
val isWebtoonView = ReadingModeType.isWebtoonType(mangaViewer)
|
||||
updatePagedGroup(!isWebtoonView)
|
||||
landscapeZoom.isVisible = it == SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE - 1
|
||||
|
@ -33,7 +33,7 @@ class ReaderPagedView @JvmOverloads constructor(context: Context, attrs: Attribu
|
|||
pagerInvert.bindToPreference(preferences.pagerNavInverted())
|
||||
extendPastCutout.bindToPreference(preferences.pagerCutoutBehavior())
|
||||
pageLayout.bindToPreference(preferences.pageLayout()) {
|
||||
val mangaViewer = (context as? ReaderActivity)?.presenter?.getMangaReadingMode() ?: 0
|
||||
val mangaViewer = (context as? ReaderActivity)?.viewModel?.getMangaReadingMode() ?: 0
|
||||
val isWebtoonView = ReadingModeType.isWebtoonType(mangaViewer)
|
||||
updatePagedGroup(!isWebtoonView)
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ class ReaderPagedView @JvmOverloads constructor(context: Context, attrs: Attribu
|
|||
|
||||
pageLayout.title = pageLayout.title.toString().addBetaTag(context)
|
||||
|
||||
val mangaViewer = (context as? ReaderActivity)?.presenter?.getMangaReadingMode() ?: 0
|
||||
val mangaViewer = (context as? ReaderActivity)?.viewModel?.getMangaReadingMode() ?: 0
|
||||
val isWebtoonView = ReadingModeType.isWebtoonType(mangaViewer)
|
||||
val hasMargins = mangaViewer == ReadingModeType.CONTINUOUS_VERTICAL.flagValue
|
||||
cropBordersWebtoon.bindToPreference(if (hasMargins) preferences.cropBorders() else preferences.cropBordersWebtoon())
|
||||
|
@ -61,7 +61,7 @@ class ReaderPagedView @JvmOverloads constructor(context: Context, attrs: Attribu
|
|||
}
|
||||
|
||||
fun updatePrefs() {
|
||||
val mangaViewer = activity.presenter.getMangaReadingMode()
|
||||
val mangaViewer = activity.viewModel.getMangaReadingMode()
|
||||
val isWebtoonView = ReadingModeType.isWebtoonType(mangaViewer)
|
||||
val hasMargins = mangaViewer == ReadingModeType.CONTINUOUS_VERTICAL.flagValue
|
||||
binding.cropBordersWebtoon.bindToPreference(if (hasMargins) preferences.cropBorders() else preferences.cropBordersWebtoon())
|
||||
|
|
|
@ -38,7 +38,7 @@ class TabbedReaderSettingsSheet(
|
|||
) as ReaderFilterView
|
||||
|
||||
var showWebtoonView: Boolean = run {
|
||||
val mangaViewer = readerActivity.presenter.getMangaReadingMode()
|
||||
val mangaViewer = readerActivity.viewModel.getMangaReadingMode()
|
||||
ReadingModeType.isWebtoonType(mangaViewer)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
|
@ -27,6 +28,17 @@ open class Pager(
|
|||
*/
|
||||
var longTapListener: ((MotionEvent) -> Boolean)? = null
|
||||
|
||||
var isRestoring = false
|
||||
|
||||
override fun onRestoreInstanceState(state: Parcelable?) {
|
||||
isRestoring = true
|
||||
val currentItem = currentItem
|
||||
super.onRestoreInstanceState(state)
|
||||
setCurrentItem(currentItem, false)
|
||||
isRestoring = false
|
||||
// super.onRestoreInstanceState(state)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gesture listener that implements tap and long tap events.
|
||||
*/
|
||||
|
|
|
@ -38,11 +38,13 @@ import eu.kanade.tachiyomi.util.system.topCutoutInset
|
|||
import eu.kanade.tachiyomi.util.view.backgroundColor
|
||||
import eu.kanade.tachiyomi.util.view.isVisibleOnScreen
|
||||
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.Default
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
|
@ -51,7 +53,6 @@ import rx.schedulers.Schedulers
|
|||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
@ -87,37 +88,47 @@ class PagerPageHolder(
|
|||
private var decodeErrorLayout: ViewGroup? = null
|
||||
|
||||
/**
|
||||
* Subscription for status changes of the page.
|
||||
* Job for loading the page.
|
||||
*/
|
||||
private var statusSubscription: Subscription? = null
|
||||
private var loadJob: Job? = null
|
||||
|
||||
/**
|
||||
* Subscription for progress changes of the page.
|
||||
* Job for status changes of the page.
|
||||
*/
|
||||
private var progressSubscription: Subscription? = null
|
||||
private var statusJob: Job? = null
|
||||
|
||||
/**
|
||||
* Subscription for status changes of the page.
|
||||
* Job for progress changes of the page.
|
||||
*/
|
||||
private var extraStatusSubscription: Subscription? = null
|
||||
private var progressJob: Job? = null
|
||||
|
||||
/**
|
||||
* Subscription for progress changes of the page.
|
||||
* Job for loading the page.
|
||||
*/
|
||||
private var extraProgressSubscription: Subscription? = null
|
||||
private var extraLoadJob: Job? = null
|
||||
|
||||
/**
|
||||
* Job for status changes of the page.
|
||||
*/
|
||||
private var extraStatusJob: Job? = null
|
||||
|
||||
/**
|
||||
* Job for progress changes of the page.
|
||||
*/
|
||||
private var extraProgressJob: Job? = null
|
||||
|
||||
/**
|
||||
* Subscription used to read the header of the image. This is needed in order to instantiate
|
||||
* the appropriate image view depending if the image is animated (GIF).
|
||||
* the appropiate image view depending if the image is animated (GIF).
|
||||
*/
|
||||
private var readImageHeaderSubscription: Subscription? = null
|
||||
|
||||
private var status: Int = 0
|
||||
private var extraStatus: Int = 0
|
||||
private var status = Page.State.READY
|
||||
private var extraStatus = Page.State.READY
|
||||
private var progress: Int = 0
|
||||
private var extraProgress: Int = 0
|
||||
|
||||
private var scope: CoroutineScope? = null
|
||||
private var scope = MainScope()
|
||||
|
||||
init {
|
||||
addView(progressBar)
|
||||
|
@ -126,8 +137,7 @@ class PagerPageHolder(
|
|||
marginStart = ((context.resources.displayMetrics.widthPixels) / 2 + viewer.config.hingeGapSize) / 2
|
||||
}
|
||||
}
|
||||
scope = CoroutineScope(Job() + Default)
|
||||
observeStatus()
|
||||
launchLoadJob()
|
||||
setBackgroundColor(
|
||||
when (val theme = viewer.config.readerTheme) {
|
||||
3 -> Color.TRANSPARENT
|
||||
|
@ -185,16 +195,64 @@ class PagerPageHolder(
|
|||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
unsubscribeProgress(1)
|
||||
unsubscribeStatus(1)
|
||||
unsubscribeProgress(2)
|
||||
unsubscribeStatus(2)
|
||||
cancelProgressJob(1)
|
||||
cancelLoadJob(1)
|
||||
cancelProgressJob(2)
|
||||
cancelLoadJob(2)
|
||||
unsubscribeReadImageHeader()
|
||||
scope?.cancel()
|
||||
scope = null
|
||||
(pageView as? SubsamplingScaleImageView)?.setOnImageEventListener(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts loading the page and processing changes to the page's status.
|
||||
*
|
||||
* @see processStatus
|
||||
*/
|
||||
private fun launchLoadJob() {
|
||||
loadJob?.cancel()
|
||||
statusJob?.cancel()
|
||||
|
||||
val loader = page.chapter.pageLoader ?: return
|
||||
loadJob = scope.launch {
|
||||
loader.loadPage(page)
|
||||
}
|
||||
statusJob = scope.launch {
|
||||
page.statusFlow.collectLatest { processStatus(it) }
|
||||
}
|
||||
val extraPage = extraPage ?: return
|
||||
extraLoadJob = scope.launch {
|
||||
loader.loadPage(extraPage)
|
||||
}
|
||||
extraStatusJob = scope.launch {
|
||||
extraPage.statusFlow.collectLatest { processStatus2(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchProgressJob() {
|
||||
progressJob?.cancel()
|
||||
progressJob = scope.launch {
|
||||
page.progressFlow.collectLatest { value ->
|
||||
progress = value
|
||||
if (extraPage == null) {
|
||||
progressBar.setProgress(progress)
|
||||
} else {
|
||||
progressBar.setProgress(((progress + extraProgress) / 2 * 0.95f).roundToInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchProgressJob2() {
|
||||
val extraPage = extraPage ?: return
|
||||
extraProgressJob?.cancel()
|
||||
extraProgressJob = scope.launch {
|
||||
extraPage.progressFlow.collectLatest { value ->
|
||||
extraProgress = value
|
||||
progressBar.setProgress(((progress + extraProgress) / 2 * 0.95f).roundToInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onPageSelected(forward: Boolean?) {
|
||||
(pageView as? SubsamplingScaleImageView)?.apply {
|
||||
if (isReady) {
|
||||
|
@ -304,88 +362,28 @@ class PagerPageHolder(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes the status of the page and notify the changes.
|
||||
*
|
||||
* @see processStatus
|
||||
*/
|
||||
private fun observeStatus() {
|
||||
statusSubscription?.unsubscribe()
|
||||
|
||||
val loader = page.chapter.pageLoader ?: return
|
||||
statusSubscription = loader.getPage(page)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
status = it
|
||||
processStatus(it)
|
||||
}
|
||||
val extraPage = extraPage ?: return
|
||||
val loader2 = extraPage.chapter.pageLoader ?: return
|
||||
extraStatusSubscription = loader2.getPage(extraPage)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
extraStatus = it
|
||||
processStatus2(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes the progress of the page and updates view.
|
||||
*/
|
||||
private fun observeProgress() {
|
||||
progressSubscription?.unsubscribe()
|
||||
|
||||
progressSubscription = Observable.interval(100, TimeUnit.MILLISECONDS)
|
||||
.map { page.progress }
|
||||
.distinctUntilChanged()
|
||||
.onBackpressureLatest()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { value ->
|
||||
progress = value
|
||||
if (extraPage == null) {
|
||||
progressBar.setProgress(progress)
|
||||
} else {
|
||||
progressBar.setProgress(((progress + extraProgress) / 2 * 0.95f).roundToInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeProgress2() {
|
||||
extraProgressSubscription?.unsubscribe()
|
||||
val extraPage = extraPage ?: return
|
||||
extraProgressSubscription = Observable.interval(100, TimeUnit.MILLISECONDS)
|
||||
.map { extraPage.progress }
|
||||
.distinctUntilChanged()
|
||||
.onBackpressureLatest()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { value ->
|
||||
extraProgress = value
|
||||
progressBar.setProgress(((progress + extraProgress) / 2 * 0.95f).roundToInt())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the status of the page changes.
|
||||
*
|
||||
* @param status the new status of the page.
|
||||
*/
|
||||
private fun processStatus(status: Int) {
|
||||
private fun processStatus(status: Page.State) {
|
||||
when (status) {
|
||||
Page.QUEUE -> setQueued()
|
||||
Page.LOAD_PAGE -> setLoading()
|
||||
Page.DOWNLOAD_IMAGE -> {
|
||||
observeProgress()
|
||||
Page.State.QUEUE -> setQueued()
|
||||
Page.State.LOAD_PAGE -> setLoading()
|
||||
Page.State.DOWNLOAD_IMAGE -> {
|
||||
launchProgressJob()
|
||||
setDownloading()
|
||||
}
|
||||
Page.READY -> {
|
||||
if (extraStatus == Page.READY || extraPage == null) {
|
||||
Page.State.READY -> {
|
||||
if (extraStatus == Page.State.READY || extraPage == null) {
|
||||
setImage()
|
||||
}
|
||||
unsubscribeProgress(1)
|
||||
cancelProgressJob(1)
|
||||
}
|
||||
Page.ERROR -> {
|
||||
Page.State.ERROR -> {
|
||||
setError()
|
||||
unsubscribeProgress(1)
|
||||
cancelProgressJob(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -395,43 +393,51 @@ class PagerPageHolder(
|
|||
*
|
||||
* @param status the new status of the page.
|
||||
*/
|
||||
private fun processStatus2(status: Int) {
|
||||
private fun processStatus2(status: Page.State) {
|
||||
when (status) {
|
||||
Page.QUEUE -> setQueued()
|
||||
Page.LOAD_PAGE -> setLoading()
|
||||
Page.DOWNLOAD_IMAGE -> {
|
||||
observeProgress2()
|
||||
Page.State.QUEUE -> setQueued()
|
||||
Page.State.LOAD_PAGE -> setLoading()
|
||||
Page.State.DOWNLOAD_IMAGE -> {
|
||||
launchProgressJob2()
|
||||
setDownloading()
|
||||
}
|
||||
Page.READY -> {
|
||||
if (this.status == Page.READY) {
|
||||
Page.State.READY -> {
|
||||
if (this.status == Page.State.READY) {
|
||||
setImage()
|
||||
}
|
||||
unsubscribeProgress(2)
|
||||
cancelProgressJob(2)
|
||||
}
|
||||
Page.ERROR -> {
|
||||
Page.State.ERROR -> {
|
||||
setError()
|
||||
unsubscribeProgress(2)
|
||||
cancelProgressJob(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes from the status subscription.
|
||||
* Cancels loading the page and processing changes to the page's status.
|
||||
*/
|
||||
private fun unsubscribeStatus(page: Int) {
|
||||
val subscription = if (page == 1) statusSubscription else extraStatusSubscription
|
||||
subscription?.unsubscribe()
|
||||
if (page == 1) statusSubscription = null else extraStatusSubscription = null
|
||||
private fun cancelLoadJob(page: Int) {
|
||||
if (page == 1) {
|
||||
loadJob?.cancel()
|
||||
loadJob = null
|
||||
statusJob?.cancel()
|
||||
statusJob = null
|
||||
} else {
|
||||
extraLoadJob?.cancel()
|
||||
extraLoadJob = null
|
||||
extraStatusJob?.cancel()
|
||||
extraStatusJob = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes from the progress subscription.
|
||||
*/
|
||||
private fun unsubscribeProgress(page: Int) {
|
||||
val subscription = if (page == 1) progressSubscription else extraProgressSubscription
|
||||
subscription?.unsubscribe()
|
||||
if (page == 1) progressSubscription = null else extraProgressSubscription = null
|
||||
private fun cancelProgressJob(page: Int) {
|
||||
(if (page == 1) progressJob else extraProgressJob)?.cancel()
|
||||
if (page == 1) {
|
||||
progressJob = null
|
||||
} else {
|
||||
extraProgressJob = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -515,7 +521,7 @@ class PagerPageHolder(
|
|||
setImage(bytesStream, false, imageConfig)
|
||||
bytesStream.close()
|
||||
|
||||
scope?.launchUI {
|
||||
scope.launchUI {
|
||||
try {
|
||||
pageView?.background = setBG(bytesArray)
|
||||
} catch (e: Exception) {
|
||||
|
|
|
@ -59,7 +59,7 @@ class PagerTransitionHolder(
|
|||
addView(transitionView)
|
||||
addView(pagesContainer)
|
||||
|
||||
transitionView.bind(transition, viewer.downloadManager, viewer.activity.presenter.manga)
|
||||
transitionView.bind(transition, viewer.downloadManager, viewer.activity.viewModel.state.value.manga)
|
||||
transition.to?.let { observeStatus(it) }
|
||||
|
||||
if (viewer.config.hingeGapSize > 0) {
|
||||
|
|
|
@ -89,6 +89,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
|||
|
||||
private var pagerListener = object : ViewPager.SimpleOnPageChangeListener() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
if (pager.isRestoring) return
|
||||
val page = adapter.joinedItems.getOrNull(position)
|
||||
if (!activity.isScrollingThroughPagesOrChapters && page?.first !is ChapterTransition) {
|
||||
activity.hideMenu()
|
||||
|
|
|
@ -23,13 +23,16 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
|||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressBar
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Holder of the webtoon reader for a single page of a chapter.
|
||||
|
@ -75,19 +78,26 @@ class WebtoonPageHolder(
|
|||
*/
|
||||
private var page: ReaderPage? = null
|
||||
|
||||
/**
|
||||
* Subscription for status changes of the page.
|
||||
*/
|
||||
private var statusSubscription: Subscription? = null
|
||||
private val scope = MainScope()
|
||||
|
||||
/**
|
||||
* Subscription for progress changes of the page.
|
||||
* Job for loading the page.
|
||||
*/
|
||||
private var progressSubscription: Subscription? = null
|
||||
private var loadJob: Job? = null
|
||||
|
||||
/**
|
||||
* Job for status changes of the page.
|
||||
*/
|
||||
private var statusJob: Job? = null
|
||||
|
||||
/**
|
||||
* Job for progress changes of the page.
|
||||
*/
|
||||
private var progressJob: Job? = null
|
||||
|
||||
/**
|
||||
* Subscription used to read the header of the image. This is needed in order to instantiate
|
||||
* the appropiate image view depending if the image is animated (GIF).
|
||||
* the appropriate image view depending if the image is animated (GIF).
|
||||
*/
|
||||
private var readImageHeaderSubscription: Subscription? = null
|
||||
|
||||
|
@ -105,7 +115,7 @@ class WebtoonPageHolder(
|
|||
*/
|
||||
fun bind(page: ReaderPage) {
|
||||
this.page = page
|
||||
observeStatus()
|
||||
launchLoadJob()
|
||||
refreshLayoutParams()
|
||||
}
|
||||
|
||||
|
@ -124,8 +134,8 @@ class WebtoonPageHolder(
|
|||
* Called when the view is recycled and added to the view pool.
|
||||
*/
|
||||
override fun recycle() {
|
||||
unsubscribeStatus()
|
||||
unsubscribeProgress()
|
||||
cancelLoadJob()
|
||||
cancelProgressJob()
|
||||
unsubscribeReadImageHeader()
|
||||
|
||||
removeDecodeErrorLayout()
|
||||
|
@ -138,34 +148,30 @@ class WebtoonPageHolder(
|
|||
*
|
||||
* @see processStatus
|
||||
*/
|
||||
private fun observeStatus() {
|
||||
unsubscribeStatus()
|
||||
private fun launchLoadJob() {
|
||||
cancelLoadJob()
|
||||
|
||||
val page = page ?: return
|
||||
val loader = page.chapter.pageLoader ?: return
|
||||
statusSubscription = loader.getPage(page)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { processStatus(it) }
|
||||
|
||||
addSubscription(statusSubscription)
|
||||
loadJob = scope.launch {
|
||||
loader.loadPage(page)
|
||||
}
|
||||
statusJob = scope.launch {
|
||||
page.statusFlow.collectLatest { processStatus(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes the progress of the page and updates view.
|
||||
*/
|
||||
private fun observeProgress() {
|
||||
unsubscribeProgress()
|
||||
private fun launchProgressJob() {
|
||||
cancelProgressJob()
|
||||
|
||||
val page = page ?: return
|
||||
|
||||
progressSubscription = Observable.interval(100, TimeUnit.MILLISECONDS)
|
||||
.map { page.progress }
|
||||
.distinctUntilChanged()
|
||||
.onBackpressureLatest()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { value -> progressBar.setProgress(value) }
|
||||
|
||||
addSubscription(progressSubscription)
|
||||
progressJob = scope.launch {
|
||||
page.progressFlow.collectLatest { value -> progressBar.setProgress(value) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -173,39 +179,41 @@ class WebtoonPageHolder(
|
|||
*
|
||||
* @param status the new status of the page.
|
||||
*/
|
||||
private fun processStatus(status: Int) {
|
||||
private fun processStatus(status: Page.State) {
|
||||
when (status) {
|
||||
Page.QUEUE -> setQueued()
|
||||
Page.LOAD_PAGE -> setLoading()
|
||||
Page.DOWNLOAD_IMAGE -> {
|
||||
observeProgress()
|
||||
Page.State.QUEUE -> setQueued()
|
||||
Page.State.LOAD_PAGE -> setLoading()
|
||||
Page.State.DOWNLOAD_IMAGE -> {
|
||||
launchProgressJob()
|
||||
setDownloading()
|
||||
}
|
||||
Page.READY -> {
|
||||
Page.State.READY -> {
|
||||
setImage()
|
||||
unsubscribeProgress()
|
||||
cancelProgressJob()
|
||||
}
|
||||
Page.ERROR -> {
|
||||
Page.State.ERROR -> {
|
||||
setError()
|
||||
unsubscribeProgress()
|
||||
cancelProgressJob()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes from the status subscription.
|
||||
* Cancels loading the page and processing changes to the page's status.
|
||||
*/
|
||||
private fun unsubscribeStatus() {
|
||||
removeSubscription(statusSubscription)
|
||||
statusSubscription = null
|
||||
private fun cancelLoadJob() {
|
||||
loadJob?.cancel()
|
||||
loadJob = null
|
||||
statusJob?.cancel()
|
||||
statusJob = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes from the progress subscription.
|
||||
*/
|
||||
private fun unsubscribeProgress() {
|
||||
removeSubscription(progressSubscription)
|
||||
progressSubscription = null
|
||||
private fun cancelProgressJob() {
|
||||
progressJob?.cancel()
|
||||
progressJob = null
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -64,7 +64,7 @@ class WebtoonTransitionHolder(
|
|||
* Binds the given [transition] with this view holder, subscribing to its state.
|
||||
*/
|
||||
fun bind(transition: ChapterTransition) {
|
||||
transitionView.bind(transition, viewer.downloadManager, viewer.activity.presenter.manga)
|
||||
transitionView.bind(transition, viewer.downloadManager, viewer.activity.viewModel.state.value.manga)
|
||||
transition.to?.let { observeStatus(it, transition) }
|
||||
}
|
||||
|
||||
|
|
|
@ -139,7 +139,7 @@ class EpubFile(file: File) : Closeable {
|
|||
*/
|
||||
private fun getPagesFromDocument(document: Document): List<String> {
|
||||
val pages = document.select("manifest > item")
|
||||
.filter { "application/xhtml+xml" == it.attr("media-type") }
|
||||
.filter { node -> "application/xhtml+xml" == node.attr("media-type") }
|
||||
.associateBy { it.attr("id") }
|
||||
|
||||
val spine = document.select("spine > itemref").map { it.attr("idref") }
|
||||
|
|
|
@ -5,6 +5,7 @@ import kotlinx.coroutines.CoroutineStart
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
@ -23,6 +24,9 @@ fun CoroutineScope.launchIO(block: suspend CoroutineScope.() -> Unit): Job =
|
|||
fun CoroutineScope.launchUI(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
launch(Dispatchers.Main, block = block)
|
||||
|
||||
fun CoroutineScope.launchNonCancellable(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
launchIO { withContext(NonCancellable, block) }
|
||||
|
||||
suspend fun <T> withUIContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.Main, block)
|
||||
|
||||
suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.IO, block)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue