Use page preview in reader while loading
This commit is contained in:
@@ -5,6 +5,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
class RetainedLifecycleCoroutineScope(
|
class RetainedLifecycleCoroutineScope(
|
||||||
@@ -14,7 +15,9 @@ class RetainedLifecycleCoroutineScope(
|
|||||||
override val coroutineContext: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
|
override val coroutineContext: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
|
||||||
|
|
||||||
init {
|
init {
|
||||||
lifecycle.addOnClearedListener(this)
|
launch(Dispatchers.Main.immediate) {
|
||||||
|
lifecycle.addOnClearedListener(this@RetainedLifecycleCoroutineScope)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ package org.koitharu.kotatsu.details.ui.pager.pages
|
|||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
import coil3.request.allowRgb565
|
import coil3.request.allowRgb565
|
||||||
|
import coil3.request.transformations
|
||||||
import coil3.size.Scale
|
import coil3.size.Scale
|
||||||
import coil3.size.Size
|
import coil3.size.Size
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
|
||||||
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.core.util.ext.decodeRegion
|
import org.koitharu.kotatsu.core.util.ext.decodeRegion
|
||||||
@@ -43,6 +45,7 @@ fun pageThumbnailAD(
|
|||||||
size(thumbSize)
|
size(thumbSize)
|
||||||
scale(Scale.FILL)
|
scale(Scale.FILL)
|
||||||
allowRgb565(true)
|
allowRgb565(true)
|
||||||
|
transformations(TrimTransformation())
|
||||||
decodeRegion(0)
|
decodeRegion(0)
|
||||||
mangaSourceExtra(item.page.source)
|
mangaSourceExtra(item.page.source)
|
||||||
enqueueWith(coil)
|
enqueueWith(coil)
|
||||||
|
|||||||
@@ -8,9 +8,15 @@ import androidx.collection.LongSparseArray
|
|||||||
import androidx.collection.set
|
import androidx.collection.set
|
||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import coil3.BitmapImage
|
||||||
|
import coil3.Image
|
||||||
|
import coil3.ImageLoader
|
||||||
|
import coil3.memory.MemoryCache
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import coil3.request.transformations
|
||||||
|
import coil3.toBitmap
|
||||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||||
import dagger.hilt.android.ActivityRetainedLifecycle
|
import dagger.hilt.android.ActivityRetainedLifecycle
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import dagger.hilt.android.scopes.ActivityRetainedScoped
|
import dagger.hilt.android.scopes.ActivityRetainedScoped
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
@@ -24,6 +30,7 @@ import kotlinx.coroutines.sync.Mutex
|
|||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okio.use
|
import okio.use
|
||||||
@@ -36,9 +43,9 @@ import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
|
|||||||
import org.koitharu.kotatsu.core.parser.CachingMangaRepository
|
import org.koitharu.kotatsu.core.parser.CachingMangaRepository
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
|
||||||
import org.koitharu.kotatsu.core.util.FileSize
|
import org.koitharu.kotatsu.core.util.FileSize
|
||||||
import org.koitharu.kotatsu.core.util.MimeTypes
|
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||||
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP
|
import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP
|
||||||
import org.koitharu.kotatsu.core.util.ext.cancelChildrenAndJoin
|
import org.koitharu.kotatsu.core.util.ext.cancelChildrenAndJoin
|
||||||
import org.koitharu.kotatsu.core.util.ext.compressToPNG
|
import org.koitharu.kotatsu.core.util.ext.compressToPNG
|
||||||
@@ -49,6 +56,8 @@ import org.koitharu.kotatsu.core.util.ext.isFileUri
|
|||||||
import org.koitharu.kotatsu.core.util.ext.isNotEmpty
|
import org.koitharu.kotatsu.core.util.ext.isNotEmpty
|
||||||
import org.koitharu.kotatsu.core.util.ext.isPowerSaveMode
|
import org.koitharu.kotatsu.core.util.ext.isPowerSaveMode
|
||||||
import org.koitharu.kotatsu.core.util.ext.isZipUri
|
import org.koitharu.kotatsu.core.util.ext.isZipUri
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
|
||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.core.util.ext.ramAvailable
|
import org.koitharu.kotatsu.core.util.ext.ramAvailable
|
||||||
import org.koitharu.kotatsu.core.util.ext.toMimeType
|
import org.koitharu.kotatsu.core.util.ext.toMimeType
|
||||||
@@ -76,13 +85,14 @@ class PageLoader @Inject constructor(
|
|||||||
lifecycle: ActivityRetainedLifecycle,
|
lifecycle: ActivityRetainedLifecycle,
|
||||||
@MangaHttpClient private val okHttp: OkHttpClient,
|
@MangaHttpClient private val okHttp: OkHttpClient,
|
||||||
private val cache: PagesCache,
|
private val cache: PagesCache,
|
||||||
|
private val coil: ImageLoader,
|
||||||
private val settings: AppSettings,
|
private val settings: AppSettings,
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
private val imageProxyInterceptor: ImageProxyInterceptor,
|
private val imageProxyInterceptor: ImageProxyInterceptor,
|
||||||
private val downloadSlowdownDispatcher: DownloadSlowdownDispatcher,
|
private val downloadSlowdownDispatcher: DownloadSlowdownDispatcher,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val loaderScope = RetainedLifecycleCoroutineScope(lifecycle) + InternalErrorHandler() + Dispatchers.Default
|
val loaderScope = lifecycle.lifecycleScope + InternalErrorHandler() + Dispatchers.Default
|
||||||
|
|
||||||
private val tasks = LongSparseArray<ProgressDeferred<Uri, Float>>()
|
private val tasks = LongSparseArray<ProgressDeferred<Uri, Float>>()
|
||||||
private val semaphore = Semaphore(3)
|
private val semaphore = Semaphore(3)
|
||||||
@@ -121,6 +131,41 @@ class PageLoader @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun loadPreview(page: MangaPage): ImageSource? {
|
||||||
|
val preview = page.preview
|
||||||
|
if (preview.isNullOrEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val request = ImageRequest.Builder(context)
|
||||||
|
.data(preview)
|
||||||
|
.mangaSourceExtra(page.source)
|
||||||
|
.transformations(TrimTransformation())
|
||||||
|
.build()
|
||||||
|
return coil.execute(request).image?.toImageSource()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun peekPreviewSource(preview: String?): ImageSource? {
|
||||||
|
if (preview.isNullOrEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
coil.memoryCache?.let { cache ->
|
||||||
|
val key = MemoryCache.Key(preview)
|
||||||
|
cache[key]?.image?.let {
|
||||||
|
return if (it is BitmapImage) {
|
||||||
|
ImageSource.cachedBitmap(it.toBitmap())
|
||||||
|
} else {
|
||||||
|
ImageSource.bitmap(it.toBitmap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
coil.diskCache?.let { cache ->
|
||||||
|
cache.openSnapshot(preview)?.use { snapshot ->
|
||||||
|
return ImageSource.file(snapshot.data.toFile())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
fun loadPageAsync(page: MangaPage, force: Boolean): ProgressDeferred<Uri, Float> {
|
fun loadPageAsync(page: MangaPage, force: Boolean): ProgressDeferred<Uri, Float> {
|
||||||
var task = tasks[page.id]?.takeIf { it.isValid() }
|
var task = tasks[page.id]?.takeIf { it.isValid() }
|
||||||
if (force) {
|
if (force) {
|
||||||
@@ -237,7 +282,7 @@ class PageLoader @Inject constructor(
|
|||||||
if (!skipCache) {
|
if (!skipCache) {
|
||||||
cache.get(pageUrl)?.let { return it.toUri() }
|
cache.get(pageUrl)?.let { return it.toUri() }
|
||||||
}
|
}
|
||||||
val uri = Uri.parse(pageUrl)
|
val uri = pageUrl.toUri()
|
||||||
return when {
|
return when {
|
||||||
uri.isZipUri() -> if (uri.scheme == URI_SCHEME_ZIP) {
|
uri.isZipUri() -> if (uri.scheme == URI_SCHEME_ZIP) {
|
||||||
uri
|
uri
|
||||||
@@ -264,6 +309,12 @@ class PageLoader @Inject constructor(
|
|||||||
return context.ramAvailable <= FileSize.MEGABYTES.convert(PREFETCH_MIN_RAM_MB, FileSize.BYTES)
|
return context.ramAvailable <= FileSize.MEGABYTES.convert(PREFETCH_MIN_RAM_MB, FileSize.BYTES)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Image.toImageSource(): ImageSource = if (this is BitmapImage) {
|
||||||
|
ImageSource.cachedBitmap(toBitmap())
|
||||||
|
} else {
|
||||||
|
ImageSource.bitmap(toBitmap())
|
||||||
|
}
|
||||||
|
|
||||||
private fun Deferred<Uri>.isValid(): Boolean {
|
private fun Deferred<Uri>.isValid(): Boolean {
|
||||||
return getCompletionResultOrNull()?.map { uri ->
|
return getCompletionResultOrNull()?.map { uri ->
|
||||||
uri.exists() && uri.isTargetNotEmpty()
|
uri.exists() && uri.isTargetNotEmpty()
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import kotlinx.coroutines.flow.onEach
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.yield
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||||
import org.koitharu.kotatsu.core.os.NetworkState
|
import org.koitharu.kotatsu.core.os.NetworkState
|
||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
@@ -108,19 +107,29 @@ class PageHolderDelegate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onReady() {
|
override fun onReady() {
|
||||||
state = State.SHOWING
|
if (state >= State.LOADED) {
|
||||||
error = null
|
state = State.SHOWING
|
||||||
callback.onImageShowing(readerSettings)
|
error = null
|
||||||
|
callback.onImageShowing(readerSettings, isPreview = false)
|
||||||
|
} else if (state == State.LOADING_WITH_PREVIEW) {
|
||||||
|
callback.onImageShowing(readerSettings, isPreview = true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onImageLoaded() {
|
override fun onImageLoaded() {
|
||||||
state = State.SHOWN
|
if (state >= State.LOADED) {
|
||||||
error = null
|
state = State.SHOWN
|
||||||
callback.onImageShown()
|
error = null
|
||||||
|
callback.onImageShown()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onImageLoadError(e: Throwable) {
|
override fun onImageLoadError(e: Throwable) {
|
||||||
e.printStackTraceDebug()
|
e.printStackTraceDebug()
|
||||||
|
if (state < State.LOADED) {
|
||||||
|
// ignore preview error
|
||||||
|
return
|
||||||
|
}
|
||||||
val uri = this.uri
|
val uri = this.uri
|
||||||
error = e
|
error = e
|
||||||
if (state == State.LOADED && e is IOException && uri != null && uri.toFileOrNull()?.exists() != false) {
|
if (state == State.LOADED && e is IOException && uri != null && uri.toFileOrNull()?.exists() != false) {
|
||||||
@@ -133,7 +142,7 @@ class PageHolderDelegate(
|
|||||||
|
|
||||||
override fun onChanged(value: ReaderSettings) {
|
override fun onChanged(value: ReaderSettings) {
|
||||||
if (state == State.SHOWN) {
|
if (state == State.SHOWN) {
|
||||||
callback.onImageShowing(readerSettings)
|
callback.onImageShowing(readerSettings, isPreview = false)
|
||||||
}
|
}
|
||||||
callback.onConfigChanged()
|
callback.onConfigChanged()
|
||||||
}
|
}
|
||||||
@@ -172,21 +181,25 @@ class PageHolderDelegate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun doLoad(data: MangaPage, force: Boolean) {
|
private suspend fun doLoad(data: MangaPage, force: Boolean) = coroutineScope {
|
||||||
state = State.LOADING
|
state = State.LOADING
|
||||||
error = null
|
error = null
|
||||||
callback.onLoadingStarted()
|
callback.onLoadingStarted()
|
||||||
yield()
|
launch {
|
||||||
|
val preview = loader.loadPreview(data) ?: return@launch
|
||||||
|
if (state == State.LOADING) {
|
||||||
|
state = State.LOADING_WITH_PREVIEW
|
||||||
|
callback.onPreviewReady(preview)
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
val task = withContext(Dispatchers.Default) {
|
val task = withContext(Dispatchers.Default) {
|
||||||
loader.loadPageAsync(data, force)
|
loader.loadPageAsync(data, force)
|
||||||
}
|
}
|
||||||
uri = coroutineScope {
|
val progressObserver = observeProgress(this, task.progressAsFlow())
|
||||||
val progressObserver = observeProgress(this, task.progressAsFlow())
|
val file = task.await()
|
||||||
val file = task.await()
|
progressObserver.cancelAndJoin()
|
||||||
progressObserver.cancelAndJoin()
|
uri = file
|
||||||
file
|
|
||||||
}
|
|
||||||
state = State.LOADED
|
state = State.LOADED
|
||||||
cachedBounds = if (readerSettings.isPagesCropEnabled(isWebtoon)) {
|
cachedBounds = if (readerSettings.isPagesCropEnabled(isWebtoon)) {
|
||||||
loader.getTrimmedBounds(checkNotNull(uri))
|
loader.getTrimmedBounds(checkNotNull(uri))
|
||||||
@@ -223,7 +236,7 @@ class PageHolderDelegate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum class State {
|
enum class State {
|
||||||
EMPTY, LOADING, LOADED, CONVERTING, CONVERTED, SHOWING, SHOWN, ERROR
|
EMPTY, LOADING, LOADING_WITH_PREVIEW, LOADED, CONVERTING, CONVERTED, SHOWING, SHOWN, ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
@@ -232,9 +245,11 @@ class PageHolderDelegate(
|
|||||||
|
|
||||||
fun onError(e: Throwable)
|
fun onError(e: Throwable)
|
||||||
|
|
||||||
|
fun onPreviewReady(source: ImageSource)
|
||||||
|
|
||||||
fun onImageReady(source: ImageSource)
|
fun onImageReady(source: ImageSource)
|
||||||
|
|
||||||
fun onImageShowing(settings: ReaderSettings)
|
fun onImageShowing(settings: ReaderSettings, isPreview: Boolean)
|
||||||
|
|
||||||
fun onImageShown()
|
fun onImageShown()
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class DoublePageHolder(
|
|||||||
.gravity = (if (isEven) Gravity.START else Gravity.END) or Gravity.BOTTOM
|
.gravity = (if (isEven) Gravity.START else Gravity.END) or Gravity.BOTTOM
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onImageShowing(settings: ReaderSettings) {
|
override fun onImageShowing(settings: ReaderSettings, isPreview: Boolean) {
|
||||||
with(binding.ssiv) {
|
with(binding.ssiv) {
|
||||||
maxScale = 2f * maxOf(
|
maxScale = 2f * maxOf(
|
||||||
width / sWidth.toFloat(),
|
width / sWidth.toFloat(),
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class ReversedPageHolder(
|
|||||||
.gravity = Gravity.START or Gravity.BOTTOM
|
.gravity = Gravity.START or Gravity.BOTTOM
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onImageShowing(settings: ReaderSettings) {
|
override fun onImageShowing(settings: ReaderSettings, isPreview: Boolean) {
|
||||||
with(binding.ssiv) {
|
with(binding.ssiv) {
|
||||||
maxScale = 2f * maxOf(
|
maxScale = 2f * maxOf(
|
||||||
width / sWidth.toFloat(),
|
width / sWidth.toFloat(),
|
||||||
|
|||||||
@@ -89,11 +89,15 @@ open class PageHolder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPreviewReady(source: ImageSource) {
|
||||||
|
binding.ssiv.setImage(source)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onImageReady(source: ImageSource) {
|
override fun onImageReady(source: ImageSource) {
|
||||||
binding.ssiv.setImage(source)
|
binding.ssiv.setImage(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onImageShowing(settings: ReaderSettings) {
|
override fun onImageShowing(settings: ReaderSettings, isPreview: Boolean) {
|
||||||
binding.ssiv.maxScale = 2f * maxOf(
|
binding.ssiv.maxScale = 2f * maxOf(
|
||||||
binding.ssiv.width / binding.ssiv.sWidth.toFloat(),
|
binding.ssiv.width / binding.ssiv.sWidth.toFloat(),
|
||||||
binding.ssiv.height / binding.ssiv.sHeight.toFloat(),
|
binding.ssiv.height / binding.ssiv.sHeight.toFloat(),
|
||||||
|
|||||||
@@ -89,11 +89,13 @@ class WebtoonHolder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPreviewReady(source: ImageSource) = Unit
|
||||||
|
|
||||||
override fun onImageReady(source: ImageSource) {
|
override fun onImageReady(source: ImageSource) {
|
||||||
binding.ssiv.setImage(source)
|
binding.ssiv.setImage(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onImageShowing(settings: ReaderSettings) {
|
override fun onImageShowing(settings: ReaderSettings, isPreview: Boolean) {
|
||||||
binding.ssiv.colorFilter = settings.colorFilter?.toColorFilter()
|
binding.ssiv.colorFilter = settings.colorFilter?.toColorFilter()
|
||||||
with(binding.ssiv) {
|
with(binding.ssiv) {
|
||||||
scrollTo(
|
scrollTo(
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:indeterminate="true"
|
android:indeterminate="true"
|
||||||
android:max="100" />
|
android:max="100"
|
||||||
|
app:hideAnimationBehavior="escape"
|
||||||
|
app:showAnimationBehavior="outward" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/layout_error"
|
android:id="@+id/layout_error"
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ material = "1.13.0-alpha11"
|
|||||||
moshi = "1.15.2"
|
moshi = "1.15.2"
|
||||||
okhttp = "4.12.0"
|
okhttp = "4.12.0"
|
||||||
okio = "3.10.2"
|
okio = "3.10.2"
|
||||||
parsers = "bebc615376"
|
parsers = "5fa7590550"
|
||||||
preference = "1.2.1"
|
preference = "1.2.1"
|
||||||
recyclerview = "1.4.0"
|
recyclerview = "1.4.0"
|
||||||
room = "2.6.1"
|
room = "2.6.1"
|
||||||
ssiv = "ba48c29803"
|
ssiv = "9a67b6a7c9"
|
||||||
swiperefreshlayout = "1.1.0"
|
swiperefreshlayout = "1.1.0"
|
||||||
testRules = "1.6.1"
|
testRules = "1.6.1"
|
||||||
testRunner = "1.6.2"
|
testRunner = "1.6.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user