diff --git a/app/build.gradle b/app/build.gradle index cd538ea26..73554c773 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,8 +15,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 32 - versionCode 419 - versionName '3.4.7' + versionCode 420 + versionName '3.5.0' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -84,7 +84,7 @@ afterEvaluate { } } dependencies { - implementation('com.github.KotatsuApp:kotatsu-parsers:7588617316') { + implementation('com.github.KotatsuApp:kotatsu-parsers:85bfe42ddf') { exclude group: 'org.json', module: 'json' } diff --git a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt index dbc3bc2fc..e2802c6c6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt @@ -1,7 +1,6 @@ package org.koitharu.kotatsu.download.domain import android.content.Context -import android.net.ConnectivityManager import android.webkit.MimeTypeMap import coil.ImageLoader import coil.request.ImageRequest @@ -20,6 +19,7 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.download.ui.service.PausingHandle import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.local.domain.CbzMangaOutput import org.koitharu.kotatsu.local.domain.LocalMangaRepository @@ -29,10 +29,9 @@ import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.utils.ext.deleteAwait import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.referer -import org.koitharu.kotatsu.utils.ext.waitForNetwork -import org.koitharu.kotatsu.utils.progress.ProgressJob +import org.koitharu.kotatsu.utils.progress.PausingProgressJob -private const val MAX_DOWNLOAD_ATTEMPTS = 3 +private const val MAX_FAILSAFE_ATTEMPTS = 2 private const val DOWNLOAD_ERROR_DELAY = 500L private const val SLOWDOWN_DELAY = 200L @@ -47,9 +46,6 @@ class DownloadManager @AssistedInject constructor( private val mangaRepositoryFactory: MangaRepository.Factory, ) { - private val connectivityManager = context.getSystemService( - Context.CONNECTIVITY_SERVICE, - ) as ConnectivityManager private val coverWidth = context.resources.getDimensionPixelSize( androidx.core.R.dimen.compat_notification_large_icon_max_width, ) @@ -62,18 +58,20 @@ class DownloadManager @AssistedInject constructor( manga: Manga, chaptersIds: LongArray?, startId: Int, - ): ProgressJob { + ): PausingProgressJob { val stateFlow = MutableStateFlow( DownloadState.Queued(startId = startId, manga = manga, cover = null), ) - val job = downloadMangaImpl(manga, chaptersIds?.takeUnless { it.isEmpty() }, stateFlow, startId) - return ProgressJob(job, stateFlow) + val pausingHandle = PausingHandle() + val job = downloadMangaImpl(manga, chaptersIds?.takeUnless { it.isEmpty() }, stateFlow, pausingHandle, startId) + return PausingProgressJob(job, stateFlow, pausingHandle) } private fun downloadMangaImpl( manga: Manga, chaptersIds: LongArray?, outState: MutableStateFlow, + pausingHandle: PausingHandle, startId: Int, ): Job = coroutineScope.launch(Dispatchers.Default + errorStateHandler(outState)) { @Suppress("NAME_SHADOWING") @@ -113,36 +111,24 @@ class DownloadManager @AssistedInject constructor( "${chaptersIdsSet?.size} of ${chaptersIds?.size} requested chapters not found in manga" } for ((chapterIndex, chapter) in chapters.withIndex()) { - val pages = repo.getPages(chapter) + val pages = runFailsafe(outState, pausingHandle) { + repo.getPages(chapter) + } for ((pageIndex, page) in pages.withIndex()) { - var retryCounter = 0 - failsafe@ while (true) { - try { - val url = repo.getPageUrl(page) - val file = cache[url] ?: downloadFile(url, page.referer, destination, tempFileName) - output.addPage( - chapter = chapter, - file = file, - pageNumber = pageIndex, - ext = MimeTypeMap.getFileExtensionFromUrl(url), - ) - break@failsafe - } catch (e: IOException) { - if (retryCounter < MAX_DOWNLOAD_ATTEMPTS) { - outState.value = DownloadState.WaitingForNetwork(startId, data, cover) - delay(DOWNLOAD_ERROR_DELAY) - connectivityManager.waitForNetwork() - retryCounter++ - } else { - throw e - } - } + runFailsafe(outState, pausingHandle) { + val url = repo.getPageUrl(page) + val file = cache[url] ?: downloadFile(url, page.referer, destination, tempFileName) + output.addPage( + chapter = chapter, + file = file, + pageNumber = pageIndex, + ext = MimeTypeMap.getFileExtensionFromUrl(url), + ) } - outState.value = DownloadState.Progress( - startId, - data, - cover, + startId = startId, + manga = data, + cover = cover, totalChapters = chapters.size, currentChapter = chapterIndex, totalPages = pages.size, @@ -164,15 +150,40 @@ class DownloadManager @AssistedInject constructor( throw e } catch (e: Throwable) { e.printStackTraceDebug() - outState.value = DownloadState.Error(startId, manga, cover, e) + outState.value = DownloadState.Error(startId, manga, cover, e, false) } finally { withContext(NonCancellable) { output?.cleanup() File(destination, tempFileName).deleteAwait() + coroutineContext[WakeLockNode]?.release() + semaphore.release() + localMangaRepository.unlockManga(manga.id) + } + } + } + + private suspend fun runFailsafe( + outState: MutableStateFlow, + pausingHandle: PausingHandle, + block: suspend () -> R, + ): R { + var countDown = MAX_FAILSAFE_ATTEMPTS + failsafe@ while (true) { + try { + return block() + } catch (e: IOException) { + if (countDown <= 0) { + val state = outState.value + outState.value = DownloadState.Error(state.startId, state.manga, state.cover, e, true) + countDown = MAX_FAILSAFE_ATTEMPTS + pausingHandle.pause() + pausingHandle.awaitResumed() + outState.value = state + } else { + countDown-- + delay(DOWNLOAD_ERROR_DELAY) + } } - coroutineContext[WakeLockNode]?.release() - semaphore.release() - localMangaRepository.unlockManga(manga.id) } } @@ -202,6 +213,7 @@ class DownloadManager @AssistedInject constructor( manga = prevValue.manga, cover = prevValue.cover, error = throwable, + canRetry = false, ) } diff --git a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadState.kt b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadState.kt index a0a78ac7a..d7f86abfb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadState.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadState.kt @@ -108,6 +108,7 @@ sealed interface DownloadState { } } + @Deprecated("TODO: remove") class WaitingForNetwork( override val startId: Int, override val manga: Manga, @@ -170,6 +171,7 @@ sealed interface DownloadState { override val manga: Manga, override val cover: Drawable?, val error: Throwable, + val canRetry: Boolean, ) : DownloadState { override fun equals(other: Any?): Boolean { @@ -182,6 +184,7 @@ sealed interface DownloadState { if (manga != other.manga) return false if (cover != other.cover) return false if (error != other.error) return false + if (canRetry != other.canRetry) return false return true } @@ -191,6 +194,7 @@ sealed interface DownloadState { result = 31 * result + manga.hashCode() result = 31 * result + (cover?.hashCode() ?: 0) result = 31 * result + error.hashCode() + result = 31 * result + canRetry.hashCode() return result } } diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt index 27b1bdd6a..5a6997de7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt @@ -29,16 +29,26 @@ class DownloadNotification(private val context: Context, startId: Int) { context.getString(android.R.string.cancel), PendingIntent.getBroadcast( context, - startId, + startId * 2, DownloadService.getCancelIntent(startId), PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE, ), ) + private val retryAction = NotificationCompat.Action( + R.drawable.ic_restart_black, + context.getString(R.string.try_again), + PendingIntent.getBroadcast( + context, + startId * 2 + 1, + DownloadService.getResumeIntent(startId), + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) + ) private val listIntent = PendingIntent.getActivity( context, REQUEST_LIST, DownloadsActivity.newIntent(context), - PendingIntentCompat.FLAG_IMMUTABLE, + PendingIntentCompat.FLAG_IMMUTABLE ) init { @@ -89,10 +99,14 @@ class DownloadNotification(private val context: Context, startId: Int) { builder.setSmallIcon(android.R.drawable.stat_notify_error) builder.setSubText(context.getString(R.string.error)) builder.setContentText(message) - builder.setAutoCancel(true) - builder.setOngoing(false) + builder.setAutoCancel(!state.canRetry) + builder.setOngoing(state.canRetry) builder.setCategory(NotificationCompat.CATEGORY_ERROR) builder.setStyle(NotificationCompat.BigTextStyle().bigText(message)) + if (state.canRetry) { + builder.addAction(cancelAction) + builder.addAction(retryAction) + } } is DownloadState.PostProcessing -> { builder.setProgress(1, 0, true) diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt index c9cd1aec8..dd58ee6a1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt @@ -27,6 +27,7 @@ import org.koitharu.kotatsu.download.domain.DownloadState import org.koitharu.kotatsu.download.domain.WakeLockNode import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.utils.ext.throttle +import org.koitharu.kotatsu.utils.progress.PausingProgressJob import org.koitharu.kotatsu.utils.progress.ProgressJob import org.koitharu.kotatsu.utils.progress.TimeLeftEstimator @@ -39,7 +40,7 @@ class DownloadService : BaseService() { @Inject lateinit var downloadManagerFactory: DownloadManager.Factory - private val jobs = LinkedHashMap>() + private val jobs = LinkedHashMap>() private val jobCount = MutableStateFlow(0) private val controlReceiver = ControlReceiver() private var binder: DownloadBinder? = null @@ -54,7 +55,10 @@ class DownloadService : BaseService() { coroutineScope = lifecycleScope + WakeLockNode(wakeLock, TimeUnit.HOURS.toMillis(1)), ) DownloadNotification.createChannel(this) - registerReceiver(controlReceiver, IntentFilter(ACTION_DOWNLOAD_CANCEL)) + val intentFilter = IntentFilter() + intentFilter.addAction(ACTION_DOWNLOAD_CANCEL) + intentFilter.addAction(ACTION_DOWNLOAD_RESUME) + registerReceiver(controlReceiver, intentFilter) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -92,7 +96,7 @@ class DownloadService : BaseService() { startId: Int, manga: Manga, chaptersIds: LongArray?, - ): ProgressJob { + ): PausingProgressJob { val job = downloadManager.downloadManga(manga, chaptersIds, startId) listenJob(job) return job @@ -146,7 +150,7 @@ class DownloadService : BaseService() { } private val DownloadState.isTerminal: Boolean - get() = this is DownloadState.Done || this is DownloadState.Error || this is DownloadState.Cancelled + get() = this is DownloadState.Done || this is DownloadState.Cancelled || (this is DownloadState.Error && !canRetry) inner class ControlReceiver : BroadcastReceiver() { @@ -157,6 +161,10 @@ class DownloadService : BaseService() { jobs.remove(cancelId)?.cancel() jobCount.value = jobs.size } + ACTION_DOWNLOAD_RESUME -> { + val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0) + jobs[cancelId]?.resume() + } } } } @@ -175,6 +183,7 @@ class DownloadService : BaseService() { const val ACTION_DOWNLOAD_COMPLETE = "${BuildConfig.APPLICATION_ID}.action.ACTION_DOWNLOAD_COMPLETE" private const val ACTION_DOWNLOAD_CANCEL = "${BuildConfig.APPLICATION_ID}.action.ACTION_DOWNLOAD_CANCEL" + private const val ACTION_DOWNLOAD_RESUME = "${BuildConfig.APPLICATION_ID}.action.ACTION_DOWNLOAD_RESUME" private const val EXTRA_MANGA = "manga" private const val EXTRA_CHAPTERS_IDS = "chapters_ids" @@ -217,6 +226,9 @@ class DownloadService : BaseService() { fun getCancelIntent(startId: Int) = Intent(ACTION_DOWNLOAD_CANCEL) .putExtra(EXTRA_CANCEL_ID, startId) + fun getResumeIntent(startId: Int) = Intent(ACTION_DOWNLOAD_RESUME) + .putExtra(EXTRA_CANCEL_ID, startId) + fun getDownloadedManga(intent: Intent?): Manga? { if (intent?.action == ACTION_DOWNLOAD_COMPLETE) { return intent.getParcelableExtra(EXTRA_MANGA)?.manga diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/PausingHandle.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/PausingHandle.kt new file mode 100644 index 000000000..499f88f34 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/PausingHandle.kt @@ -0,0 +1,30 @@ +package org.koitharu.kotatsu.download.ui.service + +import androidx.annotation.AnyThread +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first + +class PausingHandle { + + private val paused = MutableStateFlow(false) + + @get:AnyThread + val isPaused: Boolean + get() = paused.value + + @AnyThread + suspend fun awaitResumed() { + paused.filter { !it }.first() + } + + @AnyThread + fun pause() { + paused.value = true + } + + @AnyThread + fun resume() { + paused.value = false + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt index 1ba72f858..17b95bf05 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt @@ -4,6 +4,7 @@ import android.content.Context import android.net.Uri import android.webkit.MimeTypeMap import androidx.activity.result.ActivityResultLauncher +import androidx.core.net.toUri import dagger.hilt.android.qualifiers.ApplicationContext import java.io.File import javax.inject.Inject @@ -61,7 +62,11 @@ class PageSaveHelper @Inject constructor( } != null private suspend fun getProposedFileName(url: String, file: File): String { - var name = url.toHttpUrl().pathSegments.last() + var name = if (url.startsWith("cbz://")) { + requireNotNull(url.toUri().fragment) + } else { + url.toHttpUrl().pathSegments.last() + } var extension = name.substringAfterLast('.', "") name = name.substringBeforeLast('.') if (extension.length !in 2..4) { diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt index 58aba0ee6..a4d60c861 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt @@ -7,6 +7,8 @@ import android.view.View import androidx.core.view.isVisible import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.model.ZoomMode @@ -26,6 +28,8 @@ open class PageHolder( View.OnClickListener { init { + binding.ssiv.setExecutor(Dispatchers.Default.asExecutor()) + binding.ssiv.setEagerLoadingEnabled(!isLowRamDevice(context)) binding.ssiv.setOnImageEventListener(delegate) @Suppress("LeakingThis") bindingInfo.buttonRetry.setOnClickListener(this) diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonFrameLayout.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonFrameLayout.kt index 2fe267156..fd1e45290 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonFrameLayout.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonFrameLayout.kt @@ -3,13 +3,16 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon import android.content.Context import android.util.AttributeSet import android.widget.FrameLayout +import androidx.annotation.AttrRes import org.koitharu.kotatsu.R class WebtoonFrameLayout @JvmOverloads constructor( - context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 + context: Context, + attrs: AttributeSet? = null, + @AttrRes defStyleAttr: Int = 0, ) : FrameLayout(context, attrs, defStyleAttr) { - private val target by lazy { + private val target by lazy(LazyThreadSafetyMode.NONE) { findViewById(R.id.ssiv) } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt index cc4370748..4d9fc1ed8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt @@ -13,14 +13,14 @@ import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder import org.koitharu.kotatsu.reader.ui.pager.ReaderPage +import org.koitharu.kotatsu.utils.GoneOnInvisibleListener import org.koitharu.kotatsu.utils.ext.* - class WebtoonHolder( binding: ItemPageWebtoonBinding, loader: PageLoader, settings: AppSettings, - exceptionResolver: ExceptionResolver + exceptionResolver: ExceptionResolver, ) : BasePageHolder(binding, loader, settings, exceptionResolver), View.OnClickListener { @@ -29,6 +29,7 @@ class WebtoonHolder( init { binding.ssiv.setOnImageEventListener(delegate) bindingInfo.buttonRetry.setOnClickListener(this) + GoneOnInvisibleListener(bindingInfo.progressBar).attach() } override fun onBind(data: ReaderPage) { @@ -61,9 +62,9 @@ class WebtoonHolder( override fun onImageShowing(zoom: ZoomMode) { with(binding.ssiv) { - maxScale = 2f * width / sWidth.toFloat() setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM) minScale = width / sWidth.toFloat() + maxScale = minScale scrollTo( when { scrollToRestore != 0 -> scrollToRestore diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonImageView.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonImageView.kt index 31396bd06..3404d2911 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonImageView.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonImageView.kt @@ -1,11 +1,15 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon -import android.app.Activity import android.content.Context import android.graphics.PointF import android.util.AttributeSet +import androidx.recyclerview.widget.RecyclerView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor import org.koitharu.kotatsu.parsers.util.toIntUp +import org.koitharu.kotatsu.utils.ext.isLowRamDevice +import org.koitharu.kotatsu.utils.ext.parents private const val SCROLL_UNKNOWN = -1 @@ -15,15 +19,15 @@ class WebtoonImageView @JvmOverloads constructor( ) : SubsamplingScaleImageView(context, attr) { private val ct = PointF() - private val displayHeight = if (context is Activity) { - context.window.decorView.height - } else { - context.resources.displayMetrics.heightPixels - } private var scrollPos = 0 private var scrollRange = SCROLL_UNKNOWN + init { + setExecutor(Dispatchers.Default.asExecutor()) + setEagerLoadingEnabled(!isLowRamDevice(context)) + } + fun scrollBy(delta: Int) { val maxScroll = getScrollRange() if (maxScroll == 0) { @@ -36,6 +40,7 @@ class WebtoonImageView @JvmOverloads constructor( fun scrollTo(y: Int) { val maxScroll = getScrollRange() if (maxScroll == 0) { + resetScaleAndCenter() return } scrollToInternal(y.coerceIn(0, maxScroll)) @@ -58,8 +63,11 @@ class WebtoonImageView @JvmOverloads constructor( override fun getSuggestedMinimumHeight(): Int { var desiredHeight = super.getSuggestedMinimumHeight() - if (sHeight == 0 && desiredHeight < displayHeight) { - desiredHeight = displayHeight + if (sHeight == 0) { + val parentHeight = parentHeight() + if (desiredHeight < parentHeight) { + desiredHeight = parentHeight + } } return desiredHeight } @@ -84,7 +92,7 @@ class WebtoonImageView @JvmOverloads constructor( } } width = width.coerceAtLeast(suggestedMinimumWidth) - height = height.coerceIn(suggestedMinimumHeight, displayHeight) + height = height.coerceIn(suggestedMinimumHeight, parentHeight()) setMeasuredDimension(width, height) } @@ -101,4 +109,8 @@ class WebtoonImageView @JvmOverloads constructor( val totalHeight = (sHeight * minScale).toIntUp() scrollRange = (totalHeight - height).coerceAtLeast(0) } + + private fun parentHeight(): Int { + return parents.firstNotNullOfOrNull { it as? RecyclerView }?.height ?: 0 + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonLayoutManager.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonLayoutManager.kt index 29f21d08e..067ada3ac 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonLayoutManager.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonLayoutManager.kt @@ -6,6 +6,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlin.math.sign +@Suppress("unused") class WebtoonLayoutManager : LinearLayoutManager { private var scrollDirection: Int = 0 diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt index ff6bdb48e..e310ee2ca 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.reader.ui.thumbnails.adapter import android.graphics.drawable.Drawable import coil.ImageLoader import coil.request.ImageRequest +import coil.size.Scale import coil.size.Size import com.google.android.material.R as materialR import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding @@ -23,14 +24,13 @@ fun pageThumbnailAD( loader: PageLoader, clickListener: OnListItemClickListener, ) = adapterDelegateViewBinding( - { inflater, parent -> ItemPageThumbBinding.inflate(inflater, parent, false) } + { inflater, parent -> ItemPageThumbBinding.inflate(inflater, parent, false) }, ) { - var job: Job? = null val gridWidth = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width) val thumbSize = Size( width = gridWidth, - height = (gridWidth * 13f / 18f).toInt() + height = (gridWidth * 13f / 18f).toInt(), ) suspend fun loadPageThumbnail(item: PageThumbnail): Drawable? = withContext(Dispatchers.Default) { @@ -40,8 +40,9 @@ fun pageThumbnailAD( .data(url) .referer(item.page.referer) .size(thumbSize) - .allowRgb565(isLowRamDevice(context)) - .build() + .scale(Scale.FILL) + .allowRgb565(true) + .build(), ).drawable }?.let { drawable -> return@withContext drawable @@ -52,7 +53,7 @@ fun pageThumbnailAD( .data(file) .size(thumbSize) .allowRgb565(isLowRamDevice(context)) - .build() + .build(), ).drawable } @@ -81,4 +82,4 @@ fun pageThumbnailAD( job = null binding.imageViewThumb.setImageDrawable(null) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/GoneOnInvisibleListener.kt b/app/src/main/java/org/koitharu/kotatsu/utils/GoneOnInvisibleListener.kt new file mode 100644 index 000000000..da4fe3f3f --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/GoneOnInvisibleListener.kt @@ -0,0 +1,23 @@ +package org.koitharu.kotatsu.utils + +import android.view.View +import android.view.ViewTreeObserver + +/** + * ProgressIndicator become INVISIBLE instead of GONE by hide() call. + * It`s final so we need this workaround + */ +class GoneOnInvisibleListener( + private val view: View, +) : ViewTreeObserver.OnGlobalLayoutListener { + + override fun onGlobalLayout() { + if (view.visibility == View.INVISIBLE) { + view.visibility = View.GONE + } + } + + fun attach() { + view.viewTreeObserver.addOnGlobalLayoutListener(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt index 54fc9a4c1..864061300 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt @@ -11,8 +11,6 @@ import android.content.pm.ResolveInfo import android.database.SQLException import android.graphics.Color import android.net.ConnectivityManager -import android.net.Network -import android.net.NetworkRequest import android.net.Uri import android.os.Build import android.provider.Settings @@ -29,7 +27,6 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.coroutineScope import androidx.work.CoroutineWorker import com.google.android.material.elevation.ElevationOverlayProvider -import kotlin.coroutines.resume import kotlin.math.roundToLong import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.trySendBlocking @@ -39,7 +36,6 @@ import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine import okio.IOException import org.json.JSONException import org.koitharu.kotatsu.BuildConfig @@ -51,28 +47,6 @@ val Context.activityManager: ActivityManager? val Context.connectivityManager: ConnectivityManager get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager -suspend fun ConnectivityManager.waitForNetwork(): Network { - val request = NetworkRequest.Builder().build() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - // fast path - activeNetwork?.let { return it } - } - return suspendCancellableCoroutine { cont -> - val callback = object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - unregisterNetworkCallback(this) - if (cont.isActive) { - cont.resume(network) - } - } - } - registerNetworkCallback(request, callback) - cont.invokeOnCancellation { - unregisterNetworkCallback(callback) - } - } -} - fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this) suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatching { diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt index d8174c8d5..5044ab6f1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt @@ -160,8 +160,6 @@ fun RecyclerView.invalidateNestedItemDecorations() { } } -internal val View.compatPaddingStart get() = ViewCompat.getPaddingStart(this) - val View.parents: Sequence get() = sequence { var p: ViewParent? = parent diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/progress/PausingProgressJob.kt b/app/src/main/java/org/koitharu/kotatsu/utils/progress/PausingProgressJob.kt new file mode 100644 index 000000000..e53806080 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/progress/PausingProgressJob.kt @@ -0,0 +1,26 @@ +package org.koitharu.kotatsu.utils.progress + +import androidx.annotation.AnyThread +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.StateFlow +import org.koitharu.kotatsu.download.ui.service.PausingHandle + +class PausingProgressJob

( + job: Job, + progress: StateFlow

, + private val pausingHandle: PausingHandle, +) : ProgressJob

(job, progress) { + + @get:AnyThread + val isPaused: Boolean + get() = pausingHandle.isPaused + + @AnyThread + suspend fun awaitResumed() = pausingHandle.awaitResumed() + + @AnyThread + fun pause() = pausingHandle.pause() + + @AnyThread + fun resume() = pausingHandle.resume() +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/progress/ProgressJob.kt b/app/src/main/java/org/koitharu/kotatsu/utils/progress/ProgressJob.kt index d401fc83a..919d952ab 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/progress/ProgressJob.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/progress/ProgressJob.kt @@ -4,7 +4,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow -class ProgressJob

( +open class ProgressJob

( private val job: Job, private val progress: StateFlow

, ) : Job by job { diff --git a/app/src/main/res/drawable/ic_restart_black.xml b/app/src/main/res/drawable/ic_restart_black.xml new file mode 100644 index 000000000..afa22f223 --- /dev/null +++ b/app/src/main/res/drawable/ic_restart_black.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file