Compare commits
153 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
34f6e5232b | 6 months ago |
|
|
f205c1b3dc | 6 months ago |
|
|
4b2a487c37 | 6 months ago |
|
|
726ac21974 | 6 months ago |
|
|
6b35216949 | 6 months ago |
|
|
22cae62f17 | 6 months ago |
|
|
4733caf2e6 | 6 months ago |
|
|
d49103de1f | 6 months ago |
|
|
414bab7ce3 | 6 months ago |
|
|
64c1873eb5 | 6 months ago |
|
|
06a0b5829b | 6 months ago |
|
|
0ce2870c8b | 6 months ago |
|
|
f59027666b | 6 months ago |
|
|
8513bc6daf | 6 months ago |
|
|
cceaefc896 | 6 months ago |
|
|
1d32f53bdd | 6 months ago |
|
|
0e98dd8695 | 6 months ago |
|
|
119b7c2ac7 | 6 months ago |
|
|
5701862661 | 6 months ago |
|
|
5590ab7c8a | 6 months ago |
|
|
9fde0106be | 6 months ago |
|
|
e73f077dc5 | 6 months ago |
|
|
c37458d43a | 6 months ago |
|
|
e2fcfcc7a8 | 6 months ago |
|
|
7a3b2a9bb4 | 6 months ago |
|
|
881f154b5e | 6 months ago |
|
|
34be5d16f2 | 6 months ago |
|
|
e7e554648d | 6 months ago |
|
|
89a4180b46 | 6 months ago |
|
|
4e2e190547 | 6 months ago |
|
|
3c557aae6c | 6 months ago |
|
|
0b00a3675d | 6 months ago |
|
|
8f20be6953 | 6 months ago |
|
|
26875c01c6 | 6 months ago |
|
|
4beb34c1a5 | 6 months ago |
|
|
1d50ab00c4 | 6 months ago |
|
|
299cd229ec | 6 months ago |
|
|
b02f394cd4 | 6 months ago |
|
|
7352f06564 | 6 months ago |
|
|
1e4861367e | 6 months ago |
|
|
bc3208946b | 6 months ago |
|
|
d5fbb00676 | 6 months ago |
|
|
7514362ca4 | 6 months ago |
|
|
e76a04bea0 | 6 months ago |
|
|
732a6e7c26 | 6 months ago |
|
|
f3111dc636 | 6 months ago |
|
|
e0e0cf4ecd | 6 months ago |
|
|
50f302a7f8 | 6 months ago |
|
|
500995a9d8 | 6 months ago |
|
|
beaf5cc0d5 | 6 months ago |
|
|
6377de470d | 6 months ago |
|
|
dec45f7851 | 6 months ago |
|
|
dbada34a43 | 6 months ago |
|
|
b62467964e | 6 months ago |
|
|
3249e10931 | 6 months ago |
|
|
0d5229b112 | 6 months ago |
|
|
d0ed1fb85f | 6 months ago |
|
|
9e5664da3a | 6 months ago |
|
|
35c158d35a | 6 months ago |
|
|
464f24e9f0 | 6 months ago |
|
|
c8a8203c39 | 6 months ago |
|
|
b414758f32 | 6 months ago |
|
|
1181860e41 | 6 months ago |
|
|
e35521f16f | 6 months ago |
|
|
5fb8ff53f9 | 6 months ago |
|
|
a66283d035 | 6 months ago |
|
|
a1ba0b8c21 | 6 months ago |
|
|
f3b42b9a42 | 6 months ago |
|
|
aa2f2c17fc | 6 months ago |
|
|
ebc17b645b | 6 months ago |
|
|
cc14e1abcf | 6 months ago |
|
|
b1b474e2e7 | 6 months ago |
|
|
8ca3bece5d | 6 months ago |
|
|
90bd9023d5 | 6 months ago |
|
|
986627f24d | 6 months ago |
|
|
cf2b8e2481 | 6 months ago |
|
|
b9435de5cd | 6 months ago |
|
|
861c21faea | 7 months ago |
|
|
9b4d014b21 | 7 months ago |
|
|
c6da7de699 | 7 months ago |
|
|
ef3aa40acc | 7 months ago |
|
|
07af3ea703 | 7 months ago |
|
|
391c8ab649 | 7 months ago |
|
|
6b1885c89d | 7 months ago |
|
|
8423b48fb9 | 7 months ago |
|
|
803c825d91 | 7 months ago |
|
|
6a9682a077 | 7 months ago |
|
|
9197b9cc3a | 7 months ago |
|
|
02ea804874 | 7 months ago |
|
|
c424466198 | 7 months ago |
|
|
18b312dde6 | 7 months ago |
|
|
f78262b1a0 | 7 months ago |
|
|
c557a51c4d | 7 months ago |
|
|
8995762935 | 7 months ago |
|
|
ed2664db78 | 7 months ago |
|
|
f5a5e53b5a | 7 months ago |
|
|
9ef961590d | 7 months ago |
|
|
9b569615ee | 7 months ago |
|
|
f48cf2efe4 | 7 months ago |
|
|
18094a310c | 7 months ago |
|
|
320c49a831 | 7 months ago |
|
|
2a971d5dae | 7 months ago |
|
|
4467e79ae6 | 7 months ago |
|
|
c68b180bf6 | 7 months ago |
|
|
5f879f6c83 | 7 months ago |
|
|
aeb3732d75 | 7 months ago |
|
|
6292a0fd6b | 7 months ago |
|
|
8985b4135d | 7 months ago |
|
|
f8a5397542 | 7 months ago |
|
|
5f51041220 | 7 months ago |
|
|
5a14412b62 | 7 months ago |
|
|
be012f631a | 7 months ago |
|
|
0165f43603 | 7 months ago |
|
|
55801a1488 | 7 months ago |
|
|
77103f016f | 7 months ago |
|
|
6b6719a259 | 7 months ago |
|
|
822642abb0 | 7 months ago |
|
|
260745fb95 | 7 months ago |
|
|
024ec0388f | 7 months ago |
|
|
5345998eec | 7 months ago |
|
|
3d56190e71 | 7 months ago |
|
|
954431d0a5 | 7 months ago |
|
|
afec63b443 | 7 months ago |
|
|
ac5b29c35a | 7 months ago |
|
|
59f5578b66 | 7 months ago |
|
|
391dbb4237 | 7 months ago |
|
|
7d4505eb78 | 7 months ago |
|
|
e6ceb20cf7 | 7 months ago |
|
|
8004f8c093 | 7 months ago |
|
|
61bf2abb6c | 7 months ago |
|
|
d9612f3427 | 7 months ago |
|
|
435c3824f7 | 7 months ago |
|
|
c846693570 | 7 months ago |
|
|
123937cd01 | 7 months ago |
|
|
9f56554313 | 7 months ago |
|
|
f8687bb697 | 7 months ago |
|
|
43d3a2cc6a | 7 months ago |
|
|
a95db6ed21 | 8 months ago |
|
|
fd0bb57338 | 8 months ago |
|
|
6b94bc2632 | 8 months ago |
|
|
c8b91599c6 | 8 months ago |
|
|
3a8b0f9e93 | 8 months ago |
|
|
17a0725666 | 8 months ago |
|
|
3be7848ad9 | 8 months ago |
|
|
08202c11a3 | 8 months ago |
|
|
5ef907d046 | 8 months ago |
|
|
c3776ea3c6 | 8 months ago |
|
|
a624bffea3 | 8 months ago |
|
|
8f38b4fe30 | 8 months ago |
|
|
71a2de5358 | 8 months ago |
|
|
5478f8fb59 | 8 months ago |
|
|
5155c9a33d | 8 months ago |
|
|
1d1e49123a | 8 months ago |
@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AppInsightsSettings">
|
||||||
|
<option name="tabSettings">
|
||||||
|
<map>
|
||||||
|
<entry key="Firebase Crashlytics">
|
||||||
|
<value>
|
||||||
|
<InsightsFilterSettings>
|
||||||
|
<option name="connection">
|
||||||
|
<ConnectionSetting>
|
||||||
|
<option name="appId" value="PLACEHOLDER" />
|
||||||
|
<option name="mobileSdkAppId" value="" />
|
||||||
|
<option name="projectId" value="" />
|
||||||
|
<option name="projectNumber" value="" />
|
||||||
|
</ConnectionSetting>
|
||||||
|
</option>
|
||||||
|
<option name="signal" value="SIGNAL_UNSPECIFIED" />
|
||||||
|
<option name="timeIntervalDays" value="THIRTY_DAYS" />
|
||||||
|
<option name="visibilityType" value="ALL" />
|
||||||
|
</InsightsFilterSettings>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
package org.koitharu.kotatsu.core.parser
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||||
|
import org.koitharu.kotatsu.core.model.TestMangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
import java.util.EnumSet
|
||||||
|
|
||||||
|
/*
|
||||||
|
This class is for parser development and testing purposes
|
||||||
|
You can open it in the app via Settings -> Debug
|
||||||
|
*/
|
||||||
|
class TestMangaRepository(
|
||||||
|
@Suppress("unused") private val loaderContext: MangaLoaderContext,
|
||||||
|
cache: MemoryContentCache
|
||||||
|
) : CachingMangaRepository(cache) {
|
||||||
|
|
||||||
|
override val source = TestMangaSource
|
||||||
|
|
||||||
|
override val sortOrders: Set<SortOrder> = EnumSet.allOf(SortOrder::class.java)
|
||||||
|
|
||||||
|
override var defaultSortOrder: SortOrder
|
||||||
|
get() = sortOrders.first()
|
||||||
|
set(value) = Unit
|
||||||
|
|
||||||
|
override val filterCapabilities = MangaListFilterCapabilities()
|
||||||
|
|
||||||
|
override suspend fun getFilterOptions() = MangaListFilterOptions()
|
||||||
|
|
||||||
|
override suspend fun getList(
|
||||||
|
offset: Int,
|
||||||
|
order: SortOrder?,
|
||||||
|
filter: MangaListFilter?
|
||||||
|
): List<Manga> = TODO("Get manga list by filter")
|
||||||
|
|
||||||
|
override suspend fun getDetailsImpl(
|
||||||
|
manga: Manga
|
||||||
|
): Manga = TODO("Fetch manga details")
|
||||||
|
|
||||||
|
override suspend fun getPagesImpl(
|
||||||
|
chapter: MangaChapter
|
||||||
|
): List<MangaPage> = TODO("Get pages for specific chapter")
|
||||||
|
|
||||||
|
override suspend fun getPageUrl(
|
||||||
|
page: MangaPage
|
||||||
|
): String = TODO("Return direct url of page image or page.url if it is already a direct url")
|
||||||
|
|
||||||
|
override suspend fun getRelatedMangaImpl(
|
||||||
|
seed: Manga
|
||||||
|
): List<Manga> = TODO("Get list of related manga. This method is optional and parser library has a default implementation")
|
||||||
|
}
|
||||||
@ -1,17 +1,23 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.preference.PreferenceScreen
|
<androidx.preference.PreferenceScreen
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
<org.koitharu.kotatsu.settings.utils.SplitSwitchPreference
|
<org.koitharu.kotatsu.settings.utils.SplitSwitchPreference
|
||||||
android:id="@+id/action_leakcanary"
|
|
||||||
android:key="leak_canary"
|
android:key="leak_canary"
|
||||||
android:persistent="false"
|
android:persistent="false"
|
||||||
android:title="LeakCanary" />
|
android:title="LeakCanary" />
|
||||||
|
|
||||||
<Preference
|
<Preference
|
||||||
android:id="@+id/action_works"
|
|
||||||
android:key="work_inspector"
|
android:key="work_inspector"
|
||||||
android:persistent="false"
|
android:persistent="false"
|
||||||
android:title="@string/wi_lib_name" />
|
android:title="@string/wi_lib_name" />
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="test_parser"
|
||||||
|
android:persistent="false"
|
||||||
|
android:title="@string/test_parser"
|
||||||
|
app:allowDividerAbove="true" />
|
||||||
|
|
||||||
|
|
||||||
</androidx.preference.PreferenceScreen>
|
</androidx.preference.PreferenceScreen>
|
||||||
|
|||||||
@ -0,0 +1,40 @@
|
|||||||
|
package org.koitharu.kotatsu.backups.data.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ScrobblingBackup(
|
||||||
|
@SerialName("scrobbler") val scrobbler: Int,
|
||||||
|
@SerialName("id") val id: Int,
|
||||||
|
@SerialName("manga_id") val mangaId: Long,
|
||||||
|
@SerialName("target_id") val targetId: Long,
|
||||||
|
@SerialName("status") val status: String?,
|
||||||
|
@SerialName("chapter") val chapter: Int,
|
||||||
|
@SerialName("comment") val comment: String?,
|
||||||
|
@SerialName("rating") val rating: Float,
|
||||||
|
) {
|
||||||
|
|
||||||
|
constructor(entity: ScrobblingEntity) : this(
|
||||||
|
scrobbler = entity.scrobbler,
|
||||||
|
id = entity.id,
|
||||||
|
mangaId = entity.mangaId,
|
||||||
|
targetId = entity.targetId,
|
||||||
|
status = entity.status,
|
||||||
|
chapter = entity.chapter,
|
||||||
|
comment = entity.comment,
|
||||||
|
rating = entity.rating,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun toEntity() = ScrobblingEntity(
|
||||||
|
scrobbler = scrobbler,
|
||||||
|
id = id,
|
||||||
|
mangaId = mangaId,
|
||||||
|
targetId = targetId,
|
||||||
|
status = status,
|
||||||
|
chapter = chapter,
|
||||||
|
comment = comment,
|
||||||
|
rating = rating,
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package org.koitharu.kotatsu.backups.data.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.koitharu.kotatsu.stats.data.StatsEntity
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class StatisticBackup(
|
||||||
|
@SerialName("manga_id") val mangaId: Long,
|
||||||
|
@SerialName("started_at") val startedAt: Long,
|
||||||
|
@SerialName("duration") val duration: Long,
|
||||||
|
@SerialName("pages") val pages: Int,
|
||||||
|
) {
|
||||||
|
|
||||||
|
constructor(entity: StatsEntity) : this(
|
||||||
|
mangaId = entity.mangaId,
|
||||||
|
startedAt = entity.startedAt,
|
||||||
|
duration = entity.duration,
|
||||||
|
pages = entity.pages,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun toEntity() = StatsEntity(
|
||||||
|
mangaId = mangaId,
|
||||||
|
startedAt = startedAt,
|
||||||
|
duration = duration,
|
||||||
|
pages = pages,
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package org.koitharu.kotatsu.core.exceptions
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.details.ui.pager.EmptyMangaReason
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
|
||||||
|
class EmptyMangaException(
|
||||||
|
val reason: EmptyMangaReason?,
|
||||||
|
val manga: Manga,
|
||||||
|
cause: Throwable?
|
||||||
|
) : IllegalStateException(cause)
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package org.koitharu.kotatsu.core.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.serialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
|
object MangaSourceSerializer : KSerializer<MangaSource> {
|
||||||
|
|
||||||
|
override val descriptor: SerialDescriptor = serialDescriptor<String>()
|
||||||
|
|
||||||
|
override fun serialize(
|
||||||
|
encoder: Encoder,
|
||||||
|
value: MangaSource
|
||||||
|
) = encoder.encodeString(value.name)
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): MangaSource = MangaSource(decoder.decodeString())
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network.webview
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.webkit.WebView
|
||||||
|
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||||
|
import org.koitharu.kotatsu.parsers.network.CloudFlareHelper
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
|
||||||
|
class CaptchaContinuationClient(
|
||||||
|
private val cookieJar: MutableCookieJar,
|
||||||
|
private val targetUrl: String,
|
||||||
|
continuation: Continuation<Unit>,
|
||||||
|
) : ContinuationResumeWebViewClient(continuation) {
|
||||||
|
|
||||||
|
private val oldClearance = CloudFlareHelper.getClearanceCookie(cookieJar, targetUrl)
|
||||||
|
|
||||||
|
override fun onPageFinished(view: WebView?, url: String?) = Unit
|
||||||
|
|
||||||
|
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||||
|
super.onPageStarted(view, url, favicon)
|
||||||
|
checkClearance(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkClearance(view: WebView?) {
|
||||||
|
val clearance = CloudFlareHelper.getClearanceCookie(cookieJar, targetUrl)
|
||||||
|
if (clearance != null && clearance != oldClearance) {
|
||||||
|
resumeContinuation(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,46 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.parser
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|
||||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
|
||||||
import org.koitharu.kotatsu.parsers.core.AbstractMangaParser
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
|
||||||
import java.util.EnumSet
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This parser is just for parser development, it should not be used in releases
|
|
||||||
*/
|
|
||||||
class DummyParser(context: MangaLoaderContext) : AbstractMangaParser(context, MangaParserSource.DUMMY) {
|
|
||||||
|
|
||||||
override val configKeyDomain: ConfigKey.Domain
|
|
||||||
get() = ConfigKey.Domain("localhost")
|
|
||||||
|
|
||||||
override val availableSortOrders: Set<SortOrder>
|
|
||||||
get() = EnumSet.allOf(SortOrder::class.java)
|
|
||||||
|
|
||||||
override val filterCapabilities: MangaListFilterCapabilities
|
|
||||||
get() = MangaListFilterCapabilities()
|
|
||||||
|
|
||||||
override suspend fun getDetails(manga: Manga): Manga = stub(manga)
|
|
||||||
|
|
||||||
override suspend fun getFilterOptions(): MangaListFilterOptions = stub(null)
|
|
||||||
|
|
||||||
override suspend fun getList(
|
|
||||||
offset: Int,
|
|
||||||
order: SortOrder,
|
|
||||||
filter: MangaListFilter
|
|
||||||
): List<Manga> = stub(null)
|
|
||||||
|
|
||||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = stub(null)
|
|
||||||
|
|
||||||
private fun stub(manga: Manga?): Nothing {
|
|
||||||
throw UnsupportedSourceException("Usage of Dummy parser", manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.parser
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaParser
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
|
||||||
|
|
||||||
fun MangaParser(source: MangaParserSource, loaderContext: MangaLoaderContext): MangaParser {
|
|
||||||
return when (source) {
|
|
||||||
MangaParserSource.DUMMY -> DummyParser(loaderContext)
|
|
||||||
else -> loaderContext.newParserInstance(source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.ui
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
|
|
||||||
fun interface OnContextClickListenerCompat {
|
|
||||||
|
|
||||||
fun onContextClick(v: View): Boolean
|
|
||||||
}
|
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package org.koitharu.kotatsu.core.ui.widgets
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
|
||||||
|
class TouchBlockLayout @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null
|
||||||
|
) : FrameLayout(context, attrs) {
|
||||||
|
|
||||||
|
var isTouchEventsAllowed = true
|
||||||
|
|
||||||
|
override fun onInterceptTouchEvent(
|
||||||
|
ev: MotionEvent?
|
||||||
|
): Boolean = if (isTouchEventsAllowed) {
|
||||||
|
super.onInterceptTouchEvent(ev)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,46 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.util
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import androidx.work.WorkInfo
|
|
||||||
import androidx.work.WorkManager
|
|
||||||
import androidx.work.WorkQuery
|
|
||||||
import androidx.work.impl.foreground.SystemForegroundService
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
|
||||||
import javax.inject.Provider
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Workaround for issue
|
|
||||||
* https://issuetracker.google.com/issues/270245927
|
|
||||||
* https://issuetracker.google.com/issues/280504155
|
|
||||||
*/
|
|
||||||
class WorkServiceStopHelper(
|
|
||||||
private val workManagerProvider: Provider<WorkManager>,
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun setup() {
|
|
||||||
processLifecycleScope.launch(Dispatchers.Default) {
|
|
||||||
workManagerProvider.get()
|
|
||||||
.getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.RUNNING))
|
|
||||||
.map { it.isEmpty() }
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.collectLatest {
|
|
||||||
if (it) {
|
|
||||||
delay(1_000)
|
|
||||||
stopWorkerService()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
private fun stopWorkerService() {
|
|
||||||
SystemForegroundService.getInstance()?.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -0,0 +1,161 @@
|
|||||||
|
package org.koitharu.kotatsu.filter.data
|
||||||
|
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.builtins.SetSerializer
|
||||||
|
import kotlinx.serialization.builtins.serializer
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.element
|
||||||
|
import kotlinx.serialization.encoding.CompositeDecoder
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.encoding.decodeStructure
|
||||||
|
import kotlinx.serialization.encoding.encodeStructure
|
||||||
|
import kotlinx.serialization.serializer
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.toLocaleOrNull
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Demographic
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
object MangaListFilterSerializer : KSerializer<MangaListFilter> {
|
||||||
|
|
||||||
|
override val descriptor: SerialDescriptor =
|
||||||
|
buildClassSerialDescriptor(MangaListFilter::class.java.name) {
|
||||||
|
element<String?>("query", isOptional = true)
|
||||||
|
element(
|
||||||
|
elementName = "tags",
|
||||||
|
descriptor = SetSerializer(MangaTagSerializer).descriptor,
|
||||||
|
isOptional = true,
|
||||||
|
)
|
||||||
|
element(
|
||||||
|
elementName = "tagsExclude",
|
||||||
|
descriptor = SetSerializer(MangaTagSerializer).descriptor,
|
||||||
|
isOptional = true,
|
||||||
|
)
|
||||||
|
element<String?>("locale", isOptional = true)
|
||||||
|
element<String?>("originalLocale", isOptional = true)
|
||||||
|
element<Set<MangaState>>("states", isOptional = true)
|
||||||
|
element<Set<ContentRating>>("contentRating", isOptional = true)
|
||||||
|
element<Set<ContentType>>("types", isOptional = true)
|
||||||
|
element<Set<Demographic>>("demographics", isOptional = true)
|
||||||
|
element<Int>("year", isOptional = true)
|
||||||
|
element<Int>("yearFrom", isOptional = true)
|
||||||
|
element<Int>("yearTo", isOptional = true)
|
||||||
|
element<String?>("author", isOptional = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(
|
||||||
|
encoder: Encoder,
|
||||||
|
value: MangaListFilter
|
||||||
|
) = encoder.encodeStructure(descriptor) {
|
||||||
|
encodeNullableSerializableElement(descriptor, 0, String.serializer(), value.query)
|
||||||
|
encodeSerializableElement(descriptor, 1, SetSerializer(MangaTagSerializer), value.tags)
|
||||||
|
encodeSerializableElement(descriptor, 2, SetSerializer(MangaTagSerializer), value.tagsExclude)
|
||||||
|
encodeNullableSerializableElement(descriptor, 3, String.serializer(), value.locale?.toLanguageTag())
|
||||||
|
encodeNullableSerializableElement(descriptor, 4, String.serializer(), value.originalLocale?.toLanguageTag())
|
||||||
|
encodeSerializableElement(descriptor, 5, SetSerializer(serializer()), value.states)
|
||||||
|
encodeSerializableElement(descriptor, 6, SetSerializer(serializer()), value.contentRating)
|
||||||
|
encodeSerializableElement(descriptor, 7, SetSerializer(serializer()), value.types)
|
||||||
|
encodeSerializableElement(descriptor, 8, SetSerializer(serializer()), value.demographics)
|
||||||
|
encodeIntElement(descriptor, 9, value.year)
|
||||||
|
encodeIntElement(descriptor, 10, value.yearFrom)
|
||||||
|
encodeIntElement(descriptor, 11, value.yearTo)
|
||||||
|
encodeNullableSerializableElement(descriptor, 12, String.serializer(), value.author)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(
|
||||||
|
decoder: Decoder
|
||||||
|
): MangaListFilter = decoder.decodeStructure(descriptor) {
|
||||||
|
var query: String? = MangaListFilter.EMPTY.query
|
||||||
|
var tags: Set<MangaTag> = MangaListFilter.EMPTY.tags
|
||||||
|
var tagsExclude: Set<MangaTag> = MangaListFilter.EMPTY.tagsExclude
|
||||||
|
var locale: Locale? = MangaListFilter.EMPTY.locale
|
||||||
|
var originalLocale: Locale? = MangaListFilter.EMPTY.originalLocale
|
||||||
|
var states: Set<MangaState> = MangaListFilter.EMPTY.states
|
||||||
|
var contentRating: Set<ContentRating> = MangaListFilter.EMPTY.contentRating
|
||||||
|
var types: Set<ContentType> = MangaListFilter.EMPTY.types
|
||||||
|
var demographics: Set<Demographic> = MangaListFilter.EMPTY.demographics
|
||||||
|
var year: Int = MangaListFilter.EMPTY.year
|
||||||
|
var yearFrom: Int = MangaListFilter.EMPTY.yearFrom
|
||||||
|
var yearTo: Int = MangaListFilter.EMPTY.yearTo
|
||||||
|
var author: String? = MangaListFilter.EMPTY.author
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
when (decodeElementIndex(descriptor)) {
|
||||||
|
0 -> query = decodeNullableSerializableElement(descriptor, 0, serializer<String>())
|
||||||
|
1 -> tags = decodeSerializableElement(descriptor, 1, SetSerializer(MangaTagSerializer))
|
||||||
|
2 -> tagsExclude = decodeSerializableElement(descriptor, 2, SetSerializer(MangaTagSerializer))
|
||||||
|
3 -> locale = decodeNullableSerializableElement(descriptor, 3, serializer<String>())?.toLocaleOrNull()
|
||||||
|
4 -> originalLocale =
|
||||||
|
decodeNullableSerializableElement(descriptor, 4, serializer<String>())?.toLocaleOrNull()
|
||||||
|
|
||||||
|
5 -> states = decodeSerializableElement(descriptor, 5, SetSerializer(serializer()))
|
||||||
|
6 -> contentRating = decodeSerializableElement(descriptor, 6, SetSerializer(serializer()))
|
||||||
|
7 -> types = decodeSerializableElement(descriptor, 7, SetSerializer(serializer()))
|
||||||
|
8 -> demographics = decodeSerializableElement(descriptor, 8, SetSerializer(serializer()))
|
||||||
|
9 -> year = decodeIntElement(descriptor, 9)
|
||||||
|
10 -> yearFrom = decodeIntElement(descriptor, 10)
|
||||||
|
11 -> yearTo = decodeIntElement(descriptor, 11)
|
||||||
|
12 -> author = decodeNullableSerializableElement(descriptor, 12, serializer<String>())
|
||||||
|
CompositeDecoder.DECODE_DONE -> break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MangaListFilter(
|
||||||
|
query = query,
|
||||||
|
tags = tags,
|
||||||
|
tagsExclude = tagsExclude,
|
||||||
|
locale = locale,
|
||||||
|
originalLocale = originalLocale,
|
||||||
|
states = states,
|
||||||
|
contentRating = contentRating,
|
||||||
|
types = types,
|
||||||
|
demographics = demographics,
|
||||||
|
year = year,
|
||||||
|
yearFrom = yearFrom,
|
||||||
|
yearTo = yearTo,
|
||||||
|
author = author,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private object MangaTagSerializer : KSerializer<MangaTag> {
|
||||||
|
|
||||||
|
override val descriptor: SerialDescriptor = buildClassSerialDescriptor(MangaTag::class.java.name) {
|
||||||
|
element<String>("title")
|
||||||
|
element<String>("key")
|
||||||
|
element<String>("source")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: MangaTag) = encoder.encodeStructure(descriptor) {
|
||||||
|
encodeStringElement(descriptor, 0, value.title)
|
||||||
|
encodeStringElement(descriptor, 1, value.key)
|
||||||
|
encodeStringElement(descriptor, 2, value.source.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): MangaTag = decoder.decodeStructure(descriptor) {
|
||||||
|
var title: String? = null
|
||||||
|
var key: String? = null
|
||||||
|
var source: String? = null
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
when (decodeElementIndex(descriptor)) {
|
||||||
|
0 -> title = decodeStringElement(descriptor, 0)
|
||||||
|
1 -> key = decodeStringElement(descriptor, 1)
|
||||||
|
2 -> source = decodeStringElement(descriptor, 2)
|
||||||
|
CompositeDecoder.DECODE_DONE -> break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MangaTag(
|
||||||
|
title = title ?: error("Missing 'title' field"),
|
||||||
|
key = key ?: error("Missing 'key' field"),
|
||||||
|
source = MangaSource(source),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package org.koitharu.kotatsu.filter.data
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonIgnoreUnknownKeys
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaSourceSerializer
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@JsonIgnoreUnknownKeys
|
||||||
|
data class PersistableFilter(
|
||||||
|
@SerialName("name")
|
||||||
|
val name: String,
|
||||||
|
@Serializable(with = MangaSourceSerializer::class)
|
||||||
|
@SerialName("source")
|
||||||
|
val source: MangaSource,
|
||||||
|
@Serializable(with = MangaListFilterSerializer::class)
|
||||||
|
@SerialName("filter")
|
||||||
|
val filter: MangaListFilter,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val id: Int
|
||||||
|
get() = name.hashCode()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val MAX_TITLE_LENGTH = 18
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
package org.koitharu.kotatsu.filter.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import dagger.Reusable
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.SerializationException
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.observeChanges
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@Reusable
|
||||||
|
class SavedFiltersRepository @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun observeAll(source: MangaSource): Flow<List<PersistableFilter>> = getPrefs(source).observeChanges()
|
||||||
|
.onStart { emit(null) }
|
||||||
|
.map {
|
||||||
|
getAll(source)
|
||||||
|
}.distinctUntilChanged()
|
||||||
|
.flowOn(Dispatchers.Default)
|
||||||
|
|
||||||
|
suspend fun getAll(source: MangaSource): List<PersistableFilter> = withContext(Dispatchers.Default) {
|
||||||
|
val prefs = getPrefs(source)
|
||||||
|
val keys = prefs.all.keys.filter { it.startsWith(FILTER_PREFIX) }
|
||||||
|
keys.mapNotNull { key ->
|
||||||
|
val value = prefs.getString(key, null) ?: return@mapNotNull null
|
||||||
|
try {
|
||||||
|
Json.decodeFromString(value)
|
||||||
|
} catch (e: SerializationException) {
|
||||||
|
e.printStackTraceDebug()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun save(
|
||||||
|
source: MangaSource,
|
||||||
|
name: String,
|
||||||
|
filter: MangaListFilter,
|
||||||
|
): PersistableFilter = withContext(Dispatchers.Default) {
|
||||||
|
val persistableFilter = PersistableFilter(
|
||||||
|
name = name,
|
||||||
|
source = source,
|
||||||
|
filter = filter,
|
||||||
|
)
|
||||||
|
persist(persistableFilter)
|
||||||
|
persistableFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun save(
|
||||||
|
filter: PersistableFilter,
|
||||||
|
) = withContext(Dispatchers.Default) {
|
||||||
|
persist(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun rename(source: MangaSource, id: Int, newName: String) = withContext(Dispatchers.Default) {
|
||||||
|
val filter = load(source, id) ?: return@withContext
|
||||||
|
val newFilter = filter.copy(name = newName)
|
||||||
|
val prefs = getPrefs(source)
|
||||||
|
prefs.edit(commit = true) {
|
||||||
|
remove(key(id))
|
||||||
|
putString(key(newFilter.id), Json.encodeToString(newFilter))
|
||||||
|
}
|
||||||
|
newFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun delete(source: MangaSource, id: Int) = withContext(Dispatchers.Default) {
|
||||||
|
val prefs = getPrefs(source)
|
||||||
|
prefs.edit(commit = true) {
|
||||||
|
remove(key(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun persist(persistableFilter: PersistableFilter) {
|
||||||
|
val prefs = getPrefs(persistableFilter.source)
|
||||||
|
val json = Json.encodeToString(persistableFilter)
|
||||||
|
prefs.edit(commit = true) {
|
||||||
|
putString(key(persistableFilter.id), json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun load(source: MangaSource, id: Int): PersistableFilter? {
|
||||||
|
val prefs = getPrefs(source)
|
||||||
|
val json = prefs.getString(key(id), null) ?: return null
|
||||||
|
return try {
|
||||||
|
Json.decodeFromString<PersistableFilter>(json)
|
||||||
|
} catch (e: SerializationException) {
|
||||||
|
e.printStackTraceDebug()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPrefs(source: MangaSource): SharedPreferences {
|
||||||
|
val key = source.name.replace(File.separatorChar, '$')
|
||||||
|
return context.getSharedPreferences(key, Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
const val FILTER_PREFIX = "__pf_"
|
||||||
|
|
||||||
|
fun key(id: Int) = FILTER_PREFIX + id
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue