Code cleanup and refactor

This commit is contained in:
Koitharu
2024-05-10 08:57:45 +03:00
parent 77ad21bd7a
commit 82684601b7
36 changed files with 161 additions and 414 deletions

View File

@@ -12,9 +12,6 @@ import org.koitharu.kotatsu.core.db.entity.MangaWithTags
@Dao
abstract class BookmarksDao {
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
abstract suspend fun find(mangaId: Long, pageId: Long): BookmarkEntity?
@Query("SELECT * FROM bookmarks WHERE page_id = :pageId")
abstract suspend fun find(pageId: Long): BookmarkEntity?
@@ -42,9 +39,6 @@ abstract class BookmarksDao {
@Delete
abstract suspend fun delete(entity: BookmarkEntity)
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
abstract suspend fun delete(mangaId: Long, pageId: Long): Int
@Query("DELETE FROM bookmarks WHERE page_id = :pageId")
abstract suspend fun delete(pageId: Long): Int

View File

@@ -26,9 +26,6 @@ import kotlinx.coroutines.flow.asSharedFlow
import okhttp3.OkHttpClient
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
import org.koitharu.kotatsu.core.cache.ContentCache
import org.koitharu.kotatsu.core.cache.MemoryContentCache
import org.koitharu.kotatsu.core.cache.StubContentCache
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
import org.koitharu.kotatsu.core.network.MangaHttpClient
@@ -159,18 +156,6 @@ interface AppModule {
acraScreenLogger,
)
@Provides
@Singleton
fun provideContentCache(
application: Application,
): ContentCache {
return if (application.isLowRamDevice()) {
StubContentCache()
} else {
MemoryContentCache(application)
}
}
@Provides
@Singleton
@LocalStorageChanges

View File

@@ -1,29 +0,0 @@
package org.koitharu.kotatsu.core.cache
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
interface ContentCache {
val isCachingEnabled: Boolean
suspend fun getDetails(source: MangaSource, url: String): Manga?
fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>)
suspend fun getPages(source: MangaSource, url: String): List<MangaPage>?
fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>)
suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>?
fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>)
fun clear(source: MangaSource)
data class Key(
val source: MangaSource,
val url: String,
)
}

View File

@@ -2,18 +2,19 @@ package org.koitharu.kotatsu.core.cache
import androidx.collection.LruCache
import java.util.concurrent.TimeUnit
import org.koitharu.kotatsu.core.cache.MemoryContentCache.Key as CacheKey
class ExpiringLruCache<T>(
val maxSize: Int,
private val lifetime: Long,
private val timeUnit: TimeUnit,
) : Iterable<ContentCache.Key> {
) : Iterable<CacheKey> {
private val cache = LruCache<ContentCache.Key, ExpiringValue<T>>(maxSize)
private val cache = LruCache<CacheKey, ExpiringValue<T>>(maxSize)
override fun iterator(): Iterator<ContentCache.Key> = cache.snapshot().keys.iterator()
override fun iterator(): Iterator<CacheKey> = cache.snapshot().keys.iterator()
operator fun get(key: ContentCache.Key): T? {
operator fun get(key: CacheKey): T? {
val value = cache[key] ?: return null
if (value.isExpired) {
cache.remove(key)
@@ -21,7 +22,7 @@ class ExpiringLruCache<T>(
return value.get()
}
operator fun set(key: ContentCache.Key, value: T) {
operator fun set(key: CacheKey, value: T) {
cache.put(key, ExpiringValue(value, lifetime, timeUnit))
}
@@ -33,7 +34,7 @@ class ExpiringLruCache<T>(
cache.trimToSize(size)
}
fun remove(key: ContentCache.Key) {
fun remove(key: CacheKey) {
cache.remove(key)
}
}

View File

@@ -3,48 +3,54 @@ package org.koitharu.kotatsu.core.cache
import android.app.Application
import android.content.ComponentCallbacks2
import android.content.res.Configuration
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
class MemoryContentCache(application: Application) : ContentCache, ComponentCallbacks2 {
@Singleton
class MemoryContentCache @Inject constructor(application: Application) : ComponentCallbacks2 {
private val isLowRam = application.isLowRamDevice()
init {
application.registerComponentCallbacks(this)
}
private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(4, 5, TimeUnit.MINUTES)
private val pagesCache = ExpiringLruCache<SafeDeferred<List<MangaPage>>>(4, 10, TimeUnit.MINUTES)
private val relatedMangaCache = ExpiringLruCache<SafeDeferred<List<Manga>>>(4, 10, TimeUnit.MINUTES)
private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(if (isLowRam) 1 else 4, 5, TimeUnit.MINUTES)
private val pagesCache =
ExpiringLruCache<SafeDeferred<List<MangaPage>>>(if (isLowRam) 1 else 4, 10, TimeUnit.MINUTES)
private val relatedMangaCache =
ExpiringLruCache<SafeDeferred<List<Manga>>>(if (isLowRam) 1 else 3, 10, TimeUnit.MINUTES)
override val isCachingEnabled: Boolean = true
override suspend fun getDetails(source: MangaSource, url: String): Manga? {
return detailsCache[ContentCache.Key(source, url)]?.awaitOrNull()
suspend fun getDetails(source: MangaSource, url: String): Manga? {
return detailsCache[Key(source, url)]?.awaitOrNull()
}
override fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) {
detailsCache[ContentCache.Key(source, url)] = details
fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) {
detailsCache[Key(source, url)] = details
}
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? {
return pagesCache[ContentCache.Key(source, url)]?.awaitOrNull()
suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? {
return pagesCache[Key(source, url)]?.awaitOrNull()
}
override fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) {
pagesCache[ContentCache.Key(source, url)] = pages
fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) {
pagesCache[Key(source, url)] = pages
}
override suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>? {
return relatedMangaCache[ContentCache.Key(source, url)]?.awaitOrNull()
suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>? {
return relatedMangaCache[Key(source, url)]?.awaitOrNull()
}
override fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>) {
relatedMangaCache[ContentCache.Key(source, url)] = related
fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>) {
relatedMangaCache[Key(source, url)] = related
}
override fun clear(source: MangaSource) {
fun clear(source: MangaSource) {
clearCache(detailsCache, source)
clearCache(pagesCache, source)
clearCache(relatedMangaCache, source)
@@ -81,4 +87,9 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
}
}
}
data class Key(
val source: MangaSource,
val url: String,
)
}

View File

@@ -1,24 +0,0 @@
package org.koitharu.kotatsu.core.cache
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
class StubContentCache : ContentCache {
override val isCachingEnabled: Boolean = false
override suspend fun getDetails(source: MangaSource, url: String): Manga? = null
override fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) = Unit
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? = null
override fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) = Unit
override suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>? = null
override fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>) = Unit
override fun clear(source: MangaSource) = Unit
}

View File

@@ -40,7 +40,7 @@ abstract class MangaDao {
abstract suspend fun searchByTitle(query: String, source: String, limit: Int): List<MangaWithTags>
@Upsert
abstract suspend fun upsert(manga: MangaEntity)
protected abstract suspend fun upsert(manga: MangaEntity)
@Update(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun update(manga: MangaEntity): Int

View File

@@ -23,9 +23,6 @@ abstract class MangaSourcesDao {
@Query("SELECT * FROM sources WHERE enabled = 0 ORDER BY sort_key")
abstract suspend fun findAllDisabled(): List<MangaSourceEntity>
@Query("SELECT * FROM sources WHERE enabled = 0")
abstract fun observeDisabled(): Flow<List<MangaSourceEntity>>
@Query("SELECT * FROM sources ORDER BY sort_key")
abstract fun observeAll(): Flow<List<MangaSourceEntity>>

View File

@@ -28,9 +28,6 @@ interface TrackLogsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(entity: TrackLogEntity): Long
@Query("DELETE FROM track_logs WHERE manga_id = :mangaId")
suspend fun removeAll(mangaId: Long)
@Query("DELETE FROM track_logs WHERE manga_id NOT IN (SELECT manga_id FROM tracks)")
suspend fun gc()

View File

@@ -1,7 +1,7 @@
package org.koitharu.kotatsu.core.parser
import androidx.annotation.AnyThread
import org.koitharu.kotatsu.core.cache.ContentCache
import org.koitharu.kotatsu.core.cache.MemoryContentCache
import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.MangaLoaderContext
@@ -57,7 +57,7 @@ interface MangaRepository {
class Factory @Inject constructor(
private val localMangaRepository: LocalMangaRepository,
private val loaderContext: MangaLoaderContext,
private val contentCache: ContentCache,
private val contentCache: MemoryContentCache,
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
) {

View File

@@ -13,10 +13,11 @@ import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.Response
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.cache.ContentCache
import org.koitharu.kotatsu.core.cache.MemoryContentCache
import org.koitharu.kotatsu.core.cache.SafeDeferred
import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.core.util.MultiMutex
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
@@ -37,10 +38,14 @@ import java.util.Locale
class RemoteMangaRepository(
private val parser: MangaParser,
private val cache: ContentCache,
private val cache: MemoryContentCache, // TODO fix concurrency
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
) : MangaRepository, Interceptor {
private val detailsMutex = MultiMutex<Long>()
private val relatedMangaMutex = MultiMutex<Long>()
private val pagesMutex = MultiMutex<Long>()
override val source: MangaSource
get() = parser.source
@@ -96,7 +101,7 @@ class RemoteMangaRepository(
override suspend fun getDetails(manga: Manga): Manga = getDetails(manga, CachePolicy.ENABLED)
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = pagesMutex.withLock(chapter.id) {
cache.getPages(source, chapter.url)?.let { return it }
val pages = asyncSafe {
mirrorSwitchInterceptor.withMirrorSwitching {
@@ -104,8 +109,8 @@ class RemoteMangaRepository(
}
}
cache.putPages(source, chapter.url, pages)
return pages.await()
}
pages
}.await()
override suspend fun getPageUrl(page: MangaPage): String = mirrorSwitchInterceptor.withMirrorSwitching {
parser.getPageUrl(page)
@@ -123,16 +128,16 @@ class RemoteMangaRepository(
parser.getFavicons()
}
override suspend fun getRelated(seed: Manga): List<Manga> {
override suspend fun getRelated(seed: Manga): List<Manga> = relatedMangaMutex.withLock(seed.id) {
cache.getRelatedManga(source, seed.url)?.let { return it }
val related = asyncSafe {
parser.getRelatedManga(seed).filterNot { it.id == seed.id }
}
cache.putRelatedManga(source, seed.url, related)
return related.await()
}
related
}.await()
suspend fun getDetails(manga: Manga, cachePolicy: CachePolicy): Manga {
suspend fun getDetails(manga: Manga, cachePolicy: CachePolicy): Manga = detailsMutex.withLock(manga.id) {
if (cachePolicy.readEnabled) {
cache.getDetails(source, manga.url)?.let { return it }
}
@@ -144,8 +149,8 @@ class RemoteMangaRepository(
if (cachePolicy.writeEnabled) {
cache.putDetails(source, manga.url, details)
}
return details.await()
}
details
}.await()
suspend fun peekDetails(manga: Manga): Manga? {
return cache.getDetails(source, manga.url)

View File

@@ -3,33 +3,27 @@ package org.koitharu.kotatsu.core.ui
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.ActionBarContextView
import androidx.appcompat.widget.Toolbar
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.viewbinding.ViewBinding
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
import org.koitharu.kotatsu.core.ui.util.BaseActivityEntryPoint
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.isWebViewUnavailable
@Suppress("LeakingThis")
@@ -127,32 +121,13 @@ abstract class BaseActivity<B : ViewBinding> :
@CallSuper
override fun onSupportActionModeStarted(mode: ActionMode) {
super.onSupportActionModeStarted(mode)
actionModeDelegate.onSupportActionModeStarted(mode)
val actionModeColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ColorUtils.compositeColors(
ContextCompat.getColor(this, com.google.android.material.R.color.m3_appbar_overlay_color),
getThemeColor(com.google.android.material.R.attr.colorSurface),
)
} else {
ContextCompat.getColor(this, R.color.kotatsu_background)
}
defaultStatusBarColor = window.statusBarColor
window.statusBarColor = actionModeColor
val insets = ViewCompat.getRootWindowInsets(viewBinding.root)
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar).apply {
setBackgroundColor(actionModeColor)
updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
}
}
actionModeDelegate.onSupportActionModeStarted(mode, window)
}
@CallSuper
override fun onSupportActionModeFinished(mode: ActionMode) {
super.onSupportActionModeFinished(mode)
actionModeDelegate.onSupportActionModeFinished(mode)
window.statusBarColor = defaultStatusBarColor
actionModeDelegate.onSupportActionModeFinished(mode, window)
}
protected open fun dispatchNavigateUp() {
@@ -185,6 +160,12 @@ abstract class BaseActivity<B : ViewBinding> :
}
}
@EntryPoint
@InstallIn(SingletonComponent::class)
interface BaseActivityEntryPoint {
val settings: AppSettings
}
companion object {
const val EXTRA_DATA = "data"

View File

@@ -2,8 +2,6 @@ package org.koitharu.kotatsu.core.ui.sheet
import android.app.Dialog
import android.content.Context
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -16,15 +14,8 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDialog
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.ActionBarContextView
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.viewbinding.ViewBinding
import com.google.android.material.bottomsheet.BottomSheetBehavior
@@ -33,14 +24,12 @@ import com.google.android.material.sidesheet.SideSheetDialog
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import com.google.android.material.R as materialR
abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
private var waitingForDismissAllowingStateLoss = false
private var isFitToContentsDisabled = false
private var defaultStatusBarColor = Color.TRANSPARENT
var viewBinding: B? = null
private set
@@ -105,40 +94,18 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
@CallSuper
protected open fun dispatchSupportActionModeStarted(mode: ActionMode) {
actionModeDelegate?.onSupportActionModeStarted(mode)
val ctx = requireContext()
val actionModeColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ColorUtils.compositeColors(
ContextCompat.getColor(ctx, com.google.android.material.R.color.m3_appbar_overlay_color),
ctx.getThemeColor(com.google.android.material.R.attr.colorSurface),
)
} else {
ContextCompat.getColor(ctx, R.color.kotatsu_surface)
}
dialog?.window?.let {
defaultStatusBarColor = it.statusBarColor
it.statusBarColor = actionModeColor
}
val insets = ViewCompat.getRootWindowInsets(requireView())
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
dialog?.window?.decorView?.findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar)?.apply {
setBackgroundColor(actionModeColor)
updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
}
}
actionModeDelegate?.onSupportActionModeStarted(mode, dialog?.window)
}
@CallSuper
protected open fun dispatchSupportActionModeFinished(mode: ActionMode) {
actionModeDelegate?.onSupportActionModeFinished(mode)
dialog?.window?.statusBarColor = defaultStatusBarColor
actionModeDelegate?.onSupportActionModeFinished(mode, dialog?.window)
}
fun addSheetCallback(callback: AdaptiveSheetCallback, lifecycleOwner: LifecycleOwner): Boolean {
val b = behavior ?: return false
b.addCallback(callback)
val rootView = dialog?.findViewById<View>(materialR.id.design_bottom_sheet)
val rootView = dialog?.findViewById(materialR.id.design_bottom_sheet)
?: dialog?.findViewById(materialR.id.coordinator)
?: view
if (rootView != null) {

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.core.ui.util
package org.koitharu.kotatsu.core.ui.sheet
import android.view.View
import androidx.activity.OnBackPressedCallback
@@ -6,9 +6,8 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HALF_EXPANDED
import org.koitharu.kotatsu.core.util.ext.doOnExpansionsChanged
class BottomSheetClollapseCallback(
class BottomSheetCollapseCallback(
private val behavior: BottomSheetBehavior<*>,
) : OnBackPressedCallback(behavior.state == STATE_EXPANDED) {

View File

@@ -1,14 +1,28 @@
package org.koitharu.kotatsu.core.ui.util
import android.graphics.Color
import android.os.Build
import android.view.ViewGroup
import android.view.Window
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.ActionBarContextView
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import com.google.android.material.R as materialR
class ActionModeDelegate : OnBackPressedCallback(false) {
private var activeActionMode: ActionMode? = null
private var listeners: MutableList<ActionModeListener>? = null
private var defaultStatusBarColor = Color.TRANSPARENT
val isActionModeStarted: Boolean
get() = activeActionMode != null
@@ -17,16 +31,40 @@ class ActionModeDelegate : OnBackPressedCallback(false) {
finishActionMode()
}
fun onSupportActionModeStarted(mode: ActionMode) {
fun onSupportActionModeStarted(mode: ActionMode, window: Window?) {
activeActionMode = mode
isEnabled = true
listeners?.forEach { it.onActionModeStarted(mode) }
if (window != null) {
val ctx = window.context
val actionModeColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ColorUtils.compositeColors(
ContextCompat.getColor(ctx, materialR.color.m3_appbar_overlay_color),
ctx.getThemeColor(materialR.attr.colorSurface),
)
} else {
ContextCompat.getColor(ctx, R.color.kotatsu_surface)
}
defaultStatusBarColor = window.statusBarColor
window.statusBarColor = actionModeColor
val insets = ViewCompat.getRootWindowInsets(window.decorView)
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
window.decorView.findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar)?.apply {
setBackgroundColor(actionModeColor)
updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
}
}
}
}
fun onSupportActionModeFinished(mode: ActionMode) {
fun onSupportActionModeFinished(mode: ActionMode, window: Window?) {
activeActionMode = null
isEnabled = false
listeners?.forEach { it.onActionModeFinished(mode) }
if (window != null) {
window.statusBarColor = defaultStatusBarColor
}
}
fun addListener(listener: ActionModeListener) {

View File

@@ -1,12 +0,0 @@
package org.koitharu.kotatsu.core.ui.util
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.koitharu.kotatsu.core.prefs.AppSettings
@EntryPoint
@InstallIn(SingletonComponent::class)
interface BaseActivityEntryPoint {
val settings: AppSettings
}

View File

@@ -1,21 +0,0 @@
package org.koitharu.kotatsu.core.ui.util
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.bottomsheet.BottomSheetBehavior
class BottomSheetNoHalfExpandedCallback() : BottomSheetBehavior.BottomSheetCallback() {
private var previousStableState = BottomSheetBehavior.STATE_COLLAPSED
override fun onStateChanged(sheet: View, state: Int) {
if (state == BottomSheetBehavior.STATE_HALF_EXPANDED) {
val behavior = (sheet.layoutParams as? CoordinatorLayout.LayoutParams)?.behavior as? BottomSheetBehavior<*>
behavior?.state = previousStableState
} else if (state == BottomSheetBehavior.STATE_EXPANDED || state == BottomSheetBehavior.STATE_COLLAPSED) {
previousStableState = state
}
}
override fun onSlide(sheet: View, offset: Float) = Unit
}

View File

@@ -1,42 +0,0 @@
package org.koitharu.kotatsu.core.ui.util
import android.animation.ValueAnimator
import android.view.animation.AccelerateDecelerateInterpolator
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.shape.MaterialShapeDrawable
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import com.google.android.material.R as materialR
class StatusBarDimHelper : AppBarLayout.OnOffsetChangedListener {
private var animator: ValueAnimator? = null
private val interpolator = AccelerateDecelerateInterpolator()
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
val foreground = appBarLayout.statusBarForeground ?: return
val start = foreground.alpha
val collapsed = verticalOffset != 0
val end = if (collapsed) 255 else 0
animator?.cancel()
if (start == end) {
animator = null
return
}
animator = ValueAnimator.ofInt(start, end).apply {
duration = appBarLayout.context.getAnimationDuration(materialR.integer.app_bar_elevation_anim_duration)
interpolator = this@StatusBarDimHelper.interpolator
addUpdateListener {
foreground.alpha = it.animatedValue as Int
}
start()
}
}
fun attachToAppBar(appBarLayout: AppBarLayout) {
appBarLayout.addOnOffsetChangedListener(this)
appBarLayout.statusBarForeground =
MaterialShapeDrawable.createWithElevationOverlay(appBarLayout.context).apply {
alpha = 0
}
}
}

View File

@@ -1,36 +0,0 @@
package org.koitharu.kotatsu.core.ui.widgets
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.viewpager.widget.ViewPager
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
@SuppressLint("ClickableViewAccessibility")
class EnhancedViewPager @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : ViewPager(context, attrs) {
var isUserInputEnabled: Boolean = true
set(value) {
field = value
if (!value) {
cancelPendingInputEvents()
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
return isUserInputEnabled && super.onTouchEvent(event)
}
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
return try {
isUserInputEnabled && super.onInterceptTouchEvent(event)
} catch (e: IllegalArgumentException) {
e.printStackTraceDebug()
false
}
}
}

View File

@@ -2,6 +2,8 @@ package org.koitharu.kotatsu.core.util
import androidx.collection.ArrayMap
import kotlinx.coroutines.sync.Mutex
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
class MultiMutex<T : Any> : Set<T> {
@@ -10,12 +12,12 @@ class MultiMutex<T : Any> : Set<T> {
override val size: Int
get() = delegates.size
override fun contains(element: T): Boolean {
return delegates.containsKey(element)
override fun contains(element: T): Boolean = synchronized(delegates) {
delegates.containsKey(element)
}
override fun containsAll(elements: Collection<T>): Boolean {
return elements.all { x -> delegates.containsKey(x) }
override fun containsAll(elements: Collection<T>): Boolean = synchronized(delegates) {
elements.all { x -> delegates.containsKey(x) }
}
override fun isEmpty(): Boolean {
@@ -40,4 +42,16 @@ class MultiMutex<T : Any> : Set<T> {
delegates.remove(element)?.unlock()
}
}
suspend inline fun <R> withLock(element: T, block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return try {
lock(element)
block()
} finally {
unlock(element)
}
}
}

View File

@@ -4,7 +4,7 @@ import android.content.Context
import android.content.Intent
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.EntryPointAccessors
import org.koitharu.kotatsu.core.cache.ContentCache
import org.koitharu.kotatsu.core.cache.MemoryContentCache
import org.koitharu.kotatsu.core.model.findById
import org.koitharu.kotatsu.core.model.parcelable.ParcelableChapter
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
@@ -27,7 +27,7 @@ class MangaPrefetchService : CoroutineIntentService() {
lateinit var mangaRepositoryFactory: MangaRepository.Factory
@Inject
lateinit var cache: ContentCache
lateinit var cache: MemoryContentCache
@Inject
lateinit var historyRepository: HistoryRepository
@@ -120,7 +120,7 @@ class MangaPrefetchService : CoroutineIntentService() {
context,
PrefetchCompanionEntryPoint::class.java,
)
return entryPoint.contentCache.isCachingEnabled && entryPoint.settings.isContentPrefetchEnabled
return entryPoint.settings.isContentPrefetchEnabled
}
private fun tryStart(context: Context, intent: Intent) {

View File

@@ -3,12 +3,10 @@ package org.koitharu.kotatsu.details.service
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.koitharu.kotatsu.core.cache.ContentCache
import org.koitharu.kotatsu.core.prefs.AppSettings
@EntryPoint
@InstallIn(SingletonComponent::class)
interface PrefetchCompanionEntryPoint {
val settings: AppSettings
val contentCache: ContentCache
}

View File

@@ -53,7 +53,7 @@ import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.image.ChipIconTarget
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.util.BottomSheetClollapseCallback
import org.koitharu.kotatsu.core.ui.sheet.BottomSheetCollapseCallback
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
@@ -154,7 +154,7 @@ class DetailsActivity :
viewBinding.chipsTags.onChipClickListener = this
TitleScrollCoordinator(viewBinding.textViewTitle).attach(viewBinding.scrollView)
viewBinding.containerBottomSheet?.let { BottomSheetBehavior.from(it) }?.let { behavior ->
onBackPressedDispatcher.addCallback(BottomSheetClollapseCallback(behavior))
onBackPressedDispatcher.addCallback(BottomSheetCollapseCallback(behavior))
}
viewModel.details.filterNotNull().observe(this, ::onMangaUpdated)

View File

@@ -205,8 +205,8 @@ class ExploreFragment :
startActivity(SettingsActivity.newSourceSettingsIntent(requireContext(), sourceItem.source))
}
R.id.action_hide -> {
viewModel.hideSource(sourceItem.source)
R.id.action_disable -> {
viewModel.disableSource(sourceItem.source)
}
R.id.action_shortcut -> {

View File

@@ -92,7 +92,7 @@ class ExploreViewModel @Inject constructor(
}
}
fun hideSource(source: MangaSource) {
fun disableSource(source: MangaSource) {
launchJob(Dispatchers.Default) {
val rollback = sourcesRepository.setSourceEnabled(source, isEnabled = false)
onActionDone.call(ReversibleAction(R.string.source_disabled, rollback))

View File

@@ -20,7 +20,7 @@ abstract class FavouriteCategoriesDao {
abstract fun observeAll(): Flow<List<FavouriteCategoryEntity>>
@Query("SELECT * FROM favourite_categories WHERE deleted_at = 0 AND show_in_lib = 1 ORDER BY sort_key")
abstract fun observeAllForLibrary(): Flow<List<FavouriteCategoryEntity>>
abstract fun observeAllVisible(): Flow<List<FavouriteCategoryEntity>>
@Query("SELECT * FROM favourite_categories WHERE category_id = :id AND deleted_at = 0")
abstract fun observe(id: Long): Flow<FavouriteCategoryEntity?>
@@ -40,7 +40,7 @@ abstract class FavouriteCategoriesDao {
abstract suspend fun updateTracking(id: Long, isEnabled: Boolean)
@Query("UPDATE favourite_categories SET `show_in_lib` = :isEnabled WHERE category_id = :id")
abstract suspend fun updateLibVisibility(id: Long, isEnabled: Boolean)
abstract suspend fun updateVisibility(id: Long, isEnabled: Boolean)
@Query("UPDATE favourite_categories SET sort_key = :sortKey WHERE category_id = :id")
abstract suspend fun updateSortKey(id: Long, sortKey: Int)

View File

@@ -11,7 +11,6 @@ import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow
import org.intellij.lang.annotations.Language
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.favourites.domain.model.Cover
import org.koitharu.kotatsu.list.domain.ListSortOrder
@@ -39,13 +38,6 @@ abstract class FavouritesDao {
return observeAllImpl(query)
}
@Transaction
@Query(
"SELECT * FROM favourites WHERE deleted_at = 0 " +
"GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit OFFSET :offset",
)
abstract suspend fun findAll(offset: Int, limit: Int): List<FavouriteManga>
@Transaction
@Query("SELECT * FROM favourites WHERE deleted_at = 0 ORDER BY created_at DESC LIMIT :limit OFFSET :offset")
abstract suspend fun findAllRaw(offset: Int, limit: Int): List<FavouriteManga>
@@ -72,19 +64,6 @@ abstract class FavouritesDao {
return observeAllImpl(query)
}
@Transaction
@Query(
"SELECT * FROM favourites WHERE category_id = :categoryId AND deleted_at = 0 " +
"GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit OFFSET :offset",
)
abstract suspend fun findAll(categoryId: Long, offset: Int, limit: Int): List<FavouriteManga>
@Query(
"SELECT * FROM manga WHERE manga_id IN " +
"(SELECT manga_id FROM favourites WHERE category_id = :categoryId AND deleted_at = 0)",
)
abstract suspend fun findAllManga(categoryId: Int): List<MangaEntity>
suspend fun findCovers(categoryId: Long, order: ListSortOrder): List<Cover> {
val orderBy = getOrderBy(order)
@@ -114,21 +93,9 @@ abstract class FavouritesDao {
@Query("SELECT COUNT(DISTINCT manga_id) FROM favourites WHERE deleted_at = 0")
abstract fun observeMangaCount(): Flow<Int>
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM favourites WHERE deleted_at = 0)")
abstract suspend fun findAllManga(): List<MangaEntity>
@Transaction
@Query("SELECT * FROM favourites WHERE manga_id = :id AND deleted_at = 0 GROUP BY manga_id")
abstract suspend fun find(id: Long): FavouriteManga?
@Query("SELECT * FROM favourites WHERE manga_id = :mangaId AND deleted_at = 0")
abstract suspend fun findAllRaw(mangaId: Long): List<FavouriteEntity>
@Transaction
@Deprecated("Ignores order")
@Query("SELECT * FROM favourites WHERE manga_id = :id AND deleted_at = 0 GROUP BY manga_id")
abstract fun observe(id: Long): Flow<FavouriteManga?>
@Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id = :id AND deleted_at = 0")
abstract fun observeIds(id: Long): Flow<List<Long>>
@@ -138,9 +105,6 @@ abstract class FavouritesDao {
@Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id IN (:mangaIds) AND deleted_at = 0 ORDER BY favourites.created_at ASC")
abstract suspend fun findCategoriesIds(mangaIds: Collection<Long>): List<Long>
@Query("SELECT DISTINCT favourite_categories.category_id FROM favourites LEFT JOIN favourite_categories ON favourites.category_id = favourite_categories.category_id WHERE manga_id = :mangaId AND favourites.deleted_at = 0 AND favourite_categories.deleted_at = 0 AND favourite_categories.track = 1")
abstract suspend fun findCategoriesIdsWithTrack(mangaId: Long): List<Long>
/** INSERT **/
@Insert(onConflict = OnConflictStrategy.REPLACE)
@@ -194,7 +158,7 @@ abstract class FavouritesDao {
protected abstract suspend fun setDeletedAt(mangaId: Long, deletedAt: Long)
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE manga_id = :mangaId AND category_id = :categoryId")
abstract suspend fun setDeletedAt(categoryId: Long, mangaId: Long, deletedAt: Long)
protected abstract suspend fun setDeletedAt(categoryId: Long, mangaId: Long, deletedAt: Long)
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE category_id = :categoryId AND deleted_at = 0")
protected abstract suspend fun setDeletedAtAll(categoryId: Long, deletedAt: Long)

View File

@@ -76,7 +76,7 @@ class FavouritesRepository @Inject constructor(
}
fun observeCategoriesForLibrary(): Flow<List<FavouriteCategory>> {
return db.getFavouriteCategoriesDao().observeAllForLibrary().mapItems {
return db.getFavouriteCategoriesDao().observeAllVisible().mapItems {
it.toFavouriteCategory()
}.distinctUntilChanged()
}
@@ -157,7 +157,7 @@ class FavouritesRepository @Inject constructor(
}
suspend fun updateCategory(id: Long, isVisibleInLibrary: Boolean) {
db.getFavouriteCategoriesDao().updateLibVisibility(id, isVisibleInLibrary)
db.getFavouriteCategoriesDao().updateVisibility(id, isVisibleInLibrary)
}
suspend fun updateCategoryTracking(id: Long, isTrackingEnabled: Boolean) {

View File

@@ -10,7 +10,6 @@ import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow
import org.intellij.lang.annotations.Language
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.list.domain.ListSortOrder
@@ -21,10 +20,6 @@ abstract class HistoryDao {
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC LIMIT :limit OFFSET :offset")
abstract suspend fun findAll(offset: Int, limit: Int): List<HistoryWithManga>
@Transaction
@Query("SELECT * FROM history WHERE deleted_at = 0 AND manga_id IN (:ids)")
abstract suspend fun findAll(ids: Collection<Long>): List<HistoryEntity>
@Transaction
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC")
abstract fun observeAll(): Flow<List<HistoryWithManga>>
@@ -33,6 +28,7 @@ abstract class HistoryDao {
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC LIMIT :limit")
abstract fun observeAll(limit: Int): Flow<List<HistoryWithManga>>
// TODO pagination
fun observeAll(order: ListSortOrder): Flow<List<HistoryWithManga>> {
val orderBy = when (order) {
ListSortOrder.LAST_READ -> "history.updated_at DESC"
@@ -56,9 +52,6 @@ abstract class HistoryDao {
return observeAllImpl(query)
}
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history WHERE deleted_at = 0)")
abstract suspend fun findAllManga(): List<MangaEntity>
@Query("SELECT manga_id FROM history WHERE deleted_at = 0")
abstract suspend fun findAllIds(): LongArray

View File

@@ -8,7 +8,7 @@ import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.cache.ContentCache
import org.koitharu.kotatsu.core.cache.MemoryContentCache
import org.koitharu.kotatsu.core.network.DoHProvider
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
@@ -23,11 +23,10 @@ class NetworkSettingsFragment :
SharedPreferences.OnSharedPreferenceChangeListener {
@Inject
lateinit var contentCache: ContentCache
lateinit var contentCache: MemoryContentCache
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_network)
findPreference<Preference>(AppSettings.KEY_PREFETCH_CONTENT)?.isVisible = contentCache.isCachingEnabled
findPreference<ListPreference>(AppSettings.KEY_DOH)?.run {
entryValues = DoHProvider.entries.names()
setDefaultValueCompat(DoHProvider.NONE.name)

View File

@@ -1,25 +1,18 @@
package org.koitharu.kotatsu.stats.data
import android.database.sqlite.SQLiteQueryBuilder
import androidx.room.Dao
import androidx.room.MapColumn
import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Transaction
import androidx.room.Upsert
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.history.data.HistoryWithManga
@Dao
abstract class StatsDao {
@Query("SELECT * FROM stats ORDER BY started_at")
abstract suspend fun findAll(): List<StatsEntity>
@Query("SELECT * FROM stats WHERE manga_id = :mangaId ORDER BY started_at")
abstract suspend fun findAll(mangaId: Long): List<StatsEntity>
@@ -32,12 +25,6 @@ abstract class StatsDao {
@Query("SELECT IFNULL(SUM(duration)/SUM(pages), 0) FROM stats")
abstract suspend fun getAverageTimePerPage(): Long
@Query("SELECT IFNULL(SUM(duration), 0) FROM stats WHERE manga_id = :mangaId")
abstract suspend fun getReadingTime(mangaId: Long): Long
@Query("SELECT IFNULL(SUM(duration), 0) FROM stats")
abstract suspend fun getTotalReadingTime(): Long
@Query("DELETE FROM stats")
abstract suspend fun clear()
@@ -47,7 +34,11 @@ abstract class StatsDao {
@Upsert
abstract suspend fun upsert(entity: StatsEntity)
suspend fun getDurationStats(fromDate: Long, isNsfw: Boolean?, favouriteCategories: Set<Long>): Map<MangaEntity, Long> {
suspend fun getDurationStats(
fromDate: Long,
isNsfw: Boolean?,
favouriteCategories: Set<Long>
): Map<MangaEntity, Long> {
val conditions = ArrayList<String>()
conditions.add("stats.started_at >= $fromDate")
if (favouriteCategories.isNotEmpty()) {
@@ -66,7 +57,7 @@ abstract class StatsDao {
}
@RawQuery
protected abstract fun getDurationStatsImpl(
protected abstract suspend fun getDurationStatsImpl(
query: SupportSQLiteQuery
): Map<@MapColumn("manga") MangaEntity, @MapColumn("d") Long>
}

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.tracker.data
import androidx.room.Dao
import androidx.room.MapColumn
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Upsert
@@ -10,9 +9,6 @@ import kotlinx.coroutines.flow.Flow
@Dao
abstract class TracksDao {
@Query("SELECT * FROM tracks")
abstract suspend fun findAll(): List<TrackEntity>
@Transaction
@Query("SELECT * FROM tracks ORDER BY last_check_time ASC LIMIT :limit OFFSET :offset")
abstract suspend fun findAll(offset: Int, limit: Int): List<TrackWithManga>
@@ -24,9 +20,6 @@ abstract class TracksDao {
@Query("SELECT manga_id FROM tracks")
abstract suspend fun findAllIds(): LongArray
@Query("SELECT * FROM tracks WHERE manga_id IN (:ids)")
abstract suspend fun findAll(ids: Collection<Long>): List<TrackEntity>
@Query("SELECT * FROM tracks WHERE manga_id = :mangaId")
abstract suspend fun find(mangaId: Long): TrackEntity?
@@ -36,9 +29,6 @@ abstract class TracksDao {
@Query("SELECT COUNT(*) FROM tracks")
abstract suspend fun getTracksCount(): Int
@Query("SELECT manga_id, chapters_new FROM tracks")
abstract fun observeNewChaptersMap(): Flow<Map<@MapColumn(columnName = "manga_id") Long, @MapColumn(columnName = "chapters_new") Int>>
@Query("SELECT chapters_new FROM tracks")
abstract fun observeNewChapters(): Flow<List<Int>>

View File

@@ -15,8 +15,6 @@ import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
import java.time.Instant
import javax.inject.Inject
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
@Reusable
class Tracker @Inject constructor(
@@ -25,6 +23,8 @@ class Tracker @Inject constructor(
private val mangaRepositoryFactory: MangaRepository.Factory,
) {
private val mangaMutex = MultiMutex<Long>()
suspend fun getTracks(limit: Int): List<MangaTracking> {
repository.updateTracks()
return repository.getTracks(offset = 0, limit = limit)
@@ -37,7 +37,7 @@ class Tracker @Inject constructor(
suspend fun fetchUpdates(
track: MangaTracking,
commit: Boolean
): MangaUpdates = withMangaLock(track.manga.id) {
): MangaUpdates = mangaMutex.withLock(track.manga.id) {
val updates = runCatchingCancellable {
val repo = mangaRepositoryFactory.create(track.manga.source)
require(repo is RemoteMangaRepository) { "Repository ${repo.javaClass.simpleName} is not supported" }
@@ -52,7 +52,7 @@ class Tracker @Inject constructor(
if (commit) {
repository.saveUpdates(updates)
}
return updates
updates
}
suspend fun syncWithDetails(details: Manga) {
@@ -94,7 +94,7 @@ class Tracker @Inject constructor(
}
@VisibleForTesting
suspend fun deleteTrack(mangaId: Long) = withMangaLock(mangaId) {
suspend fun deleteTrack(mangaId: Long) = mangaMutex.withLock(mangaId) {
repository.deleteTrack(mangaId)
}
@@ -135,18 +135,5 @@ class Tracker @Inject constructor(
private companion object {
const val NO_ID = 0L
private val mangaMutex = MultiMutex<Long>()
suspend inline fun <T> withMangaLock(id: Long, action: () -> T): T {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
mangaMutex.lock(id)
try {
return action()
} finally {
mangaMutex.unlock(id)
}
}
}
}

View File

@@ -11,7 +11,7 @@
android:title="@string/create_shortcut" />
<item
android:id="@+id/action_hide"
android:title="@string/hide" />
android:id="@+id/action_disable"
android:title="@string/disable" />
</menu>

View File

@@ -641,4 +641,5 @@
<string name="suggested_queries">Suggested queries</string>
<string name="authors">Authors</string>
<string name="blocked_by_server_message">You are blocked by the server. Try to use a different network connection (VPN, Proxy, etc.)</string>
<string name="disable">Disable</string>
</resources>

View File

@@ -10,7 +10,6 @@
android:entryValues="@array/values_network_policy"
android:key="prefetch_content"
android:title="@string/prefetch_content"
app:isPreferenceVisible="false"
app:useSimpleSummaryProvider="true"
tools:isPreferenceVisible="true" />