Preload pages
This commit is contained in:
@@ -10,25 +10,61 @@ import kotlinx.coroutines.sync.Mutex
|
|||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
import okio.Closeable
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.get
|
||||||
import org.koitharu.kotatsu.core.model.MangaPage
|
import org.koitharu.kotatsu.core.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
|
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||||
import org.koitharu.kotatsu.local.data.PagesCache
|
import org.koitharu.kotatsu.local.data.PagesCache
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
import org.koitharu.kotatsu.utils.ext.await
|
import org.koitharu.kotatsu.utils.ext.await
|
||||||
import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf
|
import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
class PageLoader(
|
class PageLoader : KoinComponent, Closeable {
|
||||||
scope: CoroutineScope,
|
|
||||||
private val okHttp: OkHttpClient,
|
|
||||||
private val cache: PagesCache
|
|
||||||
) : CoroutineScope by scope, KoinComponent {
|
|
||||||
|
|
||||||
private var repository: MangaRepository? = null
|
val loaderScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
|
private val okHttp = get<OkHttpClient>()
|
||||||
|
private val cache = get<PagesCache>()
|
||||||
private val tasks = LongSparseArray<Deferred<File>>()
|
private val tasks = LongSparseArray<Deferred<File>>()
|
||||||
private val convertLock = Mutex()
|
private val convertLock = Mutex()
|
||||||
|
private var repository: MangaRepository? = null
|
||||||
|
private var prefetchQueue = LinkedList<MangaPage>()
|
||||||
|
private val counter = AtomicInteger(0)
|
||||||
|
private var prefetchQueueLimit = 10 // TODO adaptive
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
loaderScope.cancel()
|
||||||
|
tasks.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isPrefetchApplicable(): Boolean {
|
||||||
|
return repository is RemoteMangaRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
fun prefetch(pages: List<ReaderPage>) {
|
||||||
|
synchronized(prefetchQueue) {
|
||||||
|
for (page in pages.asReversed()) {
|
||||||
|
if (tasks.containsKey(page.id)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
prefetchQueue.offerFirst(page.toMangaPage())
|
||||||
|
if (prefetchQueue.size > prefetchQueueLimit) {
|
||||||
|
prefetchQueue.pollLast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (counter.get() == 0) {
|
||||||
|
onIdle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun loadPage(page: MangaPage, force: Boolean): File {
|
suspend fun loadPage(page: MangaPage, force: Boolean): File {
|
||||||
if (!force) {
|
if (!force) {
|
||||||
@@ -42,53 +78,14 @@ class PageLoader(
|
|||||||
} else if (task?.isCancelled == false) {
|
} else if (task?.isCancelled == false) {
|
||||||
return task.await()
|
return task.await()
|
||||||
}
|
}
|
||||||
task = loadAsync(page)
|
task = loadPageAsync(page)
|
||||||
tasks[page.id] = task
|
tasks[page.id] = task
|
||||||
return task.await()
|
return task.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadAsync(page: MangaPage): Deferred<File> {
|
|
||||||
var repo = repository
|
|
||||||
if (repo?.source != page.source) {
|
|
||||||
repo = mangaRepositoryOf(page.source)
|
|
||||||
repository = repo
|
|
||||||
}
|
|
||||||
return async(Dispatchers.IO) {
|
|
||||||
val pageUrl = repo.getPageUrl(page)
|
|
||||||
check(pageUrl.isNotBlank()) { "Cannot obtain full image url" }
|
|
||||||
val uri = Uri.parse(pageUrl)
|
|
||||||
if (uri.scheme == "cbz") {
|
|
||||||
val zip = ZipFile(uri.schemeSpecificPart)
|
|
||||||
val entry = zip.getEntry(uri.fragment)
|
|
||||||
zip.getInputStream(entry).use {
|
|
||||||
cache.put(pageUrl, it)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(pageUrl)
|
|
||||||
.get()
|
|
||||||
.header(CommonHeaders.REFERER, page.referer)
|
|
||||||
.header(CommonHeaders.ACCEPT, "image/webp,image/png;q=0.9,image/jpeg,*/*;q=0.8")
|
|
||||||
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
|
|
||||||
.build()
|
|
||||||
okHttp.newCall(request).await().use { response ->
|
|
||||||
check(response.isSuccessful) {
|
|
||||||
"Invalid response: ${response.code} ${response.message}"
|
|
||||||
}
|
|
||||||
val body = checkNotNull(response.body) {
|
|
||||||
"Null response"
|
|
||||||
}
|
|
||||||
body.byteStream().use {
|
|
||||||
cache.put(pageUrl, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun convertInPlace(file: File) {
|
suspend fun convertInPlace(file: File) {
|
||||||
convertLock.withLock(Lock) {
|
convertLock.withLock {
|
||||||
withContext(Dispatchers.Default) {
|
runInterruptible(Dispatchers.Default) {
|
||||||
val image = BitmapFactory.decodeFile(file.absolutePath)
|
val image = BitmapFactory.decodeFile(file.absolutePath)
|
||||||
try {
|
try {
|
||||||
file.outputStream().use { out ->
|
file.outputStream().use { out ->
|
||||||
@@ -101,5 +98,69 @@ class PageLoader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object Lock
|
private fun onIdle() {
|
||||||
}
|
synchronized(prefetchQueue) {
|
||||||
|
val page = prefetchQueue.pollFirst() ?: return
|
||||||
|
tasks[page.id] = loadPageAsync(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadPageAsync(page: MangaPage): Deferred<File> {
|
||||||
|
return loaderScope.async {
|
||||||
|
counter.incrementAndGet()
|
||||||
|
try {
|
||||||
|
loadPageImpl(page)
|
||||||
|
} finally {
|
||||||
|
if (counter.decrementAndGet() == 0) {
|
||||||
|
onIdle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun getRepository(source: MangaSource): MangaRepository {
|
||||||
|
val result = repository
|
||||||
|
return if (result != null && result.source == source) {
|
||||||
|
result
|
||||||
|
} else {
|
||||||
|
mangaRepositoryOf(source).also { repository = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadPageImpl(page: MangaPage): File {
|
||||||
|
val pageUrl = getRepository(page.source).getPageUrl(page)
|
||||||
|
check(pageUrl.isNotBlank()) { "Cannot obtain full image url" }
|
||||||
|
val uri = Uri.parse(pageUrl)
|
||||||
|
return if (uri.scheme == "cbz") {
|
||||||
|
runInterruptible(Dispatchers.IO) {
|
||||||
|
val zip = ZipFile(uri.schemeSpecificPart)
|
||||||
|
val entry = zip.getEntry(uri.fragment)
|
||||||
|
zip.getInputStream(entry).use {
|
||||||
|
cache.put(pageUrl, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(pageUrl)
|
||||||
|
.get()
|
||||||
|
.header(CommonHeaders.REFERER, page.referer)
|
||||||
|
.header(CommonHeaders.ACCEPT, "image/webp,image/png;q=0.9,image/jpeg,*/*;q=0.8")
|
||||||
|
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
|
||||||
|
.build()
|
||||||
|
okHttp.newCall(request).await().use { response ->
|
||||||
|
check(response.isSuccessful) {
|
||||||
|
"Invalid response: ${response.code} ${response.message}"
|
||||||
|
}
|
||||||
|
val body = checkNotNull(response.body) {
|
||||||
|
"Null response"
|
||||||
|
}
|
||||||
|
runInterruptible(Dispatchers.IO) {
|
||||||
|
body.byteStream().use {
|
||||||
|
cache.put(pageUrl, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
|||||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||||
import org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy
|
import org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||||
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
|
||||||
import org.koitharu.kotatsu.utils.DownloadManagerHelper
|
import org.koitharu.kotatsu.utils.DownloadManagerHelper
|
||||||
@@ -45,6 +46,8 @@ class ReaderViewModel(
|
|||||||
private val mangaData = MutableStateFlow(intent.manga)
|
private val mangaData = MutableStateFlow(intent.manga)
|
||||||
private val chapters = LongSparseArray<MangaChapter>()
|
private val chapters = LongSparseArray<MangaChapter>()
|
||||||
|
|
||||||
|
val pageLoader = PageLoader()
|
||||||
|
|
||||||
val readerMode = MutableLiveData<ReaderMode>()
|
val readerMode = MutableLiveData<ReaderMode>()
|
||||||
val onPageSaved = SingleLiveEvent<Uri?>()
|
val onPageSaved = SingleLiveEvent<Uri?>()
|
||||||
val uiState = combine(
|
val uiState = combine(
|
||||||
@@ -126,6 +129,11 @@ class ReaderViewModel(
|
|||||||
subscribeToSettings()
|
subscribeToSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
pageLoader.close()
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
|
||||||
fun switchMode(newMode: ReaderMode) {
|
fun switchMode(newMode: ReaderMode) {
|
||||||
launchJob {
|
launchJob {
|
||||||
val manga = checkNotNull(mangaData.value)
|
val manga = checkNotNull(mangaData.value)
|
||||||
@@ -206,6 +214,9 @@ class ReaderViewModel(
|
|||||||
if (position >= pages.size - BOUNDS_PAGE_OFFSET) {
|
if (position >= pages.size - BOUNDS_PAGE_OFFSET) {
|
||||||
loadPrevNextChapter(pages.last().chapterId, 1)
|
loadPrevNextChapter(pages.last().chapterId, 1)
|
||||||
}
|
}
|
||||||
|
if (pageLoader.isPrefetchApplicable()) {
|
||||||
|
pageLoader.prefetch(pages.trySublist(position + 1, position + PREFETCH_LIMIT))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getReaderMode(isWebtoon: Boolean?) = when {
|
private fun getReaderMode(isWebtoon: Boolean?) = when {
|
||||||
@@ -262,10 +273,21 @@ class ReaderViewModel(
|
|||||||
.launchIn(viewModelScope + Dispatchers.IO)
|
.launchIn(viewModelScope + Dispatchers.IO)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun <T> List<T>.trySublist(fromIndex: Int, toIndex: Int): List<T> {
|
||||||
|
val fromIndexBounded = fromIndex.coerceAtMost(lastIndex)
|
||||||
|
val toIndexBounded = toIndex.coerceIn(fromIndexBounded, lastIndex)
|
||||||
|
return if (fromIndexBounded == toIndexBounded) {
|
||||||
|
emptyList()
|
||||||
|
} else {
|
||||||
|
subList(fromIndexBounded, toIndexBounded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private companion object : KoinComponent {
|
private companion object : KoinComponent {
|
||||||
|
|
||||||
const val BOUNDS_PAGE_OFFSET = 2
|
const val BOUNDS_PAGE_OFFSET = 2
|
||||||
const val PAGES_TRIM_THRESHOLD = 120
|
const val PAGES_TRIM_THRESHOLD = 120
|
||||||
|
const val PREFETCH_LIMIT = 10
|
||||||
|
|
||||||
fun saveState(manga: Manga, state: ReaderState) {
|
fun saveState(manga: Manga, state: ReaderState) {
|
||||||
processLifecycleScope.launch(Dispatchers.Default + IgnoreErrors) {
|
processLifecycleScope.launch(Dispatchers.Default + IgnoreErrors) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.pager
|
package org.koitharu.kotatsu.reader.ui.pager
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.annotation.CallSuper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||||
@@ -33,5 +34,8 @@ abstract class BasePageHolder<B : ViewBinding>(
|
|||||||
|
|
||||||
protected abstract fun onBind(data: ReaderPage)
|
protected abstract fun onBind(data: ReaderPage)
|
||||||
|
|
||||||
open fun onRecycled() = Unit
|
@CallSuper
|
||||||
|
open fun onRecycled() {
|
||||||
|
delegate.onRecycle()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,21 +3,15 @@ package org.koitharu.kotatsu.reader.ui.pager
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import org.koin.android.ext.android.get
|
|
||||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
|
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
|
||||||
|
|
||||||
abstract class BaseReader<B : ViewBinding> : BaseFragment<B>() {
|
abstract class BaseReader<B : ViewBinding> : BaseFragment<B>() {
|
||||||
|
|
||||||
protected val viewModel by sharedViewModel<ReaderViewModel>()
|
protected val viewModel by sharedViewModel<ReaderViewModel>()
|
||||||
protected val loader by lazy(LazyThreadSafetyMode.NONE) {
|
|
||||||
PageLoader(lifecycleScope, get(), get())
|
|
||||||
}
|
|
||||||
private var stateToSave: ReaderState? = null
|
private var stateToSave: ReaderState? = null
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ package org.koitharu.kotatsu.reader.ui.pager
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
|
import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
|
||||||
import org.koitharu.kotatsu.core.model.MangaPage
|
import org.koitharu.kotatsu.core.model.MangaPage
|
||||||
@@ -20,21 +23,22 @@ class PageHolderDelegate(
|
|||||||
private val settings: AppSettings,
|
private val settings: AppSettings,
|
||||||
private val callback: Callback,
|
private val callback: Callback,
|
||||||
private val exceptionResolver: ExceptionResolver
|
private val exceptionResolver: ExceptionResolver
|
||||||
) : SubsamplingScaleImageView.DefaultOnImageEventListener(), CoroutineScope by loader {
|
) : SubsamplingScaleImageView.DefaultOnImageEventListener() {
|
||||||
|
|
||||||
|
private val scope = loader.loaderScope + Dispatchers.Main.immediate
|
||||||
private var state = State.EMPTY
|
private var state = State.EMPTY
|
||||||
private var job: Job? = null
|
private var job: Job? = null
|
||||||
private var file: File? = null
|
private var file: File? = null
|
||||||
private var error: Throwable? = null
|
private var error: Throwable? = null
|
||||||
|
|
||||||
fun onBind(page: MangaPage) {
|
fun onBind(page: MangaPage) {
|
||||||
job = launchInstead(job) {
|
job = scope.launchInstead(job) {
|
||||||
doLoad(page, force = false)
|
doLoad(page, force = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retry(page: MangaPage) {
|
fun retry(page: MangaPage) {
|
||||||
job = launchInstead(job) {
|
job = scope.launchInstead(job) {
|
||||||
(error as? ResolvableException)?.let {
|
(error as? ResolvableException)?.let {
|
||||||
exceptionResolver.resolve(it)
|
exceptionResolver.resolve(it)
|
||||||
}
|
}
|
||||||
@@ -65,7 +69,7 @@ class PageHolderDelegate(
|
|||||||
val file = this.file
|
val file = this.file
|
||||||
error = e
|
error = e
|
||||||
if (state == State.LOADED && e is IOException && file != null && file.exists()) {
|
if (state == State.LOADED && e is IOException && file != null && file.exists()) {
|
||||||
job = launchAfter(job) {
|
job = scope.launchAfter(job) {
|
||||||
state = State.CONVERTING
|
state = State.CONVERTING
|
||||||
try {
|
try {
|
||||||
loader.convertInPlace(file)
|
loader.convertInPlace(file)
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReader
|
|||||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
|
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
import org.koitharu.kotatsu.utils.ext.doOnPageChanged
|
||||||
|
import org.koitharu.kotatsu.utils.ext.recyclerView
|
||||||
|
import org.koitharu.kotatsu.utils.ext.resetTransformations
|
||||||
|
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
||||||
@@ -27,7 +30,7 @@ class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
pagerAdapter = ReversedPagesAdapter(loader, get(), exceptionResolver)
|
pagerAdapter = ReversedPagesAdapter(viewModel.pageLoader, get(), exceptionResolver)
|
||||||
with(binding.pager) {
|
with(binding.pager) {
|
||||||
adapter = pagerAdapter
|
adapter = pagerAdapter
|
||||||
offscreenPageLimit = 2
|
offscreenPageLimit = 2
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ open class PageHolder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onRecycled() {
|
override fun onRecycled() {
|
||||||
delegate.onRecycle()
|
super.onRecycled()
|
||||||
binding.ssiv.recycle()
|
binding.ssiv.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
pagesAdapter = PagesAdapter(loader, get(), exceptionResolver)
|
pagesAdapter = PagesAdapter(viewModel.pageLoader, get(), exceptionResolver)
|
||||||
with(binding.pager) {
|
with(binding.pager) {
|
||||||
adapter = pagesAdapter
|
adapter = pagesAdapter
|
||||||
offscreenPageLimit = 2
|
offscreenPageLimit = 2
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class WebtoonHolder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onRecycled() {
|
override fun onRecycled() {
|
||||||
delegate.onRecycle()
|
super.onRecycled()
|
||||||
binding.ssiv.recycle()
|
binding.ssiv.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import org.koitharu.kotatsu.reader.ui.ReaderState
|
|||||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
|
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
import org.koitharu.kotatsu.utils.ext.doOnCurrentItemChanged
|
||||||
|
import org.koitharu.kotatsu.utils.ext.findCenterViewPosition
|
||||||
|
import org.koitharu.kotatsu.utils.ext.firstItem
|
||||||
|
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||||
|
|
||||||
class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
|
class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
|
||||||
|
|
||||||
@@ -26,7 +29,7 @@ class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
webtoonAdapter = WebtoonAdapter(loader, get(), exceptionResolver)
|
webtoonAdapter = WebtoonAdapter(viewModel.pageLoader, get(), exceptionResolver)
|
||||||
with(binding.recyclerView) {
|
with(binding.recyclerView) {
|
||||||
setHasFixedSize(true)
|
setHasFixedSize(true)
|
||||||
adapter = webtoonAdapter
|
adapter = webtoonAdapter
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import android.content.Context
|
|||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.Network
|
import android.net.Network
|
||||||
import android.net.NetworkRequest
|
import android.net.NetworkRequest
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
@@ -26,4 +28,10 @@ suspend fun ConnectivityManager.waitForNetwork(): Network {
|
|||||||
|
|
||||||
inline fun buildAlertDialog(context: Context, block: MaterialAlertDialogBuilder.() -> Unit): AlertDialog {
|
inline fun buildAlertDialog(context: Context, block: MaterialAlertDialogBuilder.() -> Unit): AlertDialog {
|
||||||
return MaterialAlertDialogBuilder(context).apply(block).create()
|
return MaterialAlertDialogBuilder(context).apply(block).create()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T : Parcelable> Bundle.requireParcelable(key: String): T {
|
||||||
|
return checkNotNull(getParcelable(key)) {
|
||||||
|
"Value for key $key not found"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user