diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt index d38ee4f71..d701edaf1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt @@ -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 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt index 2527dd4f4..f5525f1ce 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt @@ -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 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/ContentCache.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/ContentCache.kt deleted file mode 100644 index a48011836..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/ContentCache.kt +++ /dev/null @@ -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) - - suspend fun getPages(source: MangaSource, url: String): List? - - fun putPages(source: MangaSource, url: String, pages: SafeDeferred>) - - suspend fun getRelatedManga(source: MangaSource, url: String): List? - - fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred>) - - fun clear(source: MangaSource) - - data class Key( - val source: MangaSource, - val url: String, - ) -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/ExpiringLruCache.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/ExpiringLruCache.kt index d669b7b76..750682a84 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/ExpiringLruCache.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/ExpiringLruCache.kt @@ -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( val maxSize: Int, private val lifetime: Long, private val timeUnit: TimeUnit, -) : Iterable { +) : Iterable { - private val cache = LruCache>(maxSize) + private val cache = LruCache>(maxSize) - override fun iterator(): Iterator = cache.snapshot().keys.iterator() + override fun iterator(): Iterator = 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( 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( cache.trimToSize(size) } - fun remove(key: ContentCache.Key) { + fun remove(key: CacheKey) { cache.remove(key) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt index d99152d90..88a3fa19c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt @@ -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>(4, 5, TimeUnit.MINUTES) - private val pagesCache = ExpiringLruCache>>(4, 10, TimeUnit.MINUTES) - private val relatedMangaCache = ExpiringLruCache>>(4, 10, TimeUnit.MINUTES) - - override val isCachingEnabled: Boolean = true + private val detailsCache = ExpiringLruCache>(if (isLowRam) 1 else 4, 5, TimeUnit.MINUTES) + private val pagesCache = + ExpiringLruCache>>(if (isLowRam) 1 else 4, 10, TimeUnit.MINUTES) + private val relatedMangaCache = + ExpiringLruCache>>(if (isLowRam) 1 else 3, 10, TimeUnit.MINUTES) - 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) { - detailsCache[ContentCache.Key(source, url)] = details + fun putDetails(source: MangaSource, url: String, details: SafeDeferred) { + detailsCache[Key(source, url)] = details } - override suspend fun getPages(source: MangaSource, url: String): List? { - return pagesCache[ContentCache.Key(source, url)]?.awaitOrNull() + suspend fun getPages(source: MangaSource, url: String): List? { + return pagesCache[Key(source, url)]?.awaitOrNull() } - override fun putPages(source: MangaSource, url: String, pages: SafeDeferred>) { - pagesCache[ContentCache.Key(source, url)] = pages + fun putPages(source: MangaSource, url: String, pages: SafeDeferred>) { + pagesCache[Key(source, url)] = pages } - override suspend fun getRelatedManga(source: MangaSource, url: String): List? { - return relatedMangaCache[ContentCache.Key(source, url)]?.awaitOrNull() + suspend fun getRelatedManga(source: MangaSource, url: String): List? { + return relatedMangaCache[Key(source, url)]?.awaitOrNull() } - override fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred>) { - relatedMangaCache[ContentCache.Key(source, url)] = related + fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred>) { + 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, + ) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/StubContentCache.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/StubContentCache.kt deleted file mode 100644 index e68d4d885..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/StubContentCache.kt +++ /dev/null @@ -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) = Unit - - override suspend fun getPages(source: MangaSource, url: String): List? = null - - override fun putPages(source: MangaSource, url: String, pages: SafeDeferred>) = Unit - - override suspend fun getRelatedManga(source: MangaSource, url: String): List? = null - - override fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred>) = Unit - - override fun clear(source: MangaSource) = Unit -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaDao.kt index 2189cfc31..619c9befe 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaDao.kt @@ -40,7 +40,7 @@ abstract class MangaDao { abstract suspend fun searchByTitle(query: String, source: String, limit: Int): List @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 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaSourcesDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaSourcesDao.kt index 342050f63..37cd49588 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaSourcesDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaSourcesDao.kt @@ -23,9 +23,6 @@ abstract class MangaSourcesDao { @Query("SELECT * FROM sources WHERE enabled = 0 ORDER BY sort_key") abstract suspend fun findAllDisabled(): List - @Query("SELECT * FROM sources WHERE enabled = 0") - abstract fun observeDisabled(): Flow> - @Query("SELECT * FROM sources ORDER BY sort_key") abstract fun observeAll(): Flow> diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt index 879e708bb..e9580ce6d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt @@ -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() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaRepository.kt index 8971392d8..72210b1e5 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaRepository.kt @@ -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, ) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt index 011d21f68..970be2aa8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt @@ -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() + private val relatedMangaMutex = MultiMutex() + private val pagesMutex = MultiMutex() + 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 { + override suspend fun getPages(chapter: MangaChapter): List = 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 { + override suspend fun getRelated(seed: Manga): List = 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) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseActivity.kt index c7b607991..5f44be8d6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseActivity.kt @@ -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 : @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(androidx.appcompat.R.id.action_mode_bar).apply { - setBackgroundColor(actionModeColor) - updateLayoutParams { - 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 : } } + @EntryPoint + @InstallIn(SingletonComponent::class) + interface BaseActivityEntryPoint { + val settings: AppSettings + } + companion object { const val EXTRA_DATA = "data" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt index 212364282..0674015e9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt @@ -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 : 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 : 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(androidx.appcompat.R.id.action_mode_bar)?.apply { - setBackgroundColor(actionModeColor) - updateLayoutParams { - 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(materialR.id.design_bottom_sheet) + val rootView = dialog?.findViewById(materialR.id.design_bottom_sheet) ?: dialog?.findViewById(materialR.id.coordinator) ?: view if (rootView != null) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/BottomSheetClollapseCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BottomSheetCollapseCallback.kt similarity index 86% rename from app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/BottomSheetClollapseCallback.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BottomSheetCollapseCallback.kt index 4daac0aa4..de18f7b29 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/BottomSheetClollapseCallback.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BottomSheetCollapseCallback.kt @@ -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) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ActionModeDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ActionModeDelegate.kt index 2296aef53..1412f7281 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ActionModeDelegate.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ActionModeDelegate.kt @@ -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? = 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(androidx.appcompat.R.id.action_mode_bar)?.apply { + setBackgroundColor(actionModeColor) + updateLayoutParams { + 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) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/BaseActivityEntryPoint.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/BaseActivityEntryPoint.kt deleted file mode 100644 index 309883319..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/BaseActivityEntryPoint.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/BottomSheetNoHalfExpandedCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/BottomSheetNoHalfExpandedCallback.kt deleted file mode 100644 index 0776c157f..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/BottomSheetNoHalfExpandedCallback.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/StatusBarDimHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/StatusBarDimHelper.kt deleted file mode 100644 index b58f36ae1..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/StatusBarDimHelper.kt +++ /dev/null @@ -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 - } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/EnhancedViewPager.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/EnhancedViewPager.kt deleted file mode 100644 index a8680b5e3..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/EnhancedViewPager.kt +++ /dev/null @@ -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 - } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/MultiMutex.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/MultiMutex.kt index 98d228dcc..6c0b15b9f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/MultiMutex.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/MultiMutex.kt @@ -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 : Set { @@ -10,12 +12,12 @@ class MultiMutex : Set { 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): Boolean { - return elements.all { x -> delegates.containsKey(x) } + override fun containsAll(elements: Collection): Boolean = synchronized(delegates) { + elements.all { x -> delegates.containsKey(x) } } override fun isEmpty(): Boolean { @@ -40,4 +42,16 @@ class MultiMutex : Set { delegates.remove(element)?.unlock() } } + + suspend inline fun withLock(element: T, block: () -> R): R { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + return try { + lock(element) + block() + } finally { + unlock(element) + } + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt index 385c7e416..90cf74ef7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt @@ -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) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/service/PrefetchCompanionEntryPoint.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/service/PrefetchCompanionEntryPoint.kt index 57afeb770..395954991 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/service/PrefetchCompanionEntryPoint.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/service/PrefetchCompanionEntryPoint.kt @@ -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 } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index 293460f5e..4566c3b07 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -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) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt index 2b5d8bfa0..0880a73d7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt @@ -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 -> { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt index c38344f4d..1eb1b626d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt @@ -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)) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt index 9cac803fc..d133b0488 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt @@ -20,7 +20,7 @@ abstract class FavouriteCategoriesDao { abstract fun observeAll(): Flow> @Query("SELECT * FROM favourite_categories WHERE deleted_at = 0 AND show_in_lib = 1 ORDER BY sort_key") - abstract fun observeAllForLibrary(): Flow> + abstract fun observeAllVisible(): Flow> @Query("SELECT * FROM favourite_categories WHERE category_id = :id AND deleted_at = 0") abstract fun observe(id: Long): Flow @@ -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) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt index be0e55db0..061731415 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt @@ -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 - @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 @@ -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 - - @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 - suspend fun findCovers(categoryId: Long, order: ListSortOrder): List { 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 - @Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM favourites WHERE deleted_at = 0)") - abstract suspend fun findAllManga(): List - - @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 - @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 - @Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id = :id AND deleted_at = 0") abstract fun observeIds(id: Long): Flow> @@ -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): List - @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 - /** 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) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt index 5bc869c9f..89fa4987c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt @@ -76,7 +76,7 @@ class FavouritesRepository @Inject constructor( } fun observeCategoriesForLibrary(): Flow> { - 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) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt index 236f79330..815b8e8a3 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt @@ -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 - @Transaction - @Query("SELECT * FROM history WHERE deleted_at = 0 AND manga_id IN (:ids)") - abstract suspend fun findAll(ids: Collection): List - @Transaction @Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC") abstract fun observeAll(): Flow> @@ -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> + // TODO pagination fun observeAll(order: ListSortOrder): Flow> { 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 - @Query("SELECT manga_id FROM history WHERE deleted_at = 0") abstract suspend fun findAllIds(): LongArray diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/NetworkSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/NetworkSettingsFragment.kt index 72a5251d2..16af2c294 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/NetworkSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/NetworkSettingsFragment.kt @@ -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(AppSettings.KEY_PREFETCH_CONTENT)?.isVisible = contentCache.isCachingEnabled findPreference(AppSettings.KEY_DOH)?.run { entryValues = DoHProvider.entries.names() setDefaultValueCompat(DoHProvider.NONE.name) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/stats/data/StatsDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/stats/data/StatsDao.kt index a071d10e6..f0e3cbe56 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/stats/data/StatsDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/stats/data/StatsDao.kt @@ -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 - @Query("SELECT * FROM stats WHERE manga_id = :mangaId ORDER BY started_at") abstract suspend fun findAll(mangaId: Long): List @@ -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): Map { + suspend fun getDurationStats( + fromDate: Long, + isNsfw: Boolean?, + favouriteCategories: Set + ): Map { val conditions = ArrayList() 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> } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt index 02ca68098..e3820b0da 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt @@ -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 - @Transaction @Query("SELECT * FROM tracks ORDER BY last_check_time ASC LIMIT :limit OFFSET :offset") abstract suspend fun findAll(offset: Int, limit: Int): List @@ -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): List - @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> - @Query("SELECT chapters_new FROM tracks") abstract fun observeNewChapters(): Flow> diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt index 787cd8cf0..49f36de9e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt @@ -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() + suspend fun getTracks(limit: Int): List { 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() - - suspend inline fun withMangaLock(id: Long, action: () -> T): T { - contract { - callsInPlace(action, InvocationKind.EXACTLY_ONCE) - } - mangaMutex.lock(id) - try { - return action() - } finally { - mangaMutex.unlock(id) - } - } } } diff --git a/app/src/main/res/menu/popup_source.xml b/app/src/main/res/menu/popup_source.xml index 54eded2ed..ecdeaee5e 100644 --- a/app/src/main/res/menu/popup_source.xml +++ b/app/src/main/res/menu/popup_source.xml @@ -11,7 +11,7 @@ android:title="@string/create_shortcut" /> + android:id="@+id/action_disable" + android:title="@string/disable" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a275ab7a1..aa3e2ae8c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -641,4 +641,5 @@ Suggested queries Authors You are blocked by the server. Try to use a different network connection (VPN, Proxy, etc.) + Disable diff --git a/app/src/main/res/xml/pref_network.xml b/app/src/main/res/xml/pref_network.xml index 90e0ddced..af781034b 100644 --- a/app/src/main/res/xml/pref_network.xml +++ b/app/src/main/res/xml/pref_network.xml @@ -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" />