Code cleanup and refactor
This commit is contained in:
@@ -12,9 +12,6 @@ import org.koitharu.kotatsu.core.db.entity.MangaWithTags
|
|||||||
@Dao
|
@Dao
|
||||||
abstract class BookmarksDao {
|
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")
|
@Query("SELECT * FROM bookmarks WHERE page_id = :pageId")
|
||||||
abstract suspend fun find(pageId: Long): BookmarkEntity?
|
abstract suspend fun find(pageId: Long): BookmarkEntity?
|
||||||
|
|
||||||
@@ -42,9 +39,6 @@ abstract class BookmarksDao {
|
|||||||
@Delete
|
@Delete
|
||||||
abstract suspend fun delete(entity: BookmarkEntity)
|
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")
|
@Query("DELETE FROM bookmarks WHERE page_id = :pageId")
|
||||||
abstract suspend fun delete(pageId: Long): Int
|
abstract suspend fun delete(pageId: Long): Int
|
||||||
|
|
||||||
|
|||||||
@@ -26,9 +26,6 @@ import kotlinx.coroutines.flow.asSharedFlow
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
|
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.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
|
import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
|
||||||
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||||
@@ -159,18 +156,6 @@ interface AppModule {
|
|||||||
acraScreenLogger,
|
acraScreenLogger,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideContentCache(
|
|
||||||
application: Application,
|
|
||||||
): ContentCache {
|
|
||||||
return if (application.isLowRamDevice()) {
|
|
||||||
StubContentCache()
|
|
||||||
} else {
|
|
||||||
MemoryContentCache(application)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@LocalStorageChanges
|
@LocalStorageChanges
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -2,18 +2,19 @@ package org.koitharu.kotatsu.core.cache
|
|||||||
|
|
||||||
import androidx.collection.LruCache
|
import androidx.collection.LruCache
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import org.koitharu.kotatsu.core.cache.MemoryContentCache.Key as CacheKey
|
||||||
|
|
||||||
class ExpiringLruCache<T>(
|
class ExpiringLruCache<T>(
|
||||||
val maxSize: Int,
|
val maxSize: Int,
|
||||||
private val lifetime: Long,
|
private val lifetime: Long,
|
||||||
private val timeUnit: TimeUnit,
|
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
|
val value = cache[key] ?: return null
|
||||||
if (value.isExpired) {
|
if (value.isExpired) {
|
||||||
cache.remove(key)
|
cache.remove(key)
|
||||||
@@ -21,7 +22,7 @@ class ExpiringLruCache<T>(
|
|||||||
return value.get()
|
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))
|
cache.put(key, ExpiringValue(value, lifetime, timeUnit))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ class ExpiringLruCache<T>(
|
|||||||
cache.trimToSize(size)
|
cache.trimToSize(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove(key: ContentCache.Key) {
|
fun remove(key: CacheKey) {
|
||||||
cache.remove(key)
|
cache.remove(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,48 +3,54 @@ package org.koitharu.kotatsu.core.cache
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.ComponentCallbacks2
|
import android.content.ComponentCallbacks2
|
||||||
import android.content.res.Configuration
|
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.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import java.util.concurrent.TimeUnit
|
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 {
|
init {
|
||||||
application.registerComponentCallbacks(this)
|
application.registerComponentCallbacks(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(4, 5, TimeUnit.MINUTES)
|
private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(if (isLowRam) 1 else 4, 5, TimeUnit.MINUTES)
|
||||||
private val pagesCache = ExpiringLruCache<SafeDeferred<List<MangaPage>>>(4, 10, TimeUnit.MINUTES)
|
private val pagesCache =
|
||||||
private val relatedMangaCache = ExpiringLruCache<SafeDeferred<List<Manga>>>(4, 10, TimeUnit.MINUTES)
|
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
|
suspend fun getDetails(source: MangaSource, url: String): Manga? {
|
||||||
|
return detailsCache[Key(source, url)]?.awaitOrNull()
|
||||||
override suspend fun getDetails(source: MangaSource, url: String): Manga? {
|
|
||||||
return detailsCache[ContentCache.Key(source, url)]?.awaitOrNull()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) {
|
fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) {
|
||||||
detailsCache[ContentCache.Key(source, url)] = details
|
detailsCache[Key(source, url)] = details
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? {
|
suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? {
|
||||||
return pagesCache[ContentCache.Key(source, url)]?.awaitOrNull()
|
return pagesCache[Key(source, url)]?.awaitOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) {
|
fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) {
|
||||||
pagesCache[ContentCache.Key(source, url)] = pages
|
pagesCache[Key(source, url)] = pages
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>? {
|
suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>? {
|
||||||
return relatedMangaCache[ContentCache.Key(source, url)]?.awaitOrNull()
|
return relatedMangaCache[Key(source, url)]?.awaitOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>) {
|
fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>) {
|
||||||
relatedMangaCache[ContentCache.Key(source, url)] = related
|
relatedMangaCache[Key(source, url)] = related
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clear(source: MangaSource) {
|
fun clear(source: MangaSource) {
|
||||||
clearCache(detailsCache, source)
|
clearCache(detailsCache, source)
|
||||||
clearCache(pagesCache, source)
|
clearCache(pagesCache, source)
|
||||||
clearCache(relatedMangaCache, source)
|
clearCache(relatedMangaCache, source)
|
||||||
@@ -81,4 +87,9 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class Key(
|
||||||
|
val source: MangaSource,
|
||||||
|
val url: String,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -40,7 +40,7 @@ abstract class MangaDao {
|
|||||||
abstract suspend fun searchByTitle(query: String, source: String, limit: Int): List<MangaWithTags>
|
abstract suspend fun searchByTitle(query: String, source: String, limit: Int): List<MangaWithTags>
|
||||||
|
|
||||||
@Upsert
|
@Upsert
|
||||||
abstract suspend fun upsert(manga: MangaEntity)
|
protected abstract suspend fun upsert(manga: MangaEntity)
|
||||||
|
|
||||||
@Update(onConflict = OnConflictStrategy.IGNORE)
|
@Update(onConflict = OnConflictStrategy.IGNORE)
|
||||||
abstract suspend fun update(manga: MangaEntity): Int
|
abstract suspend fun update(manga: MangaEntity): Int
|
||||||
|
|||||||
@@ -23,9 +23,6 @@ abstract class MangaSourcesDao {
|
|||||||
@Query("SELECT * FROM sources WHERE enabled = 0 ORDER BY sort_key")
|
@Query("SELECT * FROM sources WHERE enabled = 0 ORDER BY sort_key")
|
||||||
abstract suspend fun findAllDisabled(): List<MangaSourceEntity>
|
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")
|
@Query("SELECT * FROM sources ORDER BY sort_key")
|
||||||
abstract fun observeAll(): Flow<List<MangaSourceEntity>>
|
abstract fun observeAll(): Flow<List<MangaSourceEntity>>
|
||||||
|
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ interface TrackLogsDao {
|
|||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insert(entity: TrackLogEntity): Long
|
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)")
|
@Query("DELETE FROM track_logs WHERE manga_id NOT IN (SELECT manga_id FROM tracks)")
|
||||||
suspend fun gc()
|
suspend fun gc()
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package org.koitharu.kotatsu.core.parser
|
package org.koitharu.kotatsu.core.parser
|
||||||
|
|
||||||
import androidx.annotation.AnyThread
|
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.core.network.MirrorSwitchInterceptor
|
||||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
@@ -57,7 +57,7 @@ interface MangaRepository {
|
|||||||
class Factory @Inject constructor(
|
class Factory @Inject constructor(
|
||||||
private val localMangaRepository: LocalMangaRepository,
|
private val localMangaRepository: LocalMangaRepository,
|
||||||
private val loaderContext: MangaLoaderContext,
|
private val loaderContext: MangaLoaderContext,
|
||||||
private val contentCache: ContentCache,
|
private val contentCache: MemoryContentCache,
|
||||||
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
|
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ import okhttp3.Headers
|
|||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
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.cache.SafeDeferred
|
||||||
import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor
|
import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor
|
||||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
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.core.util.ext.processLifecycleScope
|
||||||
import org.koitharu.kotatsu.parsers.MangaParser
|
import org.koitharu.kotatsu.parsers.MangaParser
|
||||||
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
|
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
|
||||||
@@ -37,10 +38,14 @@ import java.util.Locale
|
|||||||
|
|
||||||
class RemoteMangaRepository(
|
class RemoteMangaRepository(
|
||||||
private val parser: MangaParser,
|
private val parser: MangaParser,
|
||||||
private val cache: ContentCache,
|
private val cache: MemoryContentCache, // TODO fix concurrency
|
||||||
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
|
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
|
||||||
) : MangaRepository, Interceptor {
|
) : MangaRepository, Interceptor {
|
||||||
|
|
||||||
|
private val detailsMutex = MultiMutex<Long>()
|
||||||
|
private val relatedMangaMutex = MultiMutex<Long>()
|
||||||
|
private val pagesMutex = MultiMutex<Long>()
|
||||||
|
|
||||||
override val source: MangaSource
|
override val source: MangaSource
|
||||||
get() = parser.source
|
get() = parser.source
|
||||||
|
|
||||||
@@ -96,7 +101,7 @@ class RemoteMangaRepository(
|
|||||||
|
|
||||||
override suspend fun getDetails(manga: Manga): Manga = getDetails(manga, CachePolicy.ENABLED)
|
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 }
|
cache.getPages(source, chapter.url)?.let { return it }
|
||||||
val pages = asyncSafe {
|
val pages = asyncSafe {
|
||||||
mirrorSwitchInterceptor.withMirrorSwitching {
|
mirrorSwitchInterceptor.withMirrorSwitching {
|
||||||
@@ -104,8 +109,8 @@ class RemoteMangaRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
cache.putPages(source, chapter.url, pages)
|
cache.putPages(source, chapter.url, pages)
|
||||||
return pages.await()
|
pages
|
||||||
}
|
}.await()
|
||||||
|
|
||||||
override suspend fun getPageUrl(page: MangaPage): String = mirrorSwitchInterceptor.withMirrorSwitching {
|
override suspend fun getPageUrl(page: MangaPage): String = mirrorSwitchInterceptor.withMirrorSwitching {
|
||||||
parser.getPageUrl(page)
|
parser.getPageUrl(page)
|
||||||
@@ -123,16 +128,16 @@ class RemoteMangaRepository(
|
|||||||
parser.getFavicons()
|
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 }
|
cache.getRelatedManga(source, seed.url)?.let { return it }
|
||||||
val related = asyncSafe {
|
val related = asyncSafe {
|
||||||
parser.getRelatedManga(seed).filterNot { it.id == seed.id }
|
parser.getRelatedManga(seed).filterNot { it.id == seed.id }
|
||||||
}
|
}
|
||||||
cache.putRelatedManga(source, seed.url, related)
|
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) {
|
if (cachePolicy.readEnabled) {
|
||||||
cache.getDetails(source, manga.url)?.let { return it }
|
cache.getDetails(source, manga.url)?.let { return it }
|
||||||
}
|
}
|
||||||
@@ -144,8 +149,8 @@ class RemoteMangaRepository(
|
|||||||
if (cachePolicy.writeEnabled) {
|
if (cachePolicy.writeEnabled) {
|
||||||
cache.putDetails(source, manga.url, details)
|
cache.putDetails(source, manga.url, details)
|
||||||
}
|
}
|
||||||
return details.await()
|
details
|
||||||
}
|
}.await()
|
||||||
|
|
||||||
suspend fun peekDetails(manga: Manga): Manga? {
|
suspend fun peekDetails(manga: Manga): Manga? {
|
||||||
return cache.getDetails(source, manga.url)
|
return cache.getDetails(source, manga.url)
|
||||||
|
|||||||
@@ -3,33 +3,27 @@ package org.koitharu.kotatsu.core.ui
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.appcompat.widget.ActionBarContextView
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.app.ActivityCompat
|
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.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import dagger.hilt.EntryPoint
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.EntryPointAccessors
|
import dagger.hilt.android.EntryPointAccessors
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
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.ActionModeDelegate
|
||||||
import org.koitharu.kotatsu.core.ui.util.BaseActivityEntryPoint
|
|
||||||
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
|
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
|
||||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.isWebViewUnavailable
|
import org.koitharu.kotatsu.core.util.ext.isWebViewUnavailable
|
||||||
|
|
||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis")
|
||||||
@@ -127,32 +121,13 @@ abstract class BaseActivity<B : ViewBinding> :
|
|||||||
@CallSuper
|
@CallSuper
|
||||||
override fun onSupportActionModeStarted(mode: ActionMode) {
|
override fun onSupportActionModeStarted(mode: ActionMode) {
|
||||||
super.onSupportActionModeStarted(mode)
|
super.onSupportActionModeStarted(mode)
|
||||||
actionModeDelegate.onSupportActionModeStarted(mode)
|
actionModeDelegate.onSupportActionModeStarted(mode, window)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
override fun onSupportActionModeFinished(mode: ActionMode) {
|
override fun onSupportActionModeFinished(mode: ActionMode) {
|
||||||
super.onSupportActionModeFinished(mode)
|
super.onSupportActionModeFinished(mode)
|
||||||
actionModeDelegate.onSupportActionModeFinished(mode)
|
actionModeDelegate.onSupportActionModeFinished(mode, window)
|
||||||
window.statusBarColor = defaultStatusBarColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun dispatchNavigateUp() {
|
protected open fun dispatchNavigateUp() {
|
||||||
@@ -185,6 +160,12 @@ abstract class BaseActivity<B : ViewBinding> :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EntryPoint
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
interface BaseActivityEntryPoint {
|
||||||
|
val settings: AppSettings
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val EXTRA_DATA = "data"
|
const val EXTRA_DATA = "data"
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package org.koitharu.kotatsu.core.ui.sheet
|
|||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -16,15 +14,8 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.appcompat.app.AppCompatDialog
|
import androidx.appcompat.app.AppCompatDialog
|
||||||
import androidx.appcompat.app.AppCompatDialogFragment
|
import androidx.appcompat.app.AppCompatDialogFragment
|
||||||
import androidx.appcompat.view.ActionMode
|
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.core.view.updateLayoutParams
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.LifecycleObserver
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
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.R
|
||||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||||
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
|
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
|
||||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
||||||
|
|
||||||
private var waitingForDismissAllowingStateLoss = false
|
private var waitingForDismissAllowingStateLoss = false
|
||||||
private var isFitToContentsDisabled = false
|
private var isFitToContentsDisabled = false
|
||||||
private var defaultStatusBarColor = Color.TRANSPARENT
|
|
||||||
|
|
||||||
var viewBinding: B? = null
|
var viewBinding: B? = null
|
||||||
private set
|
private set
|
||||||
@@ -105,40 +94,18 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
|||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
protected open fun dispatchSupportActionModeStarted(mode: ActionMode) {
|
protected open fun dispatchSupportActionModeStarted(mode: ActionMode) {
|
||||||
actionModeDelegate?.onSupportActionModeStarted(mode)
|
actionModeDelegate?.onSupportActionModeStarted(mode, dialog?.window)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
protected open fun dispatchSupportActionModeFinished(mode: ActionMode) {
|
protected open fun dispatchSupportActionModeFinished(mode: ActionMode) {
|
||||||
actionModeDelegate?.onSupportActionModeFinished(mode)
|
actionModeDelegate?.onSupportActionModeFinished(mode, dialog?.window)
|
||||||
dialog?.window?.statusBarColor = defaultStatusBarColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addSheetCallback(callback: AdaptiveSheetCallback, lifecycleOwner: LifecycleOwner): Boolean {
|
fun addSheetCallback(callback: AdaptiveSheetCallback, lifecycleOwner: LifecycleOwner): Boolean {
|
||||||
val b = behavior ?: return false
|
val b = behavior ?: return false
|
||||||
b.addCallback(callback)
|
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)
|
?: dialog?.findViewById(materialR.id.coordinator)
|
||||||
?: view
|
?: view
|
||||||
if (rootView != null) {
|
if (rootView != null) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.core.ui.util
|
package org.koitharu.kotatsu.core.ui.sheet
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.OnBackPressedCallback
|
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_COLLAPSED
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
|
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HALF_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<*>,
|
private val behavior: BottomSheetBehavior<*>,
|
||||||
) : OnBackPressedCallback(behavior.state == STATE_EXPANDED) {
|
) : OnBackPressedCallback(behavior.state == STATE_EXPANDED) {
|
||||||
|
|
||||||
@@ -1,14 +1,28 @@
|
|||||||
package org.koitharu.kotatsu.core.ui.util
|
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.activity.OnBackPressedCallback
|
||||||
import androidx.appcompat.view.ActionMode
|
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.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
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) {
|
class ActionModeDelegate : OnBackPressedCallback(false) {
|
||||||
|
|
||||||
private var activeActionMode: ActionMode? = null
|
private var activeActionMode: ActionMode? = null
|
||||||
private var listeners: MutableList<ActionModeListener>? = null
|
private var listeners: MutableList<ActionModeListener>? = null
|
||||||
|
private var defaultStatusBarColor = Color.TRANSPARENT
|
||||||
|
|
||||||
val isActionModeStarted: Boolean
|
val isActionModeStarted: Boolean
|
||||||
get() = activeActionMode != null
|
get() = activeActionMode != null
|
||||||
@@ -17,16 +31,40 @@ class ActionModeDelegate : OnBackPressedCallback(false) {
|
|||||||
finishActionMode()
|
finishActionMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSupportActionModeStarted(mode: ActionMode) {
|
fun onSupportActionModeStarted(mode: ActionMode, window: Window?) {
|
||||||
activeActionMode = mode
|
activeActionMode = mode
|
||||||
isEnabled = true
|
isEnabled = true
|
||||||
listeners?.forEach { it.onActionModeStarted(mode) }
|
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
|
activeActionMode = null
|
||||||
isEnabled = false
|
isEnabled = false
|
||||||
listeners?.forEach { it.onActionModeFinished(mode) }
|
listeners?.forEach { it.onActionModeFinished(mode) }
|
||||||
|
if (window != null) {
|
||||||
|
window.statusBarColor = defaultStatusBarColor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addListener(listener: ActionModeListener) {
|
fun addListener(listener: ActionModeListener) {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,8 @@ package org.koitharu.kotatsu.core.util
|
|||||||
|
|
||||||
import androidx.collection.ArrayMap
|
import androidx.collection.ArrayMap
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlin.contracts.InvocationKind
|
||||||
|
import kotlin.contracts.contract
|
||||||
|
|
||||||
class MultiMutex<T : Any> : Set<T> {
|
class MultiMutex<T : Any> : Set<T> {
|
||||||
|
|
||||||
@@ -10,12 +12,12 @@ class MultiMutex<T : Any> : Set<T> {
|
|||||||
override val size: Int
|
override val size: Int
|
||||||
get() = delegates.size
|
get() = delegates.size
|
||||||
|
|
||||||
override fun contains(element: T): Boolean {
|
override fun contains(element: T): Boolean = synchronized(delegates) {
|
||||||
return delegates.containsKey(element)
|
delegates.containsKey(element)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun containsAll(elements: Collection<T>): Boolean {
|
override fun containsAll(elements: Collection<T>): Boolean = synchronized(delegates) {
|
||||||
return elements.all { x -> delegates.containsKey(x) }
|
elements.all { x -> delegates.containsKey(x) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isEmpty(): Boolean {
|
override fun isEmpty(): Boolean {
|
||||||
@@ -40,4 +42,16 @@ class MultiMutex<T : Any> : Set<T> {
|
|||||||
delegates.remove(element)?.unlock()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dagger.hilt.android.EntryPointAccessors
|
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.findById
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableChapter
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableChapter
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||||
@@ -27,7 +27,7 @@ class MangaPrefetchService : CoroutineIntentService() {
|
|||||||
lateinit var mangaRepositoryFactory: MangaRepository.Factory
|
lateinit var mangaRepositoryFactory: MangaRepository.Factory
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var cache: ContentCache
|
lateinit var cache: MemoryContentCache
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var historyRepository: HistoryRepository
|
lateinit var historyRepository: HistoryRepository
|
||||||
@@ -120,7 +120,7 @@ class MangaPrefetchService : CoroutineIntentService() {
|
|||||||
context,
|
context,
|
||||||
PrefetchCompanionEntryPoint::class.java,
|
PrefetchCompanionEntryPoint::class.java,
|
||||||
)
|
)
|
||||||
return entryPoint.contentCache.isCachingEnabled && entryPoint.settings.isContentPrefetchEnabled
|
return entryPoint.settings.isContentPrefetchEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tryStart(context: Context, intent: Intent) {
|
private fun tryStart(context: Context, intent: Intent) {
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ package org.koitharu.kotatsu.details.service
|
|||||||
import dagger.hilt.EntryPoint
|
import dagger.hilt.EntryPoint
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
|
||||||
@EntryPoint
|
@EntryPoint
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface PrefetchCompanionEntryPoint {
|
interface PrefetchCompanionEntryPoint {
|
||||||
val settings: AppSettings
|
val settings: AppSettings
|
||||||
val contentCache: ContentCache
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.ChipIconTarget
|
||||||
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
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.MenuInvalidator
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||||
@@ -154,7 +154,7 @@ class DetailsActivity :
|
|||||||
viewBinding.chipsTags.onChipClickListener = this
|
viewBinding.chipsTags.onChipClickListener = this
|
||||||
TitleScrollCoordinator(viewBinding.textViewTitle).attach(viewBinding.scrollView)
|
TitleScrollCoordinator(viewBinding.textViewTitle).attach(viewBinding.scrollView)
|
||||||
viewBinding.containerBottomSheet?.let { BottomSheetBehavior.from(it) }?.let { behavior ->
|
viewBinding.containerBottomSheet?.let { BottomSheetBehavior.from(it) }?.let { behavior ->
|
||||||
onBackPressedDispatcher.addCallback(BottomSheetClollapseCallback(behavior))
|
onBackPressedDispatcher.addCallback(BottomSheetCollapseCallback(behavior))
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.details.filterNotNull().observe(this, ::onMangaUpdated)
|
viewModel.details.filterNotNull().observe(this, ::onMangaUpdated)
|
||||||
|
|||||||
@@ -205,8 +205,8 @@ class ExploreFragment :
|
|||||||
startActivity(SettingsActivity.newSourceSettingsIntent(requireContext(), sourceItem.source))
|
startActivity(SettingsActivity.newSourceSettingsIntent(requireContext(), sourceItem.source))
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_hide -> {
|
R.id.action_disable -> {
|
||||||
viewModel.hideSource(sourceItem.source)
|
viewModel.disableSource(sourceItem.source)
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_shortcut -> {
|
R.id.action_shortcut -> {
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class ExploreViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hideSource(source: MangaSource) {
|
fun disableSource(source: MangaSource) {
|
||||||
launchJob(Dispatchers.Default) {
|
launchJob(Dispatchers.Default) {
|
||||||
val rollback = sourcesRepository.setSourceEnabled(source, isEnabled = false)
|
val rollback = sourcesRepository.setSourceEnabled(source, isEnabled = false)
|
||||||
onActionDone.call(ReversibleAction(R.string.source_disabled, rollback))
|
onActionDone.call(ReversibleAction(R.string.source_disabled, rollback))
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ abstract class FavouriteCategoriesDao {
|
|||||||
abstract fun observeAll(): Flow<List<FavouriteCategoryEntity>>
|
abstract fun observeAll(): Flow<List<FavouriteCategoryEntity>>
|
||||||
|
|
||||||
@Query("SELECT * FROM favourite_categories WHERE deleted_at = 0 AND show_in_lib = 1 ORDER BY sort_key")
|
@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")
|
@Query("SELECT * FROM favourite_categories WHERE category_id = :id AND deleted_at = 0")
|
||||||
abstract fun observe(id: Long): Flow<FavouriteCategoryEntity?>
|
abstract fun observe(id: Long): Flow<FavouriteCategoryEntity?>
|
||||||
@@ -40,7 +40,7 @@ abstract class FavouriteCategoriesDao {
|
|||||||
abstract suspend fun updateTracking(id: Long, isEnabled: Boolean)
|
abstract suspend fun updateTracking(id: Long, isEnabled: Boolean)
|
||||||
|
|
||||||
@Query("UPDATE favourite_categories SET `show_in_lib` = :isEnabled WHERE category_id = :id")
|
@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")
|
@Query("UPDATE favourite_categories SET sort_key = :sortKey WHERE category_id = :id")
|
||||||
abstract suspend fun updateSortKey(id: Long, sortKey: Int)
|
abstract suspend fun updateSortKey(id: Long, sortKey: Int)
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import androidx.sqlite.db.SimpleSQLiteQuery
|
|||||||
import androidx.sqlite.db.SupportSQLiteQuery
|
import androidx.sqlite.db.SupportSQLiteQuery
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import org.intellij.lang.annotations.Language
|
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.favourites.domain.model.Cover
|
||||||
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
||||||
|
|
||||||
@@ -39,13 +38,6 @@ abstract class FavouritesDao {
|
|||||||
return observeAllImpl(query)
|
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
|
@Transaction
|
||||||
@Query("SELECT * FROM favourites WHERE deleted_at = 0 ORDER BY created_at DESC LIMIT :limit OFFSET :offset")
|
@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>
|
abstract suspend fun findAllRaw(offset: Int, limit: Int): List<FavouriteManga>
|
||||||
@@ -72,19 +64,6 @@ abstract class FavouritesDao {
|
|||||||
return observeAllImpl(query)
|
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> {
|
suspend fun findCovers(categoryId: Long, order: ListSortOrder): List<Cover> {
|
||||||
val orderBy = getOrderBy(order)
|
val orderBy = getOrderBy(order)
|
||||||
|
|
||||||
@@ -114,21 +93,9 @@ abstract class FavouritesDao {
|
|||||||
@Query("SELECT COUNT(DISTINCT manga_id) FROM favourites WHERE deleted_at = 0")
|
@Query("SELECT COUNT(DISTINCT manga_id) FROM favourites WHERE deleted_at = 0")
|
||||||
abstract fun observeMangaCount(): Flow<Int>
|
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")
|
@Query("SELECT * FROM favourites WHERE manga_id = :mangaId AND deleted_at = 0")
|
||||||
abstract suspend fun findAllRaw(mangaId: Long): List<FavouriteEntity>
|
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")
|
@Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id = :id AND deleted_at = 0")
|
||||||
abstract fun observeIds(id: Long): Flow<List<Long>>
|
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")
|
@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>
|
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 **/
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
@@ -194,7 +158,7 @@ abstract class FavouritesDao {
|
|||||||
protected abstract suspend fun setDeletedAt(mangaId: Long, deletedAt: Long)
|
protected abstract suspend fun setDeletedAt(mangaId: Long, deletedAt: Long)
|
||||||
|
|
||||||
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE manga_id = :mangaId AND category_id = :categoryId")
|
@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")
|
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE category_id = :categoryId AND deleted_at = 0")
|
||||||
protected abstract suspend fun setDeletedAtAll(categoryId: Long, deletedAt: Long)
|
protected abstract suspend fun setDeletedAtAll(categoryId: Long, deletedAt: Long)
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class FavouritesRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun observeCategoriesForLibrary(): Flow<List<FavouriteCategory>> {
|
fun observeCategoriesForLibrary(): Flow<List<FavouriteCategory>> {
|
||||||
return db.getFavouriteCategoriesDao().observeAllForLibrary().mapItems {
|
return db.getFavouriteCategoriesDao().observeAllVisible().mapItems {
|
||||||
it.toFavouriteCategory()
|
it.toFavouriteCategory()
|
||||||
}.distinctUntilChanged()
|
}.distinctUntilChanged()
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ class FavouritesRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateCategory(id: Long, isVisibleInLibrary: Boolean) {
|
suspend fun updateCategory(id: Long, isVisibleInLibrary: Boolean) {
|
||||||
db.getFavouriteCategoriesDao().updateLibVisibility(id, isVisibleInLibrary)
|
db.getFavouriteCategoriesDao().updateVisibility(id, isVisibleInLibrary)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateCategoryTracking(id: Long, isTrackingEnabled: Boolean) {
|
suspend fun updateCategoryTracking(id: Long, isTrackingEnabled: Boolean) {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import androidx.sqlite.db.SimpleSQLiteQuery
|
|||||||
import androidx.sqlite.db.SupportSQLiteQuery
|
import androidx.sqlite.db.SupportSQLiteQuery
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import org.intellij.lang.annotations.Language
|
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.core.db.entity.TagEntity
|
||||||
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
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")
|
@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>
|
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
|
@Transaction
|
||||||
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC")
|
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC")
|
||||||
abstract fun observeAll(): Flow<List<HistoryWithManga>>
|
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")
|
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC LIMIT :limit")
|
||||||
abstract fun observeAll(limit: Int): Flow<List<HistoryWithManga>>
|
abstract fun observeAll(limit: Int): Flow<List<HistoryWithManga>>
|
||||||
|
|
||||||
|
// TODO pagination
|
||||||
fun observeAll(order: ListSortOrder): Flow<List<HistoryWithManga>> {
|
fun observeAll(order: ListSortOrder): Flow<List<HistoryWithManga>> {
|
||||||
val orderBy = when (order) {
|
val orderBy = when (order) {
|
||||||
ListSortOrder.LAST_READ -> "history.updated_at DESC"
|
ListSortOrder.LAST_READ -> "history.updated_at DESC"
|
||||||
@@ -56,9 +52,6 @@ abstract class HistoryDao {
|
|||||||
return observeAllImpl(query)
|
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")
|
@Query("SELECT manga_id FROM history WHERE deleted_at = 0")
|
||||||
abstract suspend fun findAllIds(): LongArray
|
abstract suspend fun findAllIds(): LongArray
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import androidx.preference.Preference
|
|||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.koitharu.kotatsu.R
|
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.network.DoHProvider
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||||
@@ -23,11 +23,10 @@ class NetworkSettingsFragment :
|
|||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var contentCache: ContentCache
|
lateinit var contentCache: MemoryContentCache
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
addPreferencesFromResource(R.xml.pref_network)
|
addPreferencesFromResource(R.xml.pref_network)
|
||||||
findPreference<Preference>(AppSettings.KEY_PREFETCH_CONTENT)?.isVisible = contentCache.isCachingEnabled
|
|
||||||
findPreference<ListPreference>(AppSettings.KEY_DOH)?.run {
|
findPreference<ListPreference>(AppSettings.KEY_DOH)?.run {
|
||||||
entryValues = DoHProvider.entries.names()
|
entryValues = DoHProvider.entries.names()
|
||||||
setDefaultValueCompat(DoHProvider.NONE.name)
|
setDefaultValueCompat(DoHProvider.NONE.name)
|
||||||
|
|||||||
@@ -1,25 +1,18 @@
|
|||||||
package org.koitharu.kotatsu.stats.data
|
package org.koitharu.kotatsu.stats.data
|
||||||
|
|
||||||
import android.database.sqlite.SQLiteQueryBuilder
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.MapColumn
|
import androidx.room.MapColumn
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.RawQuery
|
import androidx.room.RawQuery
|
||||||
import androidx.room.Transaction
|
|
||||||
import androidx.room.Upsert
|
import androidx.room.Upsert
|
||||||
import androidx.sqlite.db.SimpleSQLiteQuery
|
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||||
import androidx.sqlite.db.SupportSQLiteQuery
|
import androidx.sqlite.db.SupportSQLiteQuery
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||||
import org.koitharu.kotatsu.history.data.HistoryEntity
|
|
||||||
import org.koitharu.kotatsu.history.data.HistoryWithManga
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
abstract class StatsDao {
|
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")
|
@Query("SELECT * FROM stats WHERE manga_id = :mangaId ORDER BY started_at")
|
||||||
abstract suspend fun findAll(mangaId: Long): List<StatsEntity>
|
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")
|
@Query("SELECT IFNULL(SUM(duration)/SUM(pages), 0) FROM stats")
|
||||||
abstract suspend fun getAverageTimePerPage(): Long
|
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")
|
@Query("DELETE FROM stats")
|
||||||
abstract suspend fun clear()
|
abstract suspend fun clear()
|
||||||
|
|
||||||
@@ -47,7 +34,11 @@ abstract class StatsDao {
|
|||||||
@Upsert
|
@Upsert
|
||||||
abstract suspend fun upsert(entity: StatsEntity)
|
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>()
|
val conditions = ArrayList<String>()
|
||||||
conditions.add("stats.started_at >= $fromDate")
|
conditions.add("stats.started_at >= $fromDate")
|
||||||
if (favouriteCategories.isNotEmpty()) {
|
if (favouriteCategories.isNotEmpty()) {
|
||||||
@@ -66,7 +57,7 @@ abstract class StatsDao {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RawQuery
|
@RawQuery
|
||||||
protected abstract fun getDurationStatsImpl(
|
protected abstract suspend fun getDurationStatsImpl(
|
||||||
query: SupportSQLiteQuery
|
query: SupportSQLiteQuery
|
||||||
): Map<@MapColumn("manga") MangaEntity, @MapColumn("d") Long>
|
): Map<@MapColumn("manga") MangaEntity, @MapColumn("d") Long>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.tracker.data
|
package org.koitharu.kotatsu.tracker.data
|
||||||
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.MapColumn
|
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
import androidx.room.Upsert
|
import androidx.room.Upsert
|
||||||
@@ -10,9 +9,6 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
@Dao
|
@Dao
|
||||||
abstract class TracksDao {
|
abstract class TracksDao {
|
||||||
|
|
||||||
@Query("SELECT * FROM tracks")
|
|
||||||
abstract suspend fun findAll(): List<TrackEntity>
|
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM tracks ORDER BY last_check_time ASC LIMIT :limit OFFSET :offset")
|
@Query("SELECT * FROM tracks ORDER BY last_check_time ASC LIMIT :limit OFFSET :offset")
|
||||||
abstract suspend fun findAll(offset: Int, limit: Int): List<TrackWithManga>
|
abstract suspend fun findAll(offset: Int, limit: Int): List<TrackWithManga>
|
||||||
@@ -24,9 +20,6 @@ abstract class TracksDao {
|
|||||||
@Query("SELECT manga_id FROM tracks")
|
@Query("SELECT manga_id FROM tracks")
|
||||||
abstract suspend fun findAllIds(): LongArray
|
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")
|
@Query("SELECT * FROM tracks WHERE manga_id = :mangaId")
|
||||||
abstract suspend fun find(mangaId: Long): TrackEntity?
|
abstract suspend fun find(mangaId: Long): TrackEntity?
|
||||||
|
|
||||||
@@ -36,9 +29,6 @@ abstract class TracksDao {
|
|||||||
@Query("SELECT COUNT(*) FROM tracks")
|
@Query("SELECT COUNT(*) FROM tracks")
|
||||||
abstract suspend fun getTracksCount(): Int
|
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")
|
@Query("SELECT chapters_new FROM tracks")
|
||||||
abstract fun observeNewChapters(): Flow<List<Int>>
|
abstract fun observeNewChapters(): Flow<List<Int>>
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
|
|||||||
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
|
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.contracts.InvocationKind
|
|
||||||
import kotlin.contracts.contract
|
|
||||||
|
|
||||||
@Reusable
|
@Reusable
|
||||||
class Tracker @Inject constructor(
|
class Tracker @Inject constructor(
|
||||||
@@ -25,6 +23,8 @@ class Tracker @Inject constructor(
|
|||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val mangaMutex = MultiMutex<Long>()
|
||||||
|
|
||||||
suspend fun getTracks(limit: Int): List<MangaTracking> {
|
suspend fun getTracks(limit: Int): List<MangaTracking> {
|
||||||
repository.updateTracks()
|
repository.updateTracks()
|
||||||
return repository.getTracks(offset = 0, limit = limit)
|
return repository.getTracks(offset = 0, limit = limit)
|
||||||
@@ -37,7 +37,7 @@ class Tracker @Inject constructor(
|
|||||||
suspend fun fetchUpdates(
|
suspend fun fetchUpdates(
|
||||||
track: MangaTracking,
|
track: MangaTracking,
|
||||||
commit: Boolean
|
commit: Boolean
|
||||||
): MangaUpdates = withMangaLock(track.manga.id) {
|
): MangaUpdates = mangaMutex.withLock(track.manga.id) {
|
||||||
val updates = runCatchingCancellable {
|
val updates = runCatchingCancellable {
|
||||||
val repo = mangaRepositoryFactory.create(track.manga.source)
|
val repo = mangaRepositoryFactory.create(track.manga.source)
|
||||||
require(repo is RemoteMangaRepository) { "Repository ${repo.javaClass.simpleName} is not supported" }
|
require(repo is RemoteMangaRepository) { "Repository ${repo.javaClass.simpleName} is not supported" }
|
||||||
@@ -52,7 +52,7 @@ class Tracker @Inject constructor(
|
|||||||
if (commit) {
|
if (commit) {
|
||||||
repository.saveUpdates(updates)
|
repository.saveUpdates(updates)
|
||||||
}
|
}
|
||||||
return updates
|
updates
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun syncWithDetails(details: Manga) {
|
suspend fun syncWithDetails(details: Manga) {
|
||||||
@@ -94,7 +94,7 @@ class Tracker @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
suspend fun deleteTrack(mangaId: Long) = withMangaLock(mangaId) {
|
suspend fun deleteTrack(mangaId: Long) = mangaMutex.withLock(mangaId) {
|
||||||
repository.deleteTrack(mangaId)
|
repository.deleteTrack(mangaId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,18 +135,5 @@ class Tracker @Inject constructor(
|
|||||||
private companion object {
|
private companion object {
|
||||||
|
|
||||||
const val NO_ID = 0L
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
android:title="@string/create_shortcut" />
|
android:title="@string/create_shortcut" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_hide"
|
android:id="@+id/action_disable"
|
||||||
android:title="@string/hide" />
|
android:title="@string/disable" />
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
@@ -641,4 +641,5 @@
|
|||||||
<string name="suggested_queries">Suggested queries</string>
|
<string name="suggested_queries">Suggested queries</string>
|
||||||
<string name="authors">Authors</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="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>
|
</resources>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
android:entryValues="@array/values_network_policy"
|
android:entryValues="@array/values_network_policy"
|
||||||
android:key="prefetch_content"
|
android:key="prefetch_content"
|
||||||
android:title="@string/prefetch_content"
|
android:title="@string/prefetch_content"
|
||||||
app:isPreferenceVisible="false"
|
|
||||||
app:useSimpleSummaryProvider="true"
|
app:useSimpleSummaryProvider="true"
|
||||||
tools:isPreferenceVisible="true" />
|
tools:isPreferenceVisible="true" />
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user