Compare commits
1 Commits
v7.4.2
...
ui_playgro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4e4f18066 |
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -2,4 +2,4 @@ blank_issues_enabled: false
|
|||||||
contact_links:
|
contact_links:
|
||||||
- name: ⚠️ Source issue
|
- name: ⚠️ Source issue
|
||||||
url: https://github.com/KotatsuApp/kotatsu-parsers/issues/new
|
url: https://github.com/KotatsuApp/kotatsu-parsers/issues/new
|
||||||
about: If you have a problem with a specific **manga source** or want to propose a new one, please open an issue in the kotatsu-parsers repository instead
|
about: If you have troubles with a manga parser or want to propose new manga source, please open an issue in the kotatsu-parsers repository instead
|
||||||
4
.github/ISSUE_TEMPLATE/report_bug.yml
vendored
4
.github/ISSUE_TEMPLATE/report_bug.yml
vendored
@@ -60,7 +60,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
options:
|
options:
|
||||||
- label: This is not a duplicate of an existing issue. Please look through the list of [open issues](https://github.com/KotatsuApp/Kotatsu/issues) before creating a new one.
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
required: true
|
required: true
|
||||||
- label: This is not an issue with a specific manga source. Otherwise, you have to open an issue in the [parsers repository](https://github.com/KotatsuApp/kotatsu-parsers/issues/new/choose).
|
- label: If this is an issue with a parser, I should be opening an issue in the [parsers repository](https://github.com/KotatsuApp/kotatsu-parsers/issues/new/choose).
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
4
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@@ -20,5 +20,5 @@ body:
|
|||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||||
options:
|
options:
|
||||||
- label: This is not a duplicate of an existing issue. Please look through the list of [open issues](https://github.com/KotatsuApp/Kotatsu/issues) before creating a new one.
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
required: true
|
required: true
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -25,4 +25,3 @@
|
|||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
.cxx
|
.cxx
|
||||||
/.idea/deviceManager.xml
|
/.idea/deviceManager.xml
|
||||||
/.kotlin/
|
|
||||||
|
|||||||
1
.idea/.gitignore
generated
vendored
1
.idea/.gitignore
generated
vendored
@@ -2,4 +2,3 @@
|
|||||||
/shelf/
|
/shelf/
|
||||||
/workspace.xml
|
/workspace.xml
|
||||||
/migrations.xml
|
/migrations.xml
|
||||||
/runConfigurations.xml
|
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'org.koitharu.kotatsu'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 35
|
targetSdk = 34
|
||||||
versionCode = 659
|
versionCode = 642
|
||||||
versionName = '7.4.2'
|
versionName = '7.0.1'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||||
ksp {
|
ksp {
|
||||||
@@ -82,23 +82,22 @@ afterEvaluate {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
implementation('com.github.KotatsuApp:kotatsu-parsers:ca212ca692') {
|
implementation('com.github.KotatsuApp:kotatsu-parsers:078b59b1e2') {
|
||||||
exclude group: 'org.json', module: 'json'
|
exclude group: 'org.json', module: 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib:2.0.10-RC'
|
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.24'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0-RC'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1'
|
||||||
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'androidx.core:core-ktx:1.13.1'
|
implementation 'androidx.core:core-ktx:1.13.1'
|
||||||
implementation 'androidx.activity:activity-ktx:1.9.1'
|
implementation 'androidx.activity:activity-ktx:1.9.0'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.8.2'
|
implementation 'androidx.fragment:fragment-ktx:1.7.1'
|
||||||
implementation 'androidx.transition:transition-ktx:1.5.1'
|
implementation 'androidx.collection:collection-ktx:1.4.0'
|
||||||
implementation 'androidx.collection:collection-ktx:1.4.3'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4'
|
implementation 'androidx.lifecycle:lifecycle-service:2.8.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-service:2.8.4'
|
implementation 'androidx.lifecycle:lifecycle-process:2.8.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-process:2.8.4'
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||||
@@ -106,10 +105,10 @@ dependencies {
|
|||||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||||
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
||||||
implementation 'com.google.android.material:material:1.12.0'
|
implementation 'com.google.android.material:material:1.12.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.8.4'
|
implementation 'androidx.lifecycle:lifecycle-common-java8:2.8.0'
|
||||||
implementation 'androidx.webkit:webkit:1.11.0'
|
implementation 'androidx.webkit:webkit:1.11.0'
|
||||||
|
|
||||||
implementation 'androidx.work:work-runtime:2.9.1'
|
implementation 'androidx.work:work-runtime:2.9.0'
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
implementation('com.google.guava:guava:32.0.1-android') {
|
implementation('com.google.guava:guava:32.0.1-android') {
|
||||||
exclude group: 'com.google.guava', module: 'failureaccess'
|
exclude group: 'com.google.guava', module: 'failureaccess'
|
||||||
@@ -134,9 +133,9 @@ dependencies {
|
|||||||
implementation 'androidx.hilt:hilt-work:1.2.0'
|
implementation 'androidx.hilt:hilt-work:1.2.0'
|
||||||
kapt 'androidx.hilt:hilt-compiler:1.2.0'
|
kapt 'androidx.hilt:hilt-compiler:1.2.0'
|
||||||
|
|
||||||
implementation 'io.coil-kt:coil-base:2.7.0'
|
implementation 'io.coil-kt:coil-base:2.6.0'
|
||||||
implementation 'io.coil-kt:coil-svg:2.7.0'
|
implementation 'io.coil-kt:coil-svg:2.6.0'
|
||||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:882bc0620c'
|
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:02e6d6cfe9'
|
||||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
||||||
implementation 'io.noties.markwon:core:4.6.2'
|
implementation 'io.noties.markwon:core:4.6.2'
|
||||||
|
|
||||||
@@ -154,10 +153,10 @@ dependencies {
|
|||||||
testImplementation 'org.json:json:20240303'
|
testImplementation 'org.json:json:20240303'
|
||||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1'
|
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1'
|
||||||
|
|
||||||
androidTestImplementation 'androidx.test:runner:1.6.1'
|
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||||
androidTestImplementation 'androidx.test:rules:1.6.1'
|
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||||
androidTestImplementation 'androidx.test:core-ktx:1.6.1'
|
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
|
||||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.2.1'
|
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
|
||||||
|
|
||||||
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1'
|
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1'
|
||||||
|
|
||||||
|
|||||||
12
app/src/debug/AndroidManifest.xml
Normal file
12
app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<manifest
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".tracker.ui.debug.TrackerDebugActivity"
|
||||||
|
android:label="@string/check_for_new_chapters" />
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.settings
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.core.view.MenuProvider
|
|
||||||
import leakcanary.LeakCanary
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.workinspector.WorkInspector
|
|
||||||
|
|
||||||
class SettingsMenuProvider(
|
|
||||||
private val context: Context,
|
|
||||||
) : MenuProvider {
|
|
||||||
|
|
||||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
|
||||||
menuInflater.inflate(R.menu.opt_settings, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
|
||||||
R.id.action_leaks -> {
|
|
||||||
context.startActivity(LeakCanary.newLeakDisplayActivityIntent())
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.action_works -> {
|
|
||||||
context.startActivity(WorkInspector.getIntent(context))
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -58,7 +58,7 @@ fun trackDebugAD(
|
|||||||
append(" - ")
|
append(" - ")
|
||||||
bold {
|
bold {
|
||||||
color(context.getThemeColor(materialR.attr.colorError, Color.RED)) {
|
color(context.getThemeColor(materialR.attr.colorError, Color.RED)) {
|
||||||
append(item.lastError ?: getString(R.string.error))
|
append(getString(R.string.error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,6 @@ data class TrackDebugItem(
|
|||||||
val lastCheckTime: Instant?,
|
val lastCheckTime: Instant?,
|
||||||
val lastChapterDate: Instant?,
|
val lastChapterDate: Instant?,
|
||||||
val lastResult: Int,
|
val lastResult: Int,
|
||||||
val lastError: String?,
|
|
||||||
) : ListModel {
|
) : ListModel {
|
||||||
|
|
||||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||||
@@ -32,7 +32,6 @@ class TrackerDebugActivity : BaseActivity<ActivityTrackerDebugBinding>(), OnList
|
|||||||
val tracksAdapter = BaseListAdapter<TrackDebugItem>()
|
val tracksAdapter = BaseListAdapter<TrackDebugItem>()
|
||||||
.addDelegate(ListItemType.FEED, trackDebugAD(this, coil, this))
|
.addDelegate(ListItemType.FEED, trackDebugAD(this, coil, this))
|
||||||
with(viewBinding.recyclerView) {
|
with(viewBinding.recyclerView) {
|
||||||
setHasFixedSize(true)
|
|
||||||
adapter = tracksAdapter
|
adapter = tracksAdapter
|
||||||
addItemDecoration(TypedListSpacingDecoration(context, false))
|
addItemDecoration(TypedListSpacingDecoration(context, false))
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,6 @@ class TrackerDebugViewModel @Inject constructor(
|
|||||||
lastCheckTime = it.track.lastCheckTime.toInstantOrNull(),
|
lastCheckTime = it.track.lastCheckTime.toInstantOrNull(),
|
||||||
lastChapterDate = it.track.lastChapterDate.toInstantOrNull(),
|
lastChapterDate = it.track.lastChapterDate.toInstantOrNull(),
|
||||||
lastResult = it.track.lastResult,
|
lastResult = it.track.lastResult,
|
||||||
lastError = it.track.lastError,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,6 @@
|
|||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
|
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="16dp"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
@@ -8,9 +8,14 @@
|
|||||||
android:title="@string/leak_canary_display_activity_label"
|
android:title="@string/leak_canary_display_activity_label"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@id/action_tracker"
|
||||||
|
android:title="@string/check_for_new_chapters"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@id/action_works"
|
android:id="@id/action_works"
|
||||||
android:title="@string/wi_lib_name"
|
android:title="Works"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
@@ -20,10 +20,6 @@
|
|||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES"/>
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
|
||||||
tools:ignore="QueryAllPackagesPermission" />
|
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="29" />
|
android:maxSdkVersion="29" />
|
||||||
@@ -104,13 +100,6 @@
|
|||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="${applicationId}.action.READ_MANGA" />
|
<action android:name="${applicationId}.action.READ_MANGA" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
|
||||||
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="com.samsung.android.support.REMOTE_ACTION"
|
|
||||||
android:resource="@xml/remote_action" />
|
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
|
android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
|
||||||
@@ -259,9 +248,6 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.settings.about.AppUpdateActivity"
|
android:name="org.koitharu.kotatsu.settings.about.AppUpdateActivity"
|
||||||
android:label="@string/app_update_available" />
|
android:label="@string/app_update_available" />
|
||||||
<activity
|
|
||||||
android:name="org.koitharu.kotatsu.tracker.ui.debug.TrackerDebugActivity"
|
|
||||||
android:label="@string/tracker_debug_info" />
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import org.koitharu.kotatsu.core.util.ext.almostEquals
|
|||||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -58,7 +57,7 @@ class AlternativesUseCase @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getSources(ref: MangaSource): List<MangaSource> {
|
private suspend fun getSources(ref: MangaSource): List<MangaSource> {
|
||||||
val result = ArrayList<MangaSource>(MangaParserSource.entries.size - 2)
|
val result = ArrayList<MangaSource>(MangaSource.entries.size - 2)
|
||||||
result.addAll(sourcesRepository.getEnabledSources())
|
result.addAll(sourcesRepository.getEnabledSources())
|
||||||
result.sortByDescending { it.priority(ref) }
|
result.sortByDescending { it.priority(ref) }
|
||||||
result.addAll(sourcesRepository.getDisabledSources().sortedByDescending { it.priority(ref) })
|
result.addAll(sourcesRepository.getDisabledSources().sortedByDescending { it.priority(ref) })
|
||||||
@@ -79,10 +78,8 @@ class AlternativesUseCase @Inject constructor(
|
|||||||
|
|
||||||
private fun MangaSource.priority(ref: MangaSource): Int {
|
private fun MangaSource.priority(ref: MangaSource): Int {
|
||||||
var res = 0
|
var res = 0
|
||||||
if (this is MangaParserSource && ref is MangaParserSource) {
|
if (locale == ref.locale) res += 2
|
||||||
if (locale == ref.locale) res += 2
|
if (contentType == ref.contentType) res++
|
||||||
if (contentType == ref.contentType) res++
|
|
||||||
}
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,38 +12,29 @@ import org.koitharu.kotatsu.history.data.toMangaHistory
|
|||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
|
||||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
|
||||||
import org.koitharu.kotatsu.tracker.data.TrackEntity
|
import org.koitharu.kotatsu.tracker.data.TrackEntity
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class MigrateUseCase
|
class MigrateUseCase @Inject constructor(
|
||||||
@Inject
|
|
||||||
constructor(
|
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
private val mangaDataRepository: MangaDataRepository,
|
private val mangaDataRepository: MangaDataRepository,
|
||||||
private val database: MangaDatabase,
|
private val database: MangaDatabase,
|
||||||
private val progressUpdateUseCase: ProgressUpdateUseCase,
|
private val progressUpdateUseCase: ProgressUpdateUseCase,
|
||||||
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
|
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(
|
|
||||||
oldManga: Manga,
|
suspend operator fun invoke(oldManga: Manga, newManga: Manga) {
|
||||||
newManga: Manga,
|
val oldDetails = if (oldManga.chapters.isNullOrEmpty()) {
|
||||||
) {
|
runCatchingCancellable {
|
||||||
val oldDetails =
|
mangaRepositoryFactory.create(oldManga.source).getDetails(oldManga)
|
||||||
if (oldManga.chapters.isNullOrEmpty()) {
|
}.getOrDefault(oldManga)
|
||||||
runCatchingCancellable {
|
} else {
|
||||||
mangaRepositoryFactory.create(oldManga.source).getDetails(oldManga)
|
oldManga
|
||||||
}.getOrDefault(oldManga)
|
}
|
||||||
} else {
|
val newDetails = if (newManga.chapters.isNullOrEmpty()) {
|
||||||
oldManga
|
mangaRepositoryFactory.create(newManga.source).getDetails(newManga)
|
||||||
}
|
} else {
|
||||||
val newDetails =
|
newManga
|
||||||
if (newManga.chapters.isNullOrEmpty()) {
|
}
|
||||||
mangaRepositoryFactory.create(newManga.source).getDetails(newManga)
|
|
||||||
} else {
|
|
||||||
newManga
|
|
||||||
}
|
|
||||||
mangaDataRepository.storeManga(newDetails)
|
mangaDataRepository.storeManga(newDetails)
|
||||||
database.withTransaction {
|
database.withTransaction {
|
||||||
// replace favorites
|
// replace favorites
|
||||||
@@ -52,69 +43,36 @@ constructor(
|
|||||||
if (oldFavourites.isNotEmpty()) {
|
if (oldFavourites.isNotEmpty()) {
|
||||||
favoritesDao.delete(oldManga.id)
|
favoritesDao.delete(oldManga.id)
|
||||||
for (f in oldFavourites) {
|
for (f in oldFavourites) {
|
||||||
val e =
|
val e = f.copy(
|
||||||
f.copy(
|
mangaId = newManga.id,
|
||||||
mangaId = newManga.id,
|
)
|
||||||
)
|
|
||||||
favoritesDao.upsert(e)
|
favoritesDao.upsert(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// replace history
|
// replace history
|
||||||
val historyDao = database.getHistoryDao()
|
val historyDao = database.getHistoryDao()
|
||||||
val oldHistory = historyDao.find(oldDetails.id)
|
val oldHistory = historyDao.find(oldDetails.id)
|
||||||
val newHistory =
|
if (oldHistory != null) {
|
||||||
if (oldHistory != null) {
|
val newHistory = makeNewHistory(oldDetails, newDetails, oldHistory)
|
||||||
val newHistory = makeNewHistory(oldDetails, newDetails, oldHistory)
|
historyDao.delete(oldDetails.id)
|
||||||
historyDao.delete(oldDetails.id)
|
historyDao.upsert(newHistory)
|
||||||
historyDao.upsert(newHistory)
|
}
|
||||||
newHistory
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
// track
|
// track
|
||||||
val tracksDao = database.getTracksDao()
|
val tracksDao = database.getTracksDao()
|
||||||
val oldTrack = tracksDao.find(oldDetails.id)
|
val oldTrack = tracksDao.find(oldDetails.id)
|
||||||
if (oldTrack != null) {
|
if (oldTrack != null) {
|
||||||
val lastChapter = newDetails.chapters?.lastOrNull()
|
val lastChapter = newDetails.chapters?.lastOrNull()
|
||||||
val newTrack =
|
val newTrack = TrackEntity(
|
||||||
TrackEntity(
|
mangaId = newDetails.id,
|
||||||
mangaId = newDetails.id,
|
lastChapterId = lastChapter?.id ?: 0L,
|
||||||
lastChapterId = lastChapter?.id ?: 0L,
|
newChapters = 0,
|
||||||
newChapters = 0,
|
lastCheckTime = System.currentTimeMillis(),
|
||||||
lastCheckTime = System.currentTimeMillis(),
|
lastChapterDate = lastChapter?.uploadDate ?: 0L,
|
||||||
lastChapterDate = lastChapter?.uploadDate ?: 0L,
|
lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION,
|
||||||
lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION,
|
)
|
||||||
lastError = null,
|
|
||||||
)
|
|
||||||
tracksDao.delete(oldDetails.id)
|
tracksDao.delete(oldDetails.id)
|
||||||
tracksDao.upsert(newTrack)
|
tracksDao.upsert(newTrack)
|
||||||
}
|
}
|
||||||
// scrobbling
|
|
||||||
for (scrobbler in scrobblers) {
|
|
||||||
if (!scrobbler.isEnabled) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val prevInfo = scrobbler.getScrobblingInfoOrNull(oldDetails.id) ?: continue
|
|
||||||
scrobbler.unregisterScrobbling(oldDetails.id)
|
|
||||||
scrobbler.linkManga(newDetails.id, prevInfo.targetId)
|
|
||||||
scrobbler.updateScrobblingInfo(
|
|
||||||
mangaId = newDetails.id,
|
|
||||||
rating = prevInfo.rating,
|
|
||||||
status =
|
|
||||||
prevInfo.status ?: when {
|
|
||||||
newHistory == null -> ScrobblingStatus.PLANNED
|
|
||||||
newHistory.percent == 1f -> ScrobblingStatus.COMPLETED
|
|
||||||
else -> ScrobblingStatus.READING
|
|
||||||
},
|
|
||||||
comment = prevInfo.comment,
|
|
||||||
)
|
|
||||||
if (newHistory != null) {
|
|
||||||
scrobbler.scrobble(
|
|
||||||
manga = newDetails,
|
|
||||||
chapterId = newHistory.chapterId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
progressUpdateUseCase(newManga)
|
progressUpdateUseCase(newManga)
|
||||||
}
|
}
|
||||||
@@ -127,12 +85,11 @@ constructor(
|
|||||||
if (oldManga.chapters.isNullOrEmpty()) { // probably broken manga/source
|
if (oldManga.chapters.isNullOrEmpty()) { // probably broken manga/source
|
||||||
val branch = newManga.getPreferredBranch(null)
|
val branch = newManga.getPreferredBranch(null)
|
||||||
val chapters = checkNotNull(newManga.getChapters(branch))
|
val chapters = checkNotNull(newManga.getChapters(branch))
|
||||||
val currentChapter =
|
val currentChapter = if (history.percent in 0f..1f) {
|
||||||
if (history.percent in 0f..1f) {
|
chapters[(chapters.lastIndex * history.percent).toInt()]
|
||||||
chapters[(chapters.lastIndex * history.percent).toInt()]
|
} else {
|
||||||
} else {
|
chapters.first()
|
||||||
chapters.first()
|
}
|
||||||
}
|
|
||||||
return HistoryEntity(
|
return HistoryEntity(
|
||||||
mangaId = newManga.id,
|
mangaId = newManga.id,
|
||||||
createdAt = history.createdAt,
|
createdAt = history.createdAt,
|
||||||
@@ -142,33 +99,29 @@ constructor(
|
|||||||
scroll = history.scroll,
|
scroll = history.scroll,
|
||||||
percent = history.percent,
|
percent = history.percent,
|
||||||
deletedAt = 0,
|
deletedAt = 0,
|
||||||
chaptersCount = chapters.count { it.branch == currentChapter.branch },
|
chaptersCount = chapters.size,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val branch = oldManga.getPreferredBranch(history.toMangaHistory())
|
val branch = oldManga.getPreferredBranch(history.toMangaHistory())
|
||||||
val oldChapters = checkNotNull(oldManga.getChapters(branch))
|
val oldChapters = checkNotNull(oldManga.getChapters(branch))
|
||||||
var index = oldChapters.indexOfFirst { it.id == history.chapterId }
|
var index = oldChapters.indexOfFirst { it.id == history.chapterId }
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
index =
|
index = if (history.percent in 0f..1f) {
|
||||||
if (history.percent in 0f..1f) {
|
(oldChapters.lastIndex * history.percent).toInt()
|
||||||
(oldChapters.lastIndex * history.percent).toInt()
|
} else {
|
||||||
} else {
|
0
|
||||||
0
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val newChapters = checkNotNull(newManga.chapters).groupBy { it.branch }
|
val newChapters = checkNotNull(newManga.chapters).groupBy { it.branch }
|
||||||
val newBranch =
|
val newBranch = if (newChapters.containsKey(branch)) {
|
||||||
if (newChapters.containsKey(branch)) {
|
branch
|
||||||
branch
|
} else {
|
||||||
} else {
|
newManga.getPreferredBranch(null)
|
||||||
newManga.getPreferredBranch(null)
|
}
|
||||||
}
|
val newChapterId = checkNotNull(newChapters[newBranch]).let {
|
||||||
val newChapterId =
|
val oldChapter = oldChapters[index]
|
||||||
checkNotNull(newChapters[newBranch])
|
it.findByNumber(oldChapter.volume, oldChapter.number) ?: it.getOrNull(index) ?: it.last()
|
||||||
.let {
|
}.id
|
||||||
val oldChapter = oldChapters[index]
|
|
||||||
it.findByNumber(oldChapter.volume, oldChapter.number) ?: it.getOrNull(index) ?: it.last()
|
|
||||||
}.id
|
|
||||||
|
|
||||||
return HistoryEntity(
|
return HistoryEntity(
|
||||||
mangaId = newManga.id,
|
mangaId = newManga.id,
|
||||||
@@ -183,13 +136,11 @@ constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<MangaChapter>.findByNumber(
|
private fun List<MangaChapter>.findByNumber(volume: Int, number: Float): MangaChapter? {
|
||||||
volume: Int,
|
return if (number <= 0f) {
|
||||||
number: Float,
|
|
||||||
): MangaChapter? =
|
|
||||||
if (number <= 0f) {
|
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
firstOrNull { it.volume == volume && it.number == number }
|
firstOrNull { it.volume == volume && it.number == number }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import coil.request.ImageRequest
|
|||||||
import coil.transform.CircleCropTransformation
|
import coil.transform.CircleCropTransformation
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.model.getTitle
|
|
||||||
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
||||||
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
|
||||||
@@ -62,9 +61,9 @@ fun alternativeAD(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.progressView.setProgress(item.progress, ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED in payloads)
|
binding.progressView.setPercent(item.progress, ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED in payloads)
|
||||||
binding.chipSource.also { chip ->
|
binding.chipSource.also { chip ->
|
||||||
chip.text = item.manga.source.getTitle(chip.context)
|
chip.text = item.manga.source.title
|
||||||
ImageRequest.Builder(context)
|
ImageRequest.Builder(context)
|
||||||
.data(item.manga.source.faviconUri())
|
.data(item.manga.source.faviconUri())
|
||||||
.lifecycle(lifecycleOwner)
|
.lifecycle(lifecycleOwner)
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||||||
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.exceptions.resolve.SnackbarErrorObserver
|
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||||
import org.koitharu.kotatsu.core.model.getTitle
|
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||||
@@ -96,9 +95,9 @@ class AlternativesActivity : BaseActivity<ActivityAlternativesBinding>(),
|
|||||||
getString(
|
getString(
|
||||||
R.string.migrate_confirmation,
|
R.string.migrate_confirmation,
|
||||||
viewModel.manga.title,
|
viewModel.manga.title,
|
||||||
viewModel.manga.source.getTitle(this),
|
viewModel.manga.source.title,
|
||||||
target.title,
|
target.title,
|
||||||
target.source.getTitle(this),
|
target.source.title,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
|||||||
@@ -15,13 +15,11 @@ import org.koitharu.kotatsu.core.model.chaptersCount
|
|||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||||
import org.koitharu.kotatsu.core.util.ext.call
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
import org.koitharu.kotatsu.core.util.ext.require
|
import org.koitharu.kotatsu.core.util.ext.require
|
||||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
||||||
import org.koitharu.kotatsu.list.domain.ReadingProgress
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||||
@@ -36,8 +34,7 @@ class AlternativesViewModel @Inject constructor(
|
|||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
private val alternativesUseCase: AlternativesUseCase,
|
private val alternativesUseCase: AlternativesUseCase,
|
||||||
private val migrateUseCase: MigrateUseCase,
|
private val migrateUseCase: MigrateUseCase,
|
||||||
private val historyRepository: HistoryRepository,
|
private val extraProvider: ListExtraProvider,
|
||||||
private val settings: AppSettings,
|
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
val manga = savedStateHandle.require<ParcelableManga>(MangaIntent.KEY_MANGA).manga
|
val manga = savedStateHandle.require<ParcelableManga>(MangaIntent.KEY_MANGA).manga
|
||||||
@@ -56,7 +53,7 @@ class AlternativesViewModel @Inject constructor(
|
|||||||
.map {
|
.map {
|
||||||
MangaAlternativeModel(
|
MangaAlternativeModel(
|
||||||
manga = it,
|
manga = it,
|
||||||
progress = getProgress(it.id),
|
progress = extraProvider.getProgress(it.id),
|
||||||
referenceChapters = refCount,
|
referenceChapters = refCount,
|
||||||
)
|
)
|
||||||
}.runningFold<MangaAlternativeModel, List<ListModel>>(listOf(LoadingState)) { acc, item ->
|
}.runningFold<MangaAlternativeModel, List<ListModel>>(listOf(LoadingState)) { acc, item ->
|
||||||
@@ -89,7 +86,13 @@ class AlternativesViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getProgress(mangaId: Long): ReadingProgress? {
|
private suspend fun mapList(list: List<Manga>, refCount: Int): List<MangaAlternativeModel> {
|
||||||
return historyRepository.getProgress(mangaId, settings.progressIndicatorMode)
|
return list.map {
|
||||||
|
MangaAlternativeModel(
|
||||||
|
manga = it,
|
||||||
|
progress = extraProvider.getProgress(it.id),
|
||||||
|
referenceChapters = refCount,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package org.koitharu.kotatsu.alternatives.ui
|
package org.koitharu.kotatsu.alternatives.ui
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.model.chaptersCount
|
import org.koitharu.kotatsu.core.model.chaptersCount
|
||||||
import org.koitharu.kotatsu.list.domain.ReadingProgress
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
|
||||||
data class MangaAlternativeModel(
|
data class MangaAlternativeModel(
|
||||||
val manga: Manga,
|
val manga: Manga,
|
||||||
val progress: ReadingProgress?,
|
val progress: Float,
|
||||||
private val referenceChapters: Int,
|
private val referenceChapters: Int,
|
||||||
) : ListModel {
|
) : ListModel {
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,6 @@ fun bookmarkLargeAD(
|
|||||||
source(item.manga.source)
|
source(item.manga.source)
|
||||||
enqueueWith(coil)
|
enqueueWith(coil)
|
||||||
}
|
}
|
||||||
binding.progressView.setProgress(item.percent, false)
|
binding.progressView.percent = item.percent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,14 +12,13 @@ import androidx.core.graphics.Insets
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import okhttp3.internal.userAgent
|
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
|
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||||
import org.koitharu.kotatsu.core.util.ext.configureForParser
|
import org.koitharu.kotatsu.core.util.ext.configureForParser
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
|
||||||
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
|
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
|
||||||
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
@@ -43,9 +42,10 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
|||||||
setDisplayHomeAsUpEnabled(true)
|
setDisplayHomeAsUpEnabled(true)
|
||||||
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
|
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
|
||||||
}
|
}
|
||||||
val mangaSource = MangaSource(intent?.getStringExtra(EXTRA_SOURCE))
|
val userAgent = intent?.getSerializableExtraCompat<MangaSource>(EXTRA_SOURCE)?.let { source ->
|
||||||
val repository = mangaRepositoryFactory.create(mangaSource) as? ParserMangaRepository
|
val repository = mangaRepositoryFactory.create(source) as? RemoteMangaRepository
|
||||||
repository?.headers?.get(CommonHeaders.USER_AGENT)
|
repository?.headers?.get(CommonHeaders.USER_AGENT)
|
||||||
|
}
|
||||||
viewBinding.webView.configureForParser(userAgent)
|
viewBinding.webView.configureForParser(userAgent)
|
||||||
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
|
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
|
||||||
viewBinding.webView.webViewClient = BrowserClient(this)
|
viewBinding.webView.webViewClient = BrowserClient(this)
|
||||||
@@ -108,10 +108,8 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
if (hasViewBinding()) {
|
viewBinding.webView.stopLoading()
|
||||||
viewBinding.webView.stopLoading()
|
viewBinding.webView.destroy()
|
||||||
viewBinding.webView.destroy()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoadingStateChanged(isLoading: Boolean) {
|
override fun onLoadingStateChanged(isLoading: Boolean) {
|
||||||
@@ -147,7 +145,7 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
|||||||
return Intent(context, BrowserActivity::class.java)
|
return Intent(context, BrowserActivity::class.java)
|
||||||
.setData(Uri.parse(url))
|
.setData(Uri.parse(url))
|
||||||
.putExtra(EXTRA_TITLE, title)
|
.putExtra(EXTRA_TITLE, title)
|
||||||
.putExtra(EXTRA_SOURCE, source?.name)
|
.putExtra(EXTRA_SOURCE, source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ import coil.request.ErrorResult
|
|||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||||
import org.koitharu.kotatsu.core.model.getTitle
|
|
||||||
import org.koitharu.kotatsu.core.model.isNsfw
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
|
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
class CaptchaNotifier(
|
class CaptchaNotifier(
|
||||||
@@ -47,7 +46,7 @@ class CaptchaNotifier(
|
|||||||
.setGroup(GROUP_CAPTCHA)
|
.setGroup(GROUP_CAPTCHA)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setVisibility(
|
.setVisibility(
|
||||||
if (exception.source?.isNsfw() == true) {
|
if (exception.source?.contentType == ContentType.HENTAI) {
|
||||||
NotificationCompat.VISIBILITY_SECRET
|
NotificationCompat.VISIBILITY_SECRET
|
||||||
} else {
|
} else {
|
||||||
NotificationCompat.VISIBILITY_PUBLIC
|
NotificationCompat.VISIBILITY_PUBLIC
|
||||||
@@ -56,7 +55,7 @@ class CaptchaNotifier(
|
|||||||
.setContentText(
|
.setContentText(
|
||||||
context.getString(
|
context.getString(
|
||||||
R.string.captcha_required_summary,
|
R.string.captcha_required_summary,
|
||||||
exception.source?.getTitle(context) ?: context.getString(R.string.app_name),
|
exception.source?.title ?: context.getString(R.string.app_name),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.setContentIntent(PendingIntentCompat.getActivity(context, 0, intent, 0, false))
|
.setContentIntent(PendingIntentCompat.getActivity(context, 0, intent, 0, false))
|
||||||
|
|||||||
@@ -55,11 +55,7 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
|||||||
setDisplayHomeAsUpEnabled(true)
|
setDisplayHomeAsUpEnabled(true)
|
||||||
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
|
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
|
||||||
}
|
}
|
||||||
val url = intent?.dataString
|
val url = intent?.dataString.orEmpty()
|
||||||
if (url.isNullOrEmpty()) {
|
|
||||||
finishAfterTransition()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cfClient = CloudFlareClient(cookieJar, this, url)
|
cfClient = CloudFlareClient(cookieJar, this, url)
|
||||||
viewBinding.webView.configureForParser(intent?.getStringExtra(ARG_UA))
|
viewBinding.webView.configureForParser(intent?.getStringExtra(ARG_UA))
|
||||||
viewBinding.webView.webViewClient = cfClient
|
viewBinding.webView.webViewClient = cfClient
|
||||||
@@ -67,7 +63,12 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
|||||||
onBackPressedDispatcher.addCallback(it)
|
onBackPressedDispatcher.addCallback(it)
|
||||||
}
|
}
|
||||||
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
|
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState != null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (url.isEmpty()) {
|
||||||
|
finishAfterTransition()
|
||||||
|
} else {
|
||||||
onTitleChanged(getString(R.string.loading_), url)
|
onTitleChanged(getString(R.string.loading_), url)
|
||||||
viewBinding.webView.loadUrl(url)
|
viewBinding.webView.loadUrl(url)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.core
|
package org.koitharu.kotatsu.core
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.provider.SearchRecentSuggestions
|
import android.provider.SearchRecentSuggestions
|
||||||
import android.text.Html
|
import android.text.Html
|
||||||
@@ -28,8 +27,8 @@ 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.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
|
import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
|
||||||
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||||
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
|
|
||||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||||
import org.koitharu.kotatsu.core.os.NetworkState
|
import org.koitharu.kotatsu.core.os.NetworkState
|
||||||
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
||||||
@@ -49,7 +48,6 @@ import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
|||||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||||
import org.koitharu.kotatsu.main.domain.CoverRestoreInterceptor
|
import org.koitharu.kotatsu.main.domain.CoverRestoreInterceptor
|
||||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
||||||
import org.koitharu.kotatsu.main.ui.protect.ScreenshotPolicyHelper
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
|
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
|
||||||
import org.koitharu.kotatsu.settings.backup.BackupObserver
|
import org.koitharu.kotatsu.settings.backup.BackupObserver
|
||||||
@@ -111,8 +109,6 @@ interface AppModule {
|
|||||||
.decoderDispatcher(Dispatchers.IO)
|
.decoderDispatcher(Dispatchers.IO)
|
||||||
.transformationDispatcher(Dispatchers.Default)
|
.transformationDispatcher(Dispatchers.Default)
|
||||||
.diskCache(diskCacheFactory)
|
.diskCache(diskCacheFactory)
|
||||||
.respectCacheHeaders(false)
|
|
||||||
.networkObserverEnabled(false)
|
|
||||||
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
|
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
|
||||||
.allowRgb565(context.isLowRamDevice())
|
.allowRgb565(context.isLowRamDevice())
|
||||||
.eventListener(CaptchaNotifier(context))
|
.eventListener(CaptchaNotifier(context))
|
||||||
@@ -156,12 +152,10 @@ interface AppModule {
|
|||||||
appProtectHelper: AppProtectHelper,
|
appProtectHelper: AppProtectHelper,
|
||||||
activityRecreationHandle: ActivityRecreationHandle,
|
activityRecreationHandle: ActivityRecreationHandle,
|
||||||
acraScreenLogger: AcraScreenLogger,
|
acraScreenLogger: AcraScreenLogger,
|
||||||
screenshotPolicyHelper: ScreenshotPolicyHelper,
|
|
||||||
): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf(
|
): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf(
|
||||||
appProtectHelper,
|
appProtectHelper,
|
||||||
activityRecreationHandle,
|
activityRecreationHandle,
|
||||||
acraScreenLogger,
|
acraScreenLogger,
|
||||||
screenshotPolicyHelper,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
|
|||||||
import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault
|
import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault
|
||||||
import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault
|
import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault
|
||||||
import org.koitharu.kotatsu.parsers.util.json.getIntOrDefault
|
import org.koitharu.kotatsu.parsers.util.json.getIntOrDefault
|
||||||
import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault
|
|
||||||
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
|
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
|
||||||
|
|
||||||
class JsonDeserializer(private val json: JSONObject) {
|
class JsonDeserializer(private val json: JSONObject) {
|
||||||
@@ -85,9 +84,6 @@ class JsonDeserializer(private val json: JSONObject) {
|
|||||||
source = json.getString("source"),
|
source = json.getString("source"),
|
||||||
isEnabled = json.getBoolean("enabled"),
|
isEnabled = json.getBoolean("enabled"),
|
||||||
sortKey = json.getInt("sort_key"),
|
sortKey = json.getInt("sort_key"),
|
||||||
addedIn = json.getIntOrDefault("added_in", 0),
|
|
||||||
lastUsedAt = json.getLongOrDefault("used_at", 0L),
|
|
||||||
isPinned = json.getBooleanOrDefault("pinned", false),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
fun toMap(): Map<String, Any?> {
|
fun toMap(): Map<String, Any?> {
|
||||||
|
|||||||
@@ -89,9 +89,6 @@ class JsonSerializer private constructor(private val json: JSONObject) {
|
|||||||
put("source", e.source)
|
put("source", e.source)
|
||||||
put("enabled", e.isEnabled)
|
put("enabled", e.isEnabled)
|
||||||
put("sort_key", e.sortKey)
|
put("sort_key", e.sortKey)
|
||||||
put("added_in", e.addedIn)
|
|
||||||
put("used_at", e.lastUsedAt)
|
|
||||||
put("pinned", e.isPinned)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -16,16 +16,16 @@ class MemoryContentCache @Inject constructor(application: Application) : Compone
|
|||||||
|
|
||||||
private val isLowRam = application.isLowRamDevice()
|
private val isLowRam = application.isLowRamDevice()
|
||||||
|
|
||||||
|
init {
|
||||||
|
application.registerComponentCallbacks(this)
|
||||||
|
}
|
||||||
|
|
||||||
private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(if (isLowRam) 1 else 4, 5, TimeUnit.MINUTES)
|
private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(if (isLowRam) 1 else 4, 5, TimeUnit.MINUTES)
|
||||||
private val pagesCache =
|
private val pagesCache =
|
||||||
ExpiringLruCache<SafeDeferred<List<MangaPage>>>(if (isLowRam) 1 else 4, 10, TimeUnit.MINUTES)
|
ExpiringLruCache<SafeDeferred<List<MangaPage>>>(if (isLowRam) 1 else 4, 10, TimeUnit.MINUTES)
|
||||||
private val relatedMangaCache =
|
private val relatedMangaCache =
|
||||||
ExpiringLruCache<SafeDeferred<List<Manga>>>(if (isLowRam) 1 else 3, 10, TimeUnit.MINUTES)
|
ExpiringLruCache<SafeDeferred<List<Manga>>>(if (isLowRam) 1 else 3, 10, TimeUnit.MINUTES)
|
||||||
|
|
||||||
init {
|
|
||||||
application.registerComponentCallbacks(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getDetails(source: MangaSource, url: String): Manga? {
|
suspend fun getDetails(source: MangaSource, url: String): Manga? {
|
||||||
return detailsCache[Key(source, url)]?.awaitOrNull()
|
return detailsCache[Key(source, url)]?.awaitOrNull()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ import org.koitharu.kotatsu.core.db.migrations.Migration17To18
|
|||||||
import org.koitharu.kotatsu.core.db.migrations.Migration18To19
|
import org.koitharu.kotatsu.core.db.migrations.Migration18To19
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration19To20
|
import org.koitharu.kotatsu.core.db.migrations.Migration19To20
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
|
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration20To21
|
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration21To22
|
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
|
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
|
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration4To5
|
import org.koitharu.kotatsu.core.db.migrations.Migration4To5
|
||||||
@@ -60,7 +58,7 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity
|
|||||||
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
|
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
|
||||||
import org.koitharu.kotatsu.tracker.data.TracksDao
|
import org.koitharu.kotatsu.tracker.data.TracksDao
|
||||||
|
|
||||||
const val DATABASE_VERSION = 22
|
const val DATABASE_VERSION = 20
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
@@ -120,8 +118,6 @@ fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
|
|||||||
Migration17To18(),
|
Migration17To18(),
|
||||||
Migration18To19(),
|
Migration18To19(),
|
||||||
Migration19To20(),
|
Migration19To20(),
|
||||||
Migration20To21(),
|
|
||||||
Migration21To22(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
fun MangaDatabase(context: Context): MangaDatabase = Room
|
fun MangaDatabase(context: Context): MangaDatabase = Room
|
||||||
|
|||||||
@@ -11,26 +11,19 @@ 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.BuildConfig
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
||||||
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
|
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
abstract class MangaSourcesDao {
|
abstract class MangaSourcesDao {
|
||||||
|
|
||||||
@Query("SELECT * FROM sources ORDER BY pinned DESC, sort_key")
|
@Query("SELECT * FROM sources ORDER BY sort_key")
|
||||||
abstract suspend fun findAll(): List<MangaSourceEntity>
|
abstract suspend fun findAll(): List<MangaSourceEntity>
|
||||||
|
|
||||||
@Query("SELECT source FROM sources WHERE enabled = 1")
|
@Query("SELECT source FROM sources WHERE enabled = 1")
|
||||||
abstract suspend fun findAllEnabledNames(): List<String>
|
abstract suspend fun findAllEnabledNames(): List<String>
|
||||||
|
|
||||||
@Query("SELECT * FROM sources WHERE added_in >= :version")
|
@Query("SELECT * FROM sources ORDER BY sort_key")
|
||||||
abstract suspend fun findAllFromVersion(version: Int): List<MangaSourceEntity>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM sources ORDER BY used_at DESC LIMIT :limit")
|
|
||||||
abstract suspend fun findLastUsed(limit: Int): List<MangaSourceEntity>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM sources ORDER BY pinned DESC, sort_key")
|
|
||||||
abstract fun observeAll(): Flow<List<MangaSourceEntity>>
|
abstract fun observeAll(): Flow<List<MangaSourceEntity>>
|
||||||
|
|
||||||
@Query("SELECT enabled FROM sources WHERE source = :source")
|
@Query("SELECT enabled FROM sources WHERE source = :source")
|
||||||
@@ -45,12 +38,6 @@ abstract class MangaSourcesDao {
|
|||||||
@Query("UPDATE sources SET sort_key = :sortKey WHERE source = :source")
|
@Query("UPDATE sources SET sort_key = :sortKey WHERE source = :source")
|
||||||
abstract suspend fun setSortKey(source: String, sortKey: Int)
|
abstract suspend fun setSortKey(source: String, sortKey: Int)
|
||||||
|
|
||||||
@Query("UPDATE sources SET used_at = :value WHERE source = :source")
|
|
||||||
abstract suspend fun setLastUsed(source: String, value: Long)
|
|
||||||
|
|
||||||
@Query("UPDATE sources SET pinned = :isPinned WHERE source = :source")
|
|
||||||
abstract suspend fun setPinned(source: String, isPinned: Boolean)
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
@Transaction
|
@Transaction
|
||||||
abstract suspend fun insertIfAbsent(entries: Collection<MangaSourceEntity>)
|
abstract suspend fun insertIfAbsent(entries: Collection<MangaSourceEntity>)
|
||||||
@@ -58,14 +45,11 @@ abstract class MangaSourcesDao {
|
|||||||
@Upsert
|
@Upsert
|
||||||
abstract suspend fun upsert(entry: MangaSourceEntity)
|
abstract suspend fun upsert(entry: MangaSourceEntity)
|
||||||
|
|
||||||
@Query("SELECT * FROM sources WHERE pinned = 1")
|
|
||||||
abstract suspend fun findAllPinned(): List<MangaSourceEntity>
|
|
||||||
|
|
||||||
fun observeEnabled(order: SourcesSortOrder): Flow<List<MangaSourceEntity>> {
|
fun observeEnabled(order: SourcesSortOrder): Flow<List<MangaSourceEntity>> {
|
||||||
val orderBy = getOrderBy(order)
|
val orderBy = getOrderBy(order)
|
||||||
|
|
||||||
@Language("RoomSql")
|
@Language("RoomSql")
|
||||||
val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY pinned DESC, $orderBy")
|
val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY $orderBy")
|
||||||
return observeImpl(query)
|
return observeImpl(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +57,7 @@ abstract class MangaSourcesDao {
|
|||||||
val orderBy = getOrderBy(order)
|
val orderBy = getOrderBy(order)
|
||||||
|
|
||||||
@Language("RoomSql")
|
@Language("RoomSql")
|
||||||
val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY pinned DESC, $orderBy")
|
val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY $orderBy")
|
||||||
return findAllImpl(query)
|
return findAllImpl(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,9 +68,6 @@ abstract class MangaSourcesDao {
|
|||||||
source = source,
|
source = source,
|
||||||
isEnabled = isEnabled,
|
isEnabled = isEnabled,
|
||||||
sortKey = getMaxSortKey() + 1,
|
sortKey = getMaxSortKey() + 1,
|
||||||
addedIn = BuildConfig.VERSION_CODE,
|
|
||||||
lastUsedAt = 0,
|
|
||||||
isPinned = false,
|
|
||||||
)
|
)
|
||||||
upsert(entity)
|
upsert(entity)
|
||||||
}
|
}
|
||||||
@@ -105,6 +86,5 @@ abstract class MangaSourcesDao {
|
|||||||
SourcesSortOrder.ALPHABETIC -> "source ASC"
|
SourcesSortOrder.ALPHABETIC -> "source ASC"
|
||||||
SourcesSortOrder.POPULARITY -> "(SELECT COUNT(*) FROM manga WHERE source = sources.source) DESC"
|
SourcesSortOrder.POPULARITY -> "(SELECT COUNT(*) FROM manga WHERE source = sources.source) DESC"
|
||||||
SourcesSortOrder.MANUAL -> "sort_key ASC"
|
SourcesSortOrder.MANUAL -> "sort_key ASC"
|
||||||
SourcesSortOrder.LAST_USED -> "used_at DESC"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,4 @@ data class MangaSourceEntity(
|
|||||||
val source: String,
|
val source: String,
|
||||||
@ColumnInfo(name = "enabled") val isEnabled: Boolean,
|
@ColumnInfo(name = "enabled") val isEnabled: Boolean,
|
||||||
@ColumnInfo(name = "sort_key", index = true) val sortKey: Int,
|
@ColumnInfo(name = "sort_key", index = true) val sortKey: Int,
|
||||||
@ColumnInfo(name = "added_in") val addedIn: Int,
|
|
||||||
@ColumnInfo(name = "used_at") val lastUsedAt: Long,
|
|
||||||
@ColumnInfo(name = "pinned") val isPinned: Boolean,
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import android.content.Context
|
|||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.room.migration.Migration
|
import androidx.room.migration.Migration
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
class Migration16To17(context: Context) : Migration(16, 17) {
|
class Migration16To17(context: Context) : Migration(16, 17) {
|
||||||
|
|
||||||
@@ -15,8 +15,11 @@ class Migration16To17(context: Context) : Migration(16, 17) {
|
|||||||
db.execSQL("CREATE INDEX `index_sources_sort_key` ON `sources` (`sort_key`)")
|
db.execSQL("CREATE INDEX `index_sources_sort_key` ON `sources` (`sort_key`)")
|
||||||
val hiddenSources = prefs.getStringSet("sources_hidden", null).orEmpty()
|
val hiddenSources = prefs.getStringSet("sources_hidden", null).orEmpty()
|
||||||
val order = prefs.getString("sources_order_2", null)?.split('|').orEmpty()
|
val order = prefs.getString("sources_order_2", null)?.split('|').orEmpty()
|
||||||
val sources = MangaParserSource.entries
|
val sources = MangaSource.entries
|
||||||
for (source in sources) {
|
for (source in sources) {
|
||||||
|
if (source == MangaSource.LOCAL) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
val name = source.name
|
val name = source.name
|
||||||
val isHidden = name in hiddenSources
|
val isHidden = name in hiddenSources
|
||||||
var sortKey = order.indexOf(name)
|
var sortKey = order.indexOf(name)
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.db.migrations
|
|
||||||
|
|
||||||
import androidx.room.migration.Migration
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
|
||||||
|
|
||||||
class Migration20To21 : Migration(20, 21) {
|
|
||||||
|
|
||||||
override fun migrate(db: SupportSQLiteDatabase) {
|
|
||||||
db.execSQL("ALTER TABLE tracks ADD COLUMN `last_error` TEXT DEFAULT NULL")
|
|
||||||
db.execSQL("ALTER TABLE sources ADD COLUMN `added_in` INTEGER NOT NULL DEFAULT 0")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.db.migrations
|
|
||||||
|
|
||||||
import androidx.room.migration.Migration
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
|
||||||
|
|
||||||
class Migration21To22 : Migration(21, 22) {
|
|
||||||
|
|
||||||
override fun migrate(db: SupportSQLiteDatabase) {
|
|
||||||
db.execSQL("ALTER TABLE sources ADD COLUMN `used_at` INTEGER NOT NULL DEFAULT 0")
|
|
||||||
db.execSQL("ALTER TABLE sources ADD COLUMN `pinned` INTEGER NOT NULL DEFAULT 0")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.exceptions
|
|
||||||
|
|
||||||
class IncompatiblePluginException(
|
|
||||||
val name: String?,
|
|
||||||
cause: Throwable?,
|
|
||||||
) : RuntimeException(cause)
|
|
||||||
@@ -1,48 +1,36 @@
|
|||||||
package org.koitharu.kotatsu.core.exceptions.resolve
|
package org.koitharu.kotatsu.core.exceptions.resolve
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.result.ActivityResultCallback
|
import androidx.activity.result.ActivityResultCallback
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.collection.MutableScatterMap
|
import androidx.collection.ArrayMap
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import dagger.hilt.android.EntryPointAccessors
|
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity
|
import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity
|
||||||
import org.koitharu.kotatsu.browser.BrowserActivity
|
import org.koitharu.kotatsu.browser.BrowserActivity
|
||||||
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity
|
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity
|
||||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||||
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
|
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.core.ui.BaseActivity.BaseActivityEntryPoint
|
|
||||||
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
|
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
|
||||||
import org.koitharu.kotatsu.core.util.TaggedActivityResult
|
import org.koitharu.kotatsu.core.util.TaggedActivityResult
|
||||||
import org.koitharu.kotatsu.core.util.ext.findActivity
|
|
||||||
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
||||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
|
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
|
||||||
import java.security.cert.CertPathValidatorException
|
|
||||||
import javax.net.ssl.SSLException
|
|
||||||
import kotlin.coroutines.Continuation
|
import kotlin.coroutines.Continuation
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
|
class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
|
||||||
|
|
||||||
private val continuations = MutableScatterMap<String, Continuation<Boolean>>(1)
|
private val continuations = ArrayMap<String, Continuation<Boolean>>(1)
|
||||||
private val activity: FragmentActivity?
|
private val activity: FragmentActivity?
|
||||||
private val fragment: Fragment?
|
private val fragment: Fragment?
|
||||||
private val sourceAuthContract: ActivityResultLauncher<MangaSource>
|
private val sourceAuthContract: ActivityResultLauncher<MangaSource>
|
||||||
private val cloudflareContract: ActivityResultLauncher<CloudFlareProtectedException>
|
private val cloudflareContract: ActivityResultLauncher<CloudFlareProtectedException>
|
||||||
|
|
||||||
val context: Context?
|
|
||||||
get() = activity ?: fragment?.context
|
|
||||||
|
|
||||||
constructor(activity: FragmentActivity) {
|
constructor(activity: FragmentActivity) {
|
||||||
this.activity = activity
|
this.activity = activity
|
||||||
fragment = null
|
fragment = null
|
||||||
@@ -68,12 +56,6 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
|
|||||||
suspend fun resolve(e: Throwable): Boolean = when (e) {
|
suspend fun resolve(e: Throwable): Boolean = when (e) {
|
||||||
is CloudFlareProtectedException -> resolveCF(e)
|
is CloudFlareProtectedException -> resolveCF(e)
|
||||||
is AuthRequiredException -> resolveAuthException(e.source)
|
is AuthRequiredException -> resolveAuthException(e.source)
|
||||||
is SSLException,
|
|
||||||
is CertPathValidatorException -> {
|
|
||||||
showSslErrorDialog()
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
is NotFoundException -> {
|
is NotFoundException -> {
|
||||||
openInBrowser(e.url)
|
openInBrowser(e.url)
|
||||||
false
|
false
|
||||||
@@ -98,37 +80,13 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun openInBrowser(url: String) {
|
private fun openInBrowser(url: String) {
|
||||||
context?.run {
|
val context = activity ?: fragment?.activity ?: return
|
||||||
startActivity(BrowserActivity.newIntent(this, url, null, null))
|
context.startActivity(BrowserActivity.newIntent(context, url, null, null))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openAlternatives(manga: Manga) {
|
private fun openAlternatives(manga: Manga) {
|
||||||
context?.run {
|
val context = activity ?: fragment?.activity ?: return
|
||||||
startActivity(AlternativesActivity.newIntent(this, manga))
|
context.startActivity(AlternativesActivity.newIntent(context, manga))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showSslErrorDialog() {
|
|
||||||
val ctx = context ?: return
|
|
||||||
val settings = getAppSettings(ctx)
|
|
||||||
if (settings.isSSLBypassEnabled) {
|
|
||||||
Toast.makeText(ctx, R.string.operation_not_supported, Toast.LENGTH_SHORT).show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
MaterialAlertDialogBuilder(ctx)
|
|
||||||
.setTitle(R.string.ignore_ssl_errors)
|
|
||||||
.setMessage(R.string.ignore_ssl_errors_summary)
|
|
||||||
.setPositiveButton(R.string.apply) { _, _ ->
|
|
||||||
settings.isSSLBypassEnabled = true
|
|
||||||
Toast.makeText(ctx, R.string.settings_apply_restart_required, Toast.LENGTH_SHORT).show()
|
|
||||||
ctx.findActivity()?.finishAffinity()
|
|
||||||
}.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getAppSettings(context: Context): AppSettings {
|
|
||||||
return EntryPointAccessors.fromApplication<BaseActivityEntryPoint>(context).settings
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFragmentManager() = checkNotNull(fragment?.childFragmentManager ?: activity?.supportFragmentManager)
|
private fun getFragmentManager() = checkNotNull(fragment?.childFragmentManager ?: activity?.supportFragmentManager)
|
||||||
@@ -141,9 +99,6 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
|
|||||||
is AuthRequiredException -> R.string.sign_in
|
is AuthRequiredException -> R.string.sign_in
|
||||||
is NotFoundException -> if (e.url.isNotEmpty()) R.string.open_in_browser else 0
|
is NotFoundException -> if (e.url.isNotEmpty()) R.string.open_in_browser else 0
|
||||||
is UnsupportedSourceException -> if (e.manga != null) R.string.alternatives else 0
|
is UnsupportedSourceException -> if (e.manga != null) R.string.alternatives else 0
|
||||||
is SSLException,
|
|
||||||
is CertPathValidatorException -> R.string.fix
|
|
||||||
|
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
|||||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||||
import org.koitharu.kotatsu.parsers.util.formatSimple
|
import org.koitharu.kotatsu.parsers.util.formatSimple
|
||||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||||
@@ -108,7 +109,7 @@ fun Manga.getPreferredBranch(history: MangaHistory?): String? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val Manga.isLocal: Boolean
|
val Manga.isLocal: Boolean
|
||||||
get() = source == LocalMangaSource
|
get() = source == MangaSource.LOCAL
|
||||||
|
|
||||||
val Manga.appUrl: Uri
|
val Manga.appUrl: Uri
|
||||||
get() = Uri.parse("https://kotatsu.app/manga").buildUpon()
|
get() = Uri.parse("https://kotatsu.app/manga").buildUpon()
|
||||||
|
|||||||
@@ -7,47 +7,26 @@ import android.text.style.ForegroundColorSpan
|
|||||||
import android.text.style.RelativeSizeSpan
|
import android.text.style.RelativeSizeSpan
|
||||||
import android.text.style.SuperscriptSpan
|
import android.text.style.SuperscriptSpan
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.core.text.buildSpannedString
|
||||||
import androidx.core.text.inSpans
|
import androidx.core.text.inSpans
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.parser.external.ExternalMangaSource
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.getDisplayName
|
import org.koitharu.kotatsu.core.util.ext.getDisplayName
|
||||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||||
import org.koitharu.kotatsu.core.util.ext.toLocale
|
import org.koitharu.kotatsu.core.util.ext.toLocale
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.splitTwoParts
|
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||||
|
import java.util.Locale
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
data object LocalMangaSource : MangaSource {
|
fun MangaSource(name: String): MangaSource {
|
||||||
override val name = "LOCAL"
|
MangaSource.entries.forEach {
|
||||||
}
|
|
||||||
|
|
||||||
data object UnknownMangaSource : MangaSource {
|
|
||||||
override val name = "UNKNOWN"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MangaSource(name: String?): MangaSource {
|
|
||||||
when (name ?: return UnknownMangaSource) {
|
|
||||||
UnknownMangaSource.name -> return UnknownMangaSource
|
|
||||||
|
|
||||||
LocalMangaSource.name -> return LocalMangaSource
|
|
||||||
}
|
|
||||||
if (name.startsWith("content:")) {
|
|
||||||
val parts = name.substringAfter(':').splitTwoParts('/') ?: return UnknownMangaSource
|
|
||||||
return ExternalMangaSource(packageName = parts.first, authority = parts.second)
|
|
||||||
}
|
|
||||||
MangaParserSource.entries.forEach {
|
|
||||||
if (it.name == name) return it
|
if (it.name == name) return it
|
||||||
}
|
}
|
||||||
return UnknownMangaSource
|
return MangaSource.DUMMY
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MangaSource.isNsfw(): Boolean = when (this) {
|
fun MangaSource.isNsfw() = contentType == ContentType.HENTAI
|
||||||
is MangaSourceInfo -> mangaSource.isNsfw()
|
|
||||||
is MangaParserSource -> contentType == ContentType.HENTAI
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
@get:StringRes
|
@get:StringRes
|
||||||
val ContentType.titleResId
|
val ContentType.titleResId
|
||||||
@@ -58,28 +37,23 @@ val ContentType.titleResId
|
|||||||
ContentType.OTHER -> R.string.content_type_other
|
ContentType.OTHER -> R.string.content_type_other
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MangaSource.getSummary(context: Context): String? = when (this) {
|
fun MangaSource.getSummary(context: Context): String {
|
||||||
is MangaSourceInfo -> mangaSource.getSummary(context)
|
val type = context.getString(contentType.titleResId)
|
||||||
is MangaParserSource -> {
|
val locale = locale?.toLocale().getDisplayName(context)
|
||||||
val type = context.getString(contentType.titleResId)
|
return context.getString(R.string.source_summary_pattern, type, locale)
|
||||||
val locale = locale.toLocale().getDisplayName(context)
|
}
|
||||||
context.getString(R.string.source_summary_pattern, type, locale)
|
|
||||||
|
fun MangaSource.getTitle(context: Context): CharSequence = if (isNsfw()) {
|
||||||
|
buildSpannedString {
|
||||||
|
append(title)
|
||||||
|
append(' ')
|
||||||
|
appendNsfwLabel(context)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
is ExternalMangaSource -> context.getString(R.string.external_source)
|
title
|
||||||
|
|
||||||
else -> null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MangaSource.getTitle(context: Context): String = when (this) {
|
private fun SpannableStringBuilder.appendNsfwLabel(context: Context) = inSpans(
|
||||||
is MangaSourceInfo -> mangaSource.getTitle(context)
|
|
||||||
is MangaParserSource -> title
|
|
||||||
LocalMangaSource -> context.getString(R.string.local_storage)
|
|
||||||
is ExternalMangaSource -> resolveName(context)
|
|
||||||
else -> context.getString(R.string.unknown)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun SpannableStringBuilder.appendNsfwLabel(context: Context) = inSpans(
|
|
||||||
ForegroundColorSpan(context.getThemeColor(materialR.attr.colorError, Color.RED)),
|
ForegroundColorSpan(context.getThemeColor(materialR.attr.colorError, Color.RED)),
|
||||||
RelativeSizeSpan(0.74f),
|
RelativeSizeSpan(0.74f),
|
||||||
SuperscriptSpan(),
|
SuperscriptSpan(),
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.model
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
|
|
||||||
data class MangaSourceInfo(
|
|
||||||
val mangaSource: MangaSource,
|
|
||||||
val isEnabled: Boolean,
|
|
||||||
val isPinned: Boolean,
|
|
||||||
) : MangaSource by mangaSource
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.model.parcelable
|
|
||||||
|
|
||||||
import android.os.Parcel
|
|
||||||
import kotlinx.parcelize.Parceler
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
|
|
||||||
class MangaSourceParceler : Parceler<MangaSource> {
|
|
||||||
|
|
||||||
override fun create(parcel: Parcel): MangaSource = MangaSource(parcel.readString())
|
|
||||||
|
|
||||||
override fun MangaSource.write(parcel: Parcel, flags: Int) {
|
|
||||||
parcel.writeString(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,8 +4,9 @@ import android.os.Parcel
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.parcelize.Parceler
|
import kotlinx.parcelize.Parceler
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ParcelableChapter(
|
data class ParcelableChapter(
|
||||||
@@ -24,8 +25,8 @@ data class ParcelableChapter(
|
|||||||
scanlator = parcel.readString(),
|
scanlator = parcel.readString(),
|
||||||
uploadDate = parcel.readLong(),
|
uploadDate = parcel.readLong(),
|
||||||
branch = parcel.readString(),
|
branch = parcel.readString(),
|
||||||
source = MangaSource(parcel.readString()),
|
source = parcel.readSerializableCompat() ?: MangaSource.DUMMY,
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun ParcelableChapter.write(parcel: Parcel, flags: Int) = with(chapter) {
|
override fun ParcelableChapter.write(parcel: Parcel, flags: Int) = with(chapter) {
|
||||||
@@ -37,7 +38,7 @@ data class ParcelableChapter(
|
|||||||
parcel.writeString(scanlator)
|
parcel.writeString(scanlator)
|
||||||
parcel.writeLong(uploadDate)
|
parcel.writeLong(uploadDate)
|
||||||
parcel.writeString(branch)
|
parcel.writeString(branch)
|
||||||
parcel.writeString(source.name)
|
parcel.writeSerializable(source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.os.Parcelable
|
|||||||
import androidx.core.os.ParcelCompat
|
import androidx.core.os.ParcelCompat
|
||||||
import kotlinx.parcelize.Parceler
|
import kotlinx.parcelize.Parceler
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.readParcelableCompat
|
import org.koitharu.kotatsu.core.util.ext.readParcelableCompat
|
||||||
import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
|
import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
@@ -31,7 +30,7 @@ data class ParcelableManga(
|
|||||||
parcel.writeParcelable(ParcelableMangaTags(tags), flags)
|
parcel.writeParcelable(ParcelableMangaTags(tags), flags)
|
||||||
parcel.writeSerializable(state)
|
parcel.writeSerializable(state)
|
||||||
parcel.writeString(author)
|
parcel.writeString(author)
|
||||||
parcel.writeString(source.name)
|
parcel.writeSerializable(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun create(parcel: Parcel) = ParcelableManga(
|
override fun create(parcel: Parcel) = ParcelableManga(
|
||||||
@@ -50,8 +49,8 @@ data class ParcelableManga(
|
|||||||
state = parcel.readSerializableCompat(),
|
state = parcel.readSerializableCompat(),
|
||||||
author = parcel.readString(),
|
author = parcel.readString(),
|
||||||
chapters = null,
|
chapters = null,
|
||||||
source = MangaSource(parcel.readString()),
|
source = requireNotNull(parcel.readSerializableCompat()),
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import android.os.Parcelable
|
|||||||
import kotlinx.parcelize.Parceler
|
import kotlinx.parcelize.Parceler
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import kotlinx.parcelize.TypeParceler
|
import kotlinx.parcelize.TypeParceler
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
|
||||||
object MangaPageParceler : Parceler<MangaPage> {
|
object MangaPageParceler : Parceler<MangaPage> {
|
||||||
@@ -13,14 +13,14 @@ object MangaPageParceler : Parceler<MangaPage> {
|
|||||||
id = parcel.readLong(),
|
id = parcel.readLong(),
|
||||||
url = requireNotNull(parcel.readString()),
|
url = requireNotNull(parcel.readString()),
|
||||||
preview = parcel.readString(),
|
preview = parcel.readString(),
|
||||||
source = MangaSource(parcel.readString()),
|
source = requireNotNull(parcel.readSerializableCompat()),
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun MangaPage.write(parcel: Parcel, flags: Int) {
|
override fun MangaPage.write(parcel: Parcel, flags: Int) {
|
||||||
parcel.writeLong(id)
|
parcel.writeLong(id)
|
||||||
parcel.writeString(url)
|
parcel.writeString(url)
|
||||||
parcel.writeString(preview)
|
parcel.writeString(preview)
|
||||||
parcel.writeString(source.name)
|
parcel.writeSerializable(source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,20 +5,20 @@ import android.os.Parcelable
|
|||||||
import kotlinx.parcelize.Parceler
|
import kotlinx.parcelize.Parceler
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import kotlinx.parcelize.TypeParceler
|
import kotlinx.parcelize.TypeParceler
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
|
||||||
object MangaTagParceler : Parceler<MangaTag> {
|
object MangaTagParceler : Parceler<MangaTag> {
|
||||||
override fun create(parcel: Parcel) = MangaTag(
|
override fun create(parcel: Parcel) = MangaTag(
|
||||||
title = requireNotNull(parcel.readString()),
|
title = requireNotNull(parcel.readString()),
|
||||||
key = requireNotNull(parcel.readString()),
|
key = requireNotNull(parcel.readString()),
|
||||||
source = MangaSource(parcel.readString()),
|
source = requireNotNull(parcel.readSerializableCompat()),
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun MangaTag.write(parcel: Parcel, flags: Int) {
|
override fun MangaTag.write(parcel: Parcel, flags: Int) {
|
||||||
parcel.writeString(title)
|
parcel.writeString(title)
|
||||||
parcel.writeString(key)
|
parcel.writeString(key)
|
||||||
parcel.writeString(source.name)
|
parcel.writeSerializable(source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,18 +4,15 @@ import android.util.Log
|
|||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Interceptor.Chain
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okio.IOException
|
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
|
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.mergeWith
|
import org.koitharu.kotatsu.parsers.util.mergeWith
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
|
||||||
import java.net.IDN
|
import java.net.IDN
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
@@ -26,11 +23,11 @@ class CommonHeadersInterceptor @Inject constructor(
|
|||||||
private val mangaLoaderContextLazy: Lazy<MangaLoaderContextImpl>,
|
private val mangaLoaderContextLazy: Lazy<MangaLoaderContextImpl>,
|
||||||
) : Interceptor {
|
) : Interceptor {
|
||||||
|
|
||||||
override fun intercept(chain: Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val request = chain.request()
|
val request = chain.request()
|
||||||
val source = request.tag(MangaSource::class.java)
|
val source = request.tag(MangaSource::class.java)
|
||||||
val repository = if (source != null) {
|
val repository = if (source != null) {
|
||||||
mangaRepositoryFactoryLazy.get().create(source) as? ParserMangaRepository
|
mangaRepositoryFactoryLazy.get().create(source) as? RemoteMangaRepository
|
||||||
} else {
|
} else {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Log.w("Http", "Request without source tag: ${request.url}")
|
Log.w("Http", "Request without source tag: ${request.url}")
|
||||||
@@ -49,7 +46,7 @@ class CommonHeadersInterceptor @Inject constructor(
|
|||||||
headersBuilder.trySet(CommonHeaders.REFERER, "https://$idn/")
|
headersBuilder.trySet(CommonHeaders.REFERER, "https://$idn/")
|
||||||
}
|
}
|
||||||
val newRequest = request.newBuilder().headers(headersBuilder.build()).build()
|
val newRequest = request.newBuilder().headers(headersBuilder.build()).build()
|
||||||
return repository?.interceptSafe(ProxyChain(chain, newRequest)) ?: chain.proceed(newRequest)
|
return repository?.intercept(ProxyChain(chain, newRequest)) ?: chain.proceed(newRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Headers.Builder.trySet(name: String, value: String) = try {
|
private fun Headers.Builder.trySet(name: String, value: String) = try {
|
||||||
@@ -58,21 +55,10 @@ class CommonHeadersInterceptor @Inject constructor(
|
|||||||
e.printStackTraceDebug()
|
e.printStackTraceDebug()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Interceptor.interceptSafe(chain: Chain): Response = runCatchingCancellable {
|
|
||||||
intercept(chain)
|
|
||||||
}.getOrElse { e ->
|
|
||||||
if (e is IOException) {
|
|
||||||
throw e
|
|
||||||
} else {
|
|
||||||
// only IOException can be safely thrown from an Interceptor
|
|
||||||
throw IOException("Error in interceptor: ${e.message}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ProxyChain(
|
private class ProxyChain(
|
||||||
private val delegate: Chain,
|
private val delegate: Interceptor.Chain,
|
||||||
private val request: Request,
|
private val request: Request,
|
||||||
) : Chain by delegate {
|
) : Interceptor.Chain by delegate {
|
||||||
|
|
||||||
override fun request(): Request = request
|
override fun request(): Request = request
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,11 +83,6 @@ class DoHManager(
|
|||||||
tryGetByIp("2a10:50c0::2:ff"),
|
tryGetByIp("2a10:50c0::2:ff"),
|
||||||
),
|
),
|
||||||
).build()
|
).build()
|
||||||
|
|
||||||
DoHProvider.ZERO_MS -> DnsOverHttps.Builder().client(bootstrapClient)
|
|
||||||
.url("https://2ca4h4crra.cloudflare-gateway.com/dns-query".toHttpUrl())
|
|
||||||
.resolvePublicAddresses(true)
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tryGetByIp(ip: String): InetAddress? = try {
|
private fun tryGetByIp(ip: String): InetAddress? = try {
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ package org.koitharu.kotatsu.core.network
|
|||||||
|
|
||||||
enum class DoHProvider {
|
enum class DoHProvider {
|
||||||
|
|
||||||
NONE, GOOGLE, CLOUDFLARE, ADGUARD, ZERO_MS
|
NONE, GOOGLE, CLOUDFLARE, ADGUARD
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.collection.ArraySet
|
||||||
|
import coil.intercept.Interceptor
|
||||||
|
import coil.request.ErrorResult
|
||||||
|
import coil.request.ImageResult
|
||||||
|
import coil.request.SuccessResult
|
||||||
|
import coil.size.Dimension
|
||||||
|
import coil.size.isOriginal
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.ensureSuccess
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.isHttpOrHttps
|
||||||
|
import org.koitharu.kotatsu.parsers.util.await
|
||||||
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
|
import java.util.Collections
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class ImageProxyInterceptor @Inject constructor(
|
||||||
|
private val settings: AppSettings,
|
||||||
|
) : Interceptor {
|
||||||
|
|
||||||
|
private val blacklist = Collections.synchronizedSet(ArraySet<String>())
|
||||||
|
|
||||||
|
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||||
|
val request = chain.request
|
||||||
|
if (!settings.isImagesProxyEnabled) {
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
val url: HttpUrl? = when (val data = request.data) {
|
||||||
|
is HttpUrl -> data
|
||||||
|
is String -> data.toHttpUrlOrNull()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (url == null || !url.isHttpOrHttps || url.host in blacklist) {
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
val newUrl = HttpUrl.Builder()
|
||||||
|
.scheme("https")
|
||||||
|
.host("wsrv.nl")
|
||||||
|
.addQueryParameter("url", url.toString())
|
||||||
|
.addQueryParameter("we", null)
|
||||||
|
val size = request.sizeResolver.size()
|
||||||
|
if (!size.isOriginal) {
|
||||||
|
newUrl.addQueryParameter("crop", "cover")
|
||||||
|
(size.height as? Dimension.Pixels)?.let { newUrl.addQueryParameter("h", it.toString()) }
|
||||||
|
(size.width as? Dimension.Pixels)?.let { newUrl.addQueryParameter("w", it.toString()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val newRequest = request.newBuilder()
|
||||||
|
.data(newUrl.build())
|
||||||
|
.build()
|
||||||
|
val result = chain.proceed(newRequest)
|
||||||
|
return if (result is SuccessResult) {
|
||||||
|
result
|
||||||
|
} else {
|
||||||
|
logDebug((result as? ErrorResult)?.throwable)
|
||||||
|
chain.proceed(request).also {
|
||||||
|
if (it is SuccessResult) {
|
||||||
|
blacklist.add(url.host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response {
|
||||||
|
if (!settings.isImagesProxyEnabled) {
|
||||||
|
return okHttp.newCall(request).await()
|
||||||
|
}
|
||||||
|
val sourceUrl = request.url
|
||||||
|
val targetUrl = HttpUrl.Builder()
|
||||||
|
.scheme("https")
|
||||||
|
.host("wsrv.nl")
|
||||||
|
.addQueryParameter("url", sourceUrl.toString())
|
||||||
|
.addQueryParameter("we", null)
|
||||||
|
val newRequest = request.newBuilder()
|
||||||
|
.url(targetUrl.build())
|
||||||
|
.build()
|
||||||
|
return runCatchingCancellable {
|
||||||
|
okHttp.doCall(newRequest)
|
||||||
|
}.recover {
|
||||||
|
logDebug(it)
|
||||||
|
okHttp.doCall(request).also {
|
||||||
|
blacklist.add(sourceUrl.host)
|
||||||
|
}
|
||||||
|
}.getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun OkHttpClient.doCall(request: Request): Response {
|
||||||
|
return newCall(request).await().ensureSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logDebug(e: Throwable?) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.w("ImageProxy", e.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,9 +13,8 @@ import okhttp3.internal.canParseAsIpAddress
|
|||||||
import okhttp3.internal.closeQuietly
|
import okhttp3.internal.closeQuietly
|
||||||
import okhttp3.internal.publicsuffix.PublicSuffixDatabase
|
import okhttp3.internal.publicsuffix.PublicSuffixDatabase
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
|
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import java.util.EnumMap
|
import java.util.EnumMap
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -27,8 +26,8 @@ class MirrorSwitchInterceptor @Inject constructor(
|
|||||||
private val settings: AppSettings,
|
private val settings: AppSettings,
|
||||||
) : Interceptor {
|
) : Interceptor {
|
||||||
|
|
||||||
private val locks = EnumMap<MangaParserSource, Any>(MangaParserSource::class.java)
|
private val locks = EnumMap<MangaSource, Any>(MangaSource::class.java)
|
||||||
private val blacklist = EnumMap<MangaParserSource, MutableSet<String>>(MangaParserSource::class.java)
|
private val blacklist = EnumMap<MangaSource, MutableSet<String>>(MangaSource::class.java)
|
||||||
|
|
||||||
val isEnabled: Boolean
|
val isEnabled: Boolean
|
||||||
get() = settings.isMirrorSwitchingAvailable
|
get() = settings.isMirrorSwitchingAvailable
|
||||||
@@ -54,7 +53,7 @@ class MirrorSwitchInterceptor @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun trySwitchMirror(repository: ParserMangaRepository): Boolean = runInterruptible(Dispatchers.Default) {
|
suspend fun trySwitchMirror(repository: RemoteMangaRepository): Boolean = runInterruptible(Dispatchers.Default) {
|
||||||
if (!isEnabled) {
|
if (!isEnabled) {
|
||||||
return@runInterruptible false
|
return@runInterruptible false
|
||||||
}
|
}
|
||||||
@@ -76,14 +75,14 @@ class MirrorSwitchInterceptor @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun rollback(repository: ParserMangaRepository, oldMirror: String) = synchronized(obtainLock(repository.source)) {
|
fun rollback(repository: RemoteMangaRepository, oldMirror: String) = synchronized(obtainLock(repository.source)) {
|
||||||
blacklist[repository.source]?.remove(oldMirror)
|
blacklist[repository.source]?.remove(oldMirror)
|
||||||
repository.domain = oldMirror
|
repository.domain = oldMirror
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun trySwitchMirror(request: Request, chain: Interceptor.Chain): Response? {
|
private fun trySwitchMirror(request: Request, chain: Interceptor.Chain): Response? {
|
||||||
val source = request.tag(MangaSource::class.java) ?: return null
|
val source = request.tag(MangaSource::class.java) ?: return null
|
||||||
val repository = mangaRepositoryFactoryLazy.get().create(source) as? ParserMangaRepository ?: return null
|
val repository = mangaRepositoryFactoryLazy.get().create(source) as? RemoteMangaRepository ?: return null
|
||||||
val mirrors = repository.getAvailableMirrors()
|
val mirrors = repository.getAvailableMirrors()
|
||||||
if (mirrors.isEmpty()) {
|
if (mirrors.isEmpty()) {
|
||||||
return null
|
return null
|
||||||
@@ -94,7 +93,7 @@ class MirrorSwitchInterceptor @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun tryMirrors(
|
private fun tryMirrors(
|
||||||
repository: ParserMangaRepository,
|
repository: RemoteMangaRepository,
|
||||||
mirrors: List<String>,
|
mirrors: List<String>,
|
||||||
chain: Interceptor.Chain,
|
chain: Interceptor.Chain,
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -146,15 +145,15 @@ class MirrorSwitchInterceptor @Inject constructor(
|
|||||||
return source().readByteArray().toResponseBody(contentType())
|
return source().readByteArray().toResponseBody(contentType())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun obtainLock(source: MangaParserSource): Any = locks.getOrPut(source) {
|
private fun obtainLock(source: MangaSource): Any = locks.getOrPut(source) {
|
||||||
Any()
|
Any()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isBlacklisted(source: MangaParserSource, domain: String): Boolean {
|
private fun isBlacklisted(source: MangaSource, domain: String): Boolean {
|
||||||
return blacklist[source]?.contains(domain) == true
|
return blacklist[source]?.contains(domain) == true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addToBlacklist(source: MangaParserSource, domain: String) {
|
private fun addToBlacklist(source: MangaSource, domain: String) {
|
||||||
blacklist.getOrPut(source) {
|
blacklist.getOrPut(source) {
|
||||||
ArraySet(2)
|
ArraySet(2)
|
||||||
}.add(domain)
|
}.add(domain)
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import org.koitharu.kotatsu.BuildConfig
|
|||||||
import org.koitharu.kotatsu.core.network.cookies.AndroidCookieJar
|
import org.koitharu.kotatsu.core.network.cookies.AndroidCookieJar
|
||||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||||
import org.koitharu.kotatsu.core.network.cookies.PreferencesCookieJar
|
import org.koitharu.kotatsu.core.network.cookies.PreferencesCookieJar
|
||||||
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
|
|
||||||
import org.koitharu.kotatsu.core.network.imageproxy.RealImageProxyInterceptor
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.util.ext.assertNotInMainThread
|
import org.koitharu.kotatsu.core.util.ext.assertNotInMainThread
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||||
@@ -31,9 +29,6 @@ interface NetworkModule {
|
|||||||
@Binds
|
@Binds
|
||||||
fun bindCookieJar(androidCookieJar: MutableCookieJar): CookieJar
|
fun bindCookieJar(androidCookieJar: MutableCookieJar): CookieJar
|
||||||
|
|
||||||
@Binds
|
|
||||||
fun bindImageProxyInterceptor(impl: RealImageProxyInterceptor): ImageProxyInterceptor
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.network.imageproxy
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.collection.ArraySet
|
|
||||||
import coil.intercept.Interceptor
|
|
||||||
import coil.network.HttpException
|
|
||||||
import coil.request.ErrorResult
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import coil.request.ImageResult
|
|
||||||
import coil.request.SuccessResult
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.HttpStatusException
|
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.ensureSuccess
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.isHttpOrHttps
|
|
||||||
import org.koitharu.kotatsu.parsers.util.await
|
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import java.util.Collections
|
|
||||||
|
|
||||||
abstract class BaseImageProxyInterceptor : ImageProxyInterceptor {
|
|
||||||
|
|
||||||
private val blacklist = Collections.synchronizedSet(ArraySet<String>())
|
|
||||||
|
|
||||||
final override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
|
||||||
val request = chain.request
|
|
||||||
val url: HttpUrl? = when (val data = request.data) {
|
|
||||||
is HttpUrl -> data
|
|
||||||
is String -> data.toHttpUrlOrNull()
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
if (url == null || !url.isHttpOrHttps || url.host in blacklist) {
|
|
||||||
return chain.proceed(request)
|
|
||||||
}
|
|
||||||
val newRequest = onInterceptImageRequest(request, url)
|
|
||||||
return when (val result = chain.proceed(newRequest)) {
|
|
||||||
is SuccessResult -> result
|
|
||||||
is ErrorResult -> {
|
|
||||||
logDebug(result.throwable, newRequest.data)
|
|
||||||
chain.proceed(request).also {
|
|
||||||
if (it is SuccessResult && result.throwable.isBlockedByServer()) {
|
|
||||||
blacklist.add(url.host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final override suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response {
|
|
||||||
val newRequest = onInterceptPageRequest(request)
|
|
||||||
return runCatchingCancellable {
|
|
||||||
okHttp.doCall(newRequest)
|
|
||||||
}.recover { error ->
|
|
||||||
logDebug(error, newRequest.url)
|
|
||||||
okHttp.doCall(request).also {
|
|
||||||
if (error.isBlockedByServer()) {
|
|
||||||
blacklist.add(request.url.host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.getOrThrow()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract suspend fun onInterceptImageRequest(request: ImageRequest, url: HttpUrl): ImageRequest
|
|
||||||
|
|
||||||
protected abstract suspend fun onInterceptPageRequest(request: Request): Request
|
|
||||||
|
|
||||||
private suspend fun OkHttpClient.doCall(request: Request): Response {
|
|
||||||
return newCall(request).await().ensureSuccess()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun logDebug(e: Throwable, url: Any) {
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Log.w("ImageProxy", "${e.message}: $url", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Throwable.isBlockedByServer(): Boolean {
|
|
||||||
return this is CloudFlareBlockedException
|
|
||||||
|| (this is HttpException && response.code == HttpURLConnection.HTTP_FORBIDDEN)
|
|
||||||
|| (this is HttpStatusException && statusCode == HttpURLConnection.HTTP_FORBIDDEN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.network.imageproxy
|
|
||||||
|
|
||||||
import coil.intercept.Interceptor
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
|
|
||||||
interface ImageProxyInterceptor : Interceptor {
|
|
||||||
|
|
||||||
suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.network.imageproxy
|
|
||||||
|
|
||||||
import coil.intercept.Interceptor
|
|
||||||
import coil.request.ImageResult
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.plus
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
|
||||||
import org.koitharu.kotatsu.parsers.util.await
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class RealImageProxyInterceptor @Inject constructor(
|
|
||||||
private val settings: AppSettings,
|
|
||||||
) : ImageProxyInterceptor {
|
|
||||||
|
|
||||||
private val delegate = settings.observeAsStateFlow(
|
|
||||||
scope = processLifecycleScope + Dispatchers.Default,
|
|
||||||
key = AppSettings.KEY_IMAGES_PROXY,
|
|
||||||
valueProducer = { createDelegate() },
|
|
||||||
)
|
|
||||||
|
|
||||||
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
|
||||||
return delegate.value?.intercept(chain) ?: chain.proceed(chain.request)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response {
|
|
||||||
return delegate.value?.interceptPageRequest(request, okHttp) ?: okHttp.newCall(request).await()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createDelegate(): ImageProxyInterceptor? = when (val proxy = settings.imagesProxy) {
|
|
||||||
-1 -> null
|
|
||||||
0 -> WsrvNlProxyInterceptor()
|
|
||||||
1 -> ZeroMsProxyInterceptor()
|
|
||||||
else -> error("Unsupported images proxy $proxy")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.network.imageproxy
|
|
||||||
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import coil.size.Dimension
|
|
||||||
import coil.size.isOriginal
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.Request
|
|
||||||
|
|
||||||
class WsrvNlProxyInterceptor : BaseImageProxyInterceptor() {
|
|
||||||
|
|
||||||
override suspend fun onInterceptImageRequest(request: ImageRequest, url: HttpUrl): ImageRequest {
|
|
||||||
val newUrl = HttpUrl.Builder()
|
|
||||||
.scheme("https")
|
|
||||||
.host("wsrv.nl")
|
|
||||||
.addQueryParameter("url", url.toString())
|
|
||||||
.addQueryParameter("we", null)
|
|
||||||
val size = request.sizeResolver.size()
|
|
||||||
if (!size.isOriginal) {
|
|
||||||
newUrl.addQueryParameter("crop", "cover")
|
|
||||||
(size.height as? Dimension.Pixels)?.let { newUrl.addQueryParameter("h", it.toString()) }
|
|
||||||
(size.width as? Dimension.Pixels)?.let { newUrl.addQueryParameter("w", it.toString()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
return request.newBuilder()
|
|
||||||
.data(newUrl.build())
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun onInterceptPageRequest(request: Request): Request {
|
|
||||||
val sourceUrl = request.url
|
|
||||||
val targetUrl = HttpUrl.Builder()
|
|
||||||
.scheme("https")
|
|
||||||
.host("wsrv.nl")
|
|
||||||
.addQueryParameter("url", sourceUrl.toString())
|
|
||||||
.addQueryParameter("we", null)
|
|
||||||
return request.newBuilder()
|
|
||||||
.url(targetUrl.build())
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.network.imageproxy
|
|
||||||
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Request
|
|
||||||
|
|
||||||
class ZeroMsProxyInterceptor : BaseImageProxyInterceptor() {
|
|
||||||
|
|
||||||
override suspend fun onInterceptImageRequest(request: ImageRequest, url: HttpUrl): ImageRequest {
|
|
||||||
if (url.host == "x.0ms.dev" || url.host == "0ms.dev") {
|
|
||||||
return request
|
|
||||||
}
|
|
||||||
val newUrl = ("https://x.0ms.dev/q70/$url").toHttpUrl()
|
|
||||||
return request.newBuilder()
|
|
||||||
.data(newUrl)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun onInterceptPageRequest(request: Request): Request {
|
|
||||||
val newUrl = ("https://x.0ms.dev/q70/${request.url}").toHttpUrl()
|
|
||||||
return request.newBuilder()
|
|
||||||
.url(newUrl)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,6 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
|
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
|
||||||
import org.koitharu.kotatsu.core.model.getTitle
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||||
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
@@ -174,10 +173,9 @@ class AppShortcutManager @Inject constructor(
|
|||||||
onSuccess = { IconCompat.createWithAdaptiveBitmap(it) },
|
onSuccess = { IconCompat.createWithAdaptiveBitmap(it) },
|
||||||
onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) },
|
onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) },
|
||||||
)
|
)
|
||||||
val title = source.getTitle(context)
|
|
||||||
ShortcutInfoCompat.Builder(context, source.name)
|
ShortcutInfoCompat.Builder(context, source.name)
|
||||||
.setShortLabel(title)
|
.setShortLabel(source.title)
|
||||||
.setLongLabel(title)
|
.setLongLabel(source.title)
|
||||||
.setIcon(icon)
|
.setIcon(icon)
|
||||||
.setLongLived(true)
|
.setLongLived(true)
|
||||||
.setIntent(MangaListActivity.newIntent(context, source))
|
.setIntent(MangaListActivity.newIntent(context, source))
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.parser
|
|
||||||
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import org.koitharu.kotatsu.parsers.bitmap.Bitmap
|
|
||||||
import org.koitharu.kotatsu.parsers.bitmap.Rect
|
|
||||||
import java.io.OutputStream
|
|
||||||
import android.graphics.Bitmap as AndroidBitmap
|
|
||||||
import android.graphics.Rect as AndroidRect
|
|
||||||
|
|
||||||
class BitmapWrapper private constructor(
|
|
||||||
private val androidBitmap: AndroidBitmap,
|
|
||||||
) : Bitmap {
|
|
||||||
|
|
||||||
private val canvas by lazy { Canvas(androidBitmap) } // is not always used, so initialized lazily
|
|
||||||
|
|
||||||
override val height: Int
|
|
||||||
get() = androidBitmap.height
|
|
||||||
|
|
||||||
override val width: Int
|
|
||||||
get() = androidBitmap.width
|
|
||||||
|
|
||||||
override fun drawBitmap(sourceBitmap: Bitmap, src: Rect, dst: Rect) {
|
|
||||||
val androidSourceBitmap = (sourceBitmap as BitmapWrapper).androidBitmap
|
|
||||||
canvas.drawBitmap(androidSourceBitmap, src.toAndroidRect(), dst.toAndroidRect(), null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun compressTo(output: OutputStream) {
|
|
||||||
androidBitmap.compress(AndroidBitmap.CompressFormat.PNG, 100, output)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun create(width: Int, height: Int): Bitmap = BitmapWrapper(
|
|
||||||
AndroidBitmap.createBitmap(width, height, AndroidBitmap.Config.ARGB_8888),
|
|
||||||
)
|
|
||||||
|
|
||||||
fun create(bitmap: AndroidBitmap): Bitmap = BitmapWrapper(
|
|
||||||
if (bitmap.isMutable) bitmap else bitmap.copy(AndroidBitmap.Config.ARGB_8888, true),
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun Rect.toAndroidRect() = AndroidRect(left, top, right, bottom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.parser
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.collection.MutableLongSet
|
|
||||||
import coil.request.CachePolicy
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.MainCoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.currentCoroutineContext
|
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
|
||||||
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
|
||||||
import org.koitharu.kotatsu.core.cache.SafeDeferred
|
|
||||||
import org.koitharu.kotatsu.core.util.MultiMutex
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
|
||||||
|
|
||||||
abstract class CachingMangaRepository(
|
|
||||||
private val cache: MemoryContentCache,
|
|
||||||
) : MangaRepository {
|
|
||||||
|
|
||||||
private val detailsMutex = MultiMutex<Long>()
|
|
||||||
private val relatedMangaMutex = MultiMutex<Long>()
|
|
||||||
private val pagesMutex = MultiMutex<Long>()
|
|
||||||
|
|
||||||
final override suspend fun getDetails(manga: Manga): Manga = getDetails(manga, CachePolicy.ENABLED)
|
|
||||||
|
|
||||||
final override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = pagesMutex.withLock(chapter.id) {
|
|
||||||
cache.getPages(source, chapter.url)?.let { return it }
|
|
||||||
val pages = asyncSafe {
|
|
||||||
getPagesImpl(chapter).distinctById()
|
|
||||||
}
|
|
||||||
cache.putPages(source, chapter.url, pages)
|
|
||||||
pages
|
|
||||||
}.await()
|
|
||||||
|
|
||||||
final override suspend fun getRelated(seed: Manga): List<Manga> = relatedMangaMutex.withLock(seed.id) {
|
|
||||||
cache.getRelatedManga(source, seed.url)?.let { return it }
|
|
||||||
val related = asyncSafe {
|
|
||||||
getRelatedMangaImpl(seed).filterNot { it.id == seed.id }
|
|
||||||
}
|
|
||||||
cache.putRelatedManga(source, seed.url, related)
|
|
||||||
related
|
|
||||||
}.await()
|
|
||||||
|
|
||||||
suspend fun getDetails(manga: Manga, cachePolicy: CachePolicy): Manga = detailsMutex.withLock(manga.id) {
|
|
||||||
if (cachePolicy.readEnabled) {
|
|
||||||
cache.getDetails(source, manga.url)?.let { return it }
|
|
||||||
}
|
|
||||||
val details = asyncSafe {
|
|
||||||
getDetailsImpl(manga)
|
|
||||||
}
|
|
||||||
if (cachePolicy.writeEnabled) {
|
|
||||||
cache.putDetails(source, manga.url, details)
|
|
||||||
}
|
|
||||||
details
|
|
||||||
}.await()
|
|
||||||
|
|
||||||
suspend fun peekDetails(manga: Manga): Manga? {
|
|
||||||
return cache.getDetails(source, manga.url)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invalidateCache() {
|
|
||||||
cache.clear(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract suspend fun getDetailsImpl(manga: Manga): Manga
|
|
||||||
|
|
||||||
protected abstract suspend fun getRelatedMangaImpl(seed: Manga): List<Manga>
|
|
||||||
|
|
||||||
protected abstract suspend fun getPagesImpl(chapter: MangaChapter): List<MangaPage>
|
|
||||||
|
|
||||||
private suspend fun <T> asyncSafe(block: suspend CoroutineScope.() -> T): SafeDeferred<T> {
|
|
||||||
var dispatcher = currentCoroutineContext()[CoroutineDispatcher.Key]
|
|
||||||
if (dispatcher == null || dispatcher is MainCoroutineDispatcher) {
|
|
||||||
dispatcher = Dispatchers.Default
|
|
||||||
}
|
|
||||||
return SafeDeferred(
|
|
||||||
processLifecycleScope.async(dispatcher) {
|
|
||||||
runCatchingCancellable { block() }
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun List<MangaPage>.distinctById(): List<MangaPage> {
|
|
||||||
if (isEmpty()) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
val result = ArrayList<MangaPage>(size)
|
|
||||||
val set = MutableLongSet(size)
|
|
||||||
for (page in this) {
|
|
||||||
if (set.add(page.id)) {
|
|
||||||
result.add(page)
|
|
||||||
} else if (BuildConfig.DEBUG) {
|
|
||||||
Log.w(null, "Duplicate page: $page")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
|||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
import java.util.EnumSet
|
import java.util.EnumSet
|
||||||
@@ -16,7 +16,7 @@ import java.util.EnumSet
|
|||||||
/**
|
/**
|
||||||
* This parser is just for parser development, it should not be used in releases
|
* This parser is just for parser development, it should not be used in releases
|
||||||
*/
|
*/
|
||||||
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaParserSource.DUMMY) {
|
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
|
||||||
|
|
||||||
override val configKeyDomain: ConfigKey.Domain
|
override val configKeyDomain: ConfigKey.Domain
|
||||||
get() = ConfigKey.Domain("localhost")
|
get() = ConfigKey.Domain("localhost")
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.parser
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
|
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
|
||||||
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.MangaPage
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
|
||||||
import java.util.EnumSet
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class EmptyMangaRepository(override val source: MangaSource) : MangaRepository {
|
|
||||||
|
|
||||||
override val sortOrders: Set<SortOrder>
|
|
||||||
get() = EnumSet.allOf(SortOrder::class.java)
|
|
||||||
override val states: Set<MangaState>
|
|
||||||
get() = emptySet()
|
|
||||||
override val contentRatings: Set<ContentRating>
|
|
||||||
get() = emptySet()
|
|
||||||
override var defaultSortOrder: SortOrder
|
|
||||||
get() = SortOrder.NEWEST
|
|
||||||
set(value) = Unit
|
|
||||||
override val isMultipleTagsSupported: Boolean
|
|
||||||
get() = false
|
|
||||||
override val isTagsExclusionSupported: Boolean
|
|
||||||
get() = false
|
|
||||||
override val isSearchSupported: Boolean
|
|
||||||
get() = false
|
|
||||||
|
|
||||||
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> = stub(null)
|
|
||||||
|
|
||||||
override suspend fun getDetails(manga: Manga): Manga = stub(manga)
|
|
||||||
|
|
||||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = stub(null)
|
|
||||||
|
|
||||||
override suspend fun getPageUrl(page: MangaPage): String = stub(null)
|
|
||||||
|
|
||||||
override suspend fun getTags(): Set<MangaTag> = stub(null)
|
|
||||||
|
|
||||||
override suspend fun getLocales(): Set<Locale> = stub(null)
|
|
||||||
|
|
||||||
override suspend fun getRelated(seed: Manga): List<Manga> = stub(seed)
|
|
||||||
|
|
||||||
private fun stub(manga: Manga?): Nothing {
|
|
||||||
throw UnsupportedSourceException("This manga source is not supported", manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,6 @@ import org.koitharu.kotatsu.core.db.entity.toEntities
|
|||||||
import org.koitharu.kotatsu.core.db.entity.toEntity
|
import org.koitharu.kotatsu.core.db.entity.toEntity
|
||||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||||
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||||
import org.koitharu.kotatsu.core.model.LocalMangaSource
|
|
||||||
import org.koitharu.kotatsu.core.model.isLocal
|
import org.koitharu.kotatsu.core.model.isLocal
|
||||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||||
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
|
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
|
||||||
@@ -102,7 +101,7 @@ class MangaDataRepository @Inject constructor(
|
|||||||
|
|
||||||
suspend fun cleanupLocalManga() {
|
suspend fun cleanupLocalManga() {
|
||||||
val dao = db.getMangaDao()
|
val dao = db.getMangaDao()
|
||||||
val broken = dao.findAllBySource(LocalMangaSource.name)
|
val broken = dao.findAllBySource(MangaSource.LOCAL.name)
|
||||||
.filter { x -> x.manga.url.toUri().toFileOrNull()?.exists() == false }
|
.filter { x -> x.manga.url.toUri().toFileOrNull()?.exists() == false }
|
||||||
if (broken.isNotEmpty()) {
|
if (broken.isNotEmpty()) {
|
||||||
dao.delete(broken.map { it.manga })
|
dao.delete(broken.map { it.manga })
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ import android.net.Uri
|
|||||||
import coil.request.CachePolicy
|
import coil.request.CachePolicy
|
||||||
import dagger.Reusable
|
import dagger.Reusable
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
import org.koitharu.kotatsu.core.model.UnknownMangaSource
|
|
||||||
import org.koitharu.kotatsu.core.model.isNsfw
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
||||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
@@ -37,7 +36,7 @@ class MangaLinkResolver @Inject constructor(
|
|||||||
require(uri.pathSegments.singleOrNull() == "manga") { "Invalid url" }
|
require(uri.pathSegments.singleOrNull() == "manga") { "Invalid url" }
|
||||||
val sourceName = requireNotNull(uri.getQueryParameter("source")) { "Source is not specified" }
|
val sourceName = requireNotNull(uri.getQueryParameter("source")) { "Source is not specified" }
|
||||||
val source = MangaSource(sourceName)
|
val source = MangaSource(sourceName)
|
||||||
require(source != UnknownMangaSource) { "Manga source $sourceName is not supported" }
|
require(source != MangaSource.DUMMY) { "Manga source $sourceName is not supported" }
|
||||||
val repo = repositoryFactory.create(source)
|
val repo = repositoryFactory.create(source)
|
||||||
return repo.findExact(
|
return repo.findExact(
|
||||||
url = uri.getQueryParameter("url"),
|
url = uri.getQueryParameter("url"),
|
||||||
@@ -52,7 +51,7 @@ class MangaLinkResolver @Inject constructor(
|
|||||||
val host = uri.host ?: return null
|
val host = uri.host ?: return null
|
||||||
val repo = sourcesRepository.allMangaSources.asSequence()
|
val repo = sourcesRepository.allMangaSources.asSequence()
|
||||||
.map { source ->
|
.map { source ->
|
||||||
repositoryFactory.create(source) as ParserMangaRepository
|
repositoryFactory.create(source) as RemoteMangaRepository
|
||||||
}.find { repo ->
|
}.find { repo ->
|
||||||
host in repo.domains
|
host in repo.domains
|
||||||
} ?: return null
|
} ?: return null
|
||||||
@@ -86,7 +85,7 @@ class MangaLinkResolver @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun MangaRepository.getDetailsNoCache(manga: Manga): Manga {
|
private suspend fun MangaRepository.getDetailsNoCache(manga: Manga): Manga {
|
||||||
return if (this is ParserMangaRepository) {
|
return if (this is RemoteMangaRepository) {
|
||||||
getDetails(manga, CachePolicy.READ_ONLY)
|
getDetails(manga, CachePolicy.READ_ONLY)
|
||||||
} else {
|
} else {
|
||||||
getDetails(manga)
|
getDetails(manga)
|
||||||
@@ -109,7 +108,7 @@ class MangaLinkResolver @Inject constructor(
|
|||||||
url = url,
|
url = url,
|
||||||
publicUrl = "",
|
publicUrl = "",
|
||||||
rating = 0.0f,
|
rating = 0.0f,
|
||||||
isNsfw = source.isNsfw(),
|
isNsfw = source.contentType == ContentType.HENTAI,
|
||||||
coverUrl = "",
|
coverUrl = "",
|
||||||
tags = emptySet(),
|
tags = emptySet(),
|
||||||
state = null,
|
state = null,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package org.koitharu.kotatsu.core.parser
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
@@ -11,21 +10,15 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Response
|
|
||||||
import okhttp3.ResponseBody.Companion.asResponseBody
|
|
||||||
import okio.Buffer
|
|
||||||
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||||
import org.koitharu.kotatsu.core.util.ext.configureForParser
|
import org.koitharu.kotatsu.core.util.ext.configureForParser
|
||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.core.util.ext.requireBody
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.sanitizeHeaderValue
|
import org.koitharu.kotatsu.core.util.ext.sanitizeHeaderValue
|
||||||
import org.koitharu.kotatsu.core.util.ext.toList
|
import org.koitharu.kotatsu.core.util.ext.toList
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.parsers.bitmap.Bitmap
|
|
||||||
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.network.UserAgents
|
import org.koitharu.kotatsu.parsers.network.UserAgents
|
||||||
@@ -75,27 +68,6 @@ class MangaLoaderContextImpl @Inject constructor(
|
|||||||
return LocaleListCompat.getAdjustedDefault().toList()
|
return LocaleListCompat.getAdjustedDefault().toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun redrawImageResponse(response: Response, redraw: (image: Bitmap) -> Bitmap): Response {
|
|
||||||
val image = response.requireBody().byteStream()
|
|
||||||
|
|
||||||
val opts = BitmapFactory.Options()
|
|
||||||
opts.inMutable = true
|
|
||||||
val bitmap = BitmapFactory.decodeStream(image, null, opts) ?: error("Cannot decode bitmap")
|
|
||||||
val result = redraw(BitmapWrapper.create(bitmap)) as BitmapWrapper
|
|
||||||
|
|
||||||
val body = Buffer().also {
|
|
||||||
result.compressTo(it.outputStream())
|
|
||||||
}.asResponseBody("image/jpeg".toMediaType())
|
|
||||||
|
|
||||||
return response.newBuilder()
|
|
||||||
.body(body)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createBitmap(width: Int, height: Int): Bitmap {
|
|
||||||
return BitmapWrapper.create(width, height)
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
private fun obtainWebView(): WebView {
|
private fun obtainWebView(): WebView {
|
||||||
return webViewCached?.get() ?: WebView(androidContext).also {
|
return webViewCached?.get() ?: WebView(androidContext).also {
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ package org.koitharu.kotatsu.core.parser
|
|||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.parsers.MangaParser
|
import org.koitharu.kotatsu.parsers.MangaParser
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
fun MangaParser(source: MangaParserSource, loaderContext: MangaLoaderContext): MangaParser {
|
fun MangaParser(source: MangaSource, loaderContext: MangaLoaderContext): MangaParser {
|
||||||
return when (source) {
|
return if (source == MangaSource.DUMMY) {
|
||||||
MangaParserSource.DUMMY -> DummyParser(loaderContext)
|
DummyParser(loaderContext)
|
||||||
else -> loaderContext.newParserInstance(source)
|
} else {
|
||||||
|
loaderContext.newParserInstance(source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
package org.koitharu.kotatsu.core.parser
|
package org.koitharu.kotatsu.core.parser
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.annotation.AnyThread
|
import androidx.annotation.AnyThread
|
||||||
import androidx.collection.ArrayMap
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||||
import org.koitharu.kotatsu.core.model.LocalMangaSource
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaSourceInfo
|
|
||||||
import org.koitharu.kotatsu.core.model.UnknownMangaSource
|
|
||||||
import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor
|
import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor
|
||||||
import org.koitharu.kotatsu.core.parser.external.ExternalMangaRepository
|
|
||||||
import org.koitharu.kotatsu.core.parser.external.ExternalMangaSource
|
|
||||||
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
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||||
@@ -18,12 +10,12 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
|||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
import java.util.EnumMap
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
@@ -61,60 +53,32 @@ interface MangaRepository {
|
|||||||
|
|
||||||
suspend fun getRelated(seed: Manga): List<Manga>
|
suspend fun getRelated(seed: Manga): List<Manga>
|
||||||
|
|
||||||
suspend fun find(manga: Manga): Manga? {
|
|
||||||
val list = getList(0, MangaListFilter.Search(manga.title))
|
|
||||||
return list.find { x -> x.id == manga.id }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class Factory @Inject constructor(
|
class Factory @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
|
||||||
private val localMangaRepository: LocalMangaRepository,
|
private val localMangaRepository: LocalMangaRepository,
|
||||||
private val loaderContext: MangaLoaderContext,
|
private val loaderContext: MangaLoaderContext,
|
||||||
private val contentCache: MemoryContentCache,
|
private val contentCache: MemoryContentCache,
|
||||||
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
|
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val cache = ArrayMap<MangaSource, WeakReference<MangaRepository>>()
|
private val cache = EnumMap<MangaSource, WeakReference<RemoteMangaRepository>>(MangaSource::class.java)
|
||||||
|
|
||||||
@AnyThread
|
@AnyThread
|
||||||
fun create(source: MangaSource): MangaRepository {
|
fun create(source: MangaSource): MangaRepository {
|
||||||
when (source) {
|
if (source == MangaSource.LOCAL) {
|
||||||
is MangaSourceInfo -> return create(source.mangaSource)
|
return localMangaRepository
|
||||||
LocalMangaSource -> return localMangaRepository
|
|
||||||
UnknownMangaSource -> return EmptyMangaRepository(source)
|
|
||||||
}
|
}
|
||||||
cache[source]?.get()?.let { return it }
|
cache[source]?.get()?.let { return it }
|
||||||
return synchronized(cache) {
|
return synchronized(cache) {
|
||||||
cache[source]?.get()?.let { return it }
|
cache[source]?.get()?.let { return it }
|
||||||
val repository = createRepository(source)
|
val repository = RemoteMangaRepository(
|
||||||
if (repository != null) {
|
parser = MangaParser(source, loaderContext),
|
||||||
cache[source] = WeakReference(repository)
|
|
||||||
repository
|
|
||||||
} else {
|
|
||||||
EmptyMangaRepository(source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createRepository(source: MangaSource): MangaRepository? = when (source) {
|
|
||||||
is MangaParserSource -> ParserMangaRepository(
|
|
||||||
parser = MangaParser(source, loaderContext),
|
|
||||||
cache = contentCache,
|
|
||||||
mirrorSwitchInterceptor = mirrorSwitchInterceptor,
|
|
||||||
)
|
|
||||||
|
|
||||||
is ExternalMangaSource -> if (source.isAvailable(context)) {
|
|
||||||
ExternalMangaRepository(
|
|
||||||
contentResolver = context.contentResolver,
|
|
||||||
source = source,
|
|
||||||
cache = contentCache,
|
cache = contentCache,
|
||||||
|
mirrorSwitchInterceptor = mirrorSwitchInterceptor,
|
||||||
)
|
)
|
||||||
} else {
|
cache[source] = WeakReference(repository)
|
||||||
EmptyMangaRepository(source)
|
repository
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
package org.koitharu.kotatsu.core.parser
|
package org.koitharu.kotatsu.core.parser
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.collection.MutableLongSet
|
||||||
|
import coil.request.CachePolicy
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.MainCoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||||
|
import org.koitharu.kotatsu.core.cache.SafeDeferred
|
||||||
import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor
|
import org.koitharu.kotatsu.core.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.parsers.MangaParser
|
import org.koitharu.kotatsu.parsers.MangaParser
|
||||||
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
|
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
|
||||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||||
@@ -15,7 +28,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
|||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
@@ -23,13 +36,17 @@ import org.koitharu.kotatsu.parsers.util.domain
|
|||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class ParserMangaRepository(
|
class RemoteMangaRepository(
|
||||||
private val parser: MangaParser,
|
private val parser: MangaParser,
|
||||||
|
private val cache: MemoryContentCache,
|
||||||
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
|
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
|
||||||
cache: MemoryContentCache,
|
) : MangaRepository, Interceptor {
|
||||||
) : CachingMangaRepository(cache), Interceptor {
|
|
||||||
|
|
||||||
override val source: MangaParserSource
|
private val detailsMutex = MultiMutex<Long>()
|
||||||
|
private val relatedMangaMutex = MultiMutex<Long>()
|
||||||
|
private val pagesMutex = MultiMutex<Long>()
|
||||||
|
|
||||||
|
override val source: MangaSource
|
||||||
get() = parser.source
|
get() = parser.source
|
||||||
|
|
||||||
override val sortOrders: Set<SortOrder>
|
override val sortOrders: Set<SortOrder>
|
||||||
@@ -82,11 +99,18 @@ class ParserMangaRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getPagesImpl(
|
override suspend fun getDetails(manga: Manga): Manga = getDetails(manga, CachePolicy.ENABLED)
|
||||||
chapter: MangaChapter
|
|
||||||
): List<MangaPage> = mirrorSwitchInterceptor.withMirrorSwitching {
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = pagesMutex.withLock(chapter.id) {
|
||||||
parser.getPages(chapter)
|
cache.getPages(source, chapter.url)?.let { return it }
|
||||||
}
|
val pages = asyncSafe {
|
||||||
|
mirrorSwitchInterceptor.withMirrorSwitching {
|
||||||
|
parser.getPages(chapter).distinctById()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cache.putPages(source, chapter.url, pages)
|
||||||
|
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)
|
||||||
@@ -104,10 +128,37 @@ class ParserMangaRepository(
|
|||||||
parser.getFavicons()
|
parser.getFavicons()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getRelatedMangaImpl(seed: Manga): List<Manga> = parser.getRelatedManga(seed)
|
override suspend fun getRelated(seed: Manga): List<Manga> = relatedMangaMutex.withLock(seed.id) {
|
||||||
|
cache.getRelatedManga(source, seed.url)?.let { return it }
|
||||||
|
val related = asyncSafe {
|
||||||
|
parser.getRelatedManga(seed).filterNot { it.id == seed.id }
|
||||||
|
}
|
||||||
|
cache.putRelatedManga(source, seed.url, related)
|
||||||
|
related
|
||||||
|
}.await()
|
||||||
|
|
||||||
override suspend fun getDetailsImpl(manga: Manga): Manga = mirrorSwitchInterceptor.withMirrorSwitching {
|
suspend fun getDetails(manga: Manga, cachePolicy: CachePolicy): Manga = detailsMutex.withLock(manga.id) {
|
||||||
parser.getDetails(manga)
|
if (cachePolicy.readEnabled) {
|
||||||
|
cache.getDetails(source, manga.url)?.let { return it }
|
||||||
|
}
|
||||||
|
val details = asyncSafe {
|
||||||
|
mirrorSwitchInterceptor.withMirrorSwitching {
|
||||||
|
parser.getDetails(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cachePolicy.writeEnabled) {
|
||||||
|
cache.putDetails(source, manga.url, details)
|
||||||
|
}
|
||||||
|
details
|
||||||
|
}.await()
|
||||||
|
|
||||||
|
suspend fun peekDetails(manga: Manga): Manga? {
|
||||||
|
return cache.getDetails(source, manga.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun find(manga: Manga): Manga? {
|
||||||
|
val list = getList(0, MangaListFilter.Search(manga.title))
|
||||||
|
return list.find { x -> x.id == manga.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAuthProvider(): MangaParserAuthProvider? = parser as? MangaParserAuthProvider
|
fun getAuthProvider(): MangaParserAuthProvider? = parser as? MangaParserAuthProvider
|
||||||
@@ -124,8 +175,40 @@ class ParserMangaRepository(
|
|||||||
return getConfig().isSlowdownEnabled
|
return getConfig().isSlowdownEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun invalidateCache() {
|
||||||
|
cache.clear(source)
|
||||||
|
}
|
||||||
|
|
||||||
fun getConfig() = parser.config as SourceSettings
|
fun getConfig() = parser.config as SourceSettings
|
||||||
|
|
||||||
|
private suspend fun <T> asyncSafe(block: suspend CoroutineScope.() -> T): SafeDeferred<T> {
|
||||||
|
var dispatcher = currentCoroutineContext()[CoroutineDispatcher.Key]
|
||||||
|
if (dispatcher == null || dispatcher is MainCoroutineDispatcher) {
|
||||||
|
dispatcher = Dispatchers.Default
|
||||||
|
}
|
||||||
|
return SafeDeferred(
|
||||||
|
processLifecycleScope.async(dispatcher) {
|
||||||
|
runCatchingCancellable { block() }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<MangaPage>.distinctById(): List<MangaPage> {
|
||||||
|
if (isEmpty()) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
val result = ArrayList<MangaPage>(size)
|
||||||
|
val set = MutableLongSet(size)
|
||||||
|
for (page in this) {
|
||||||
|
if (set.add(page.id)) {
|
||||||
|
result.add(page)
|
||||||
|
} else if (BuildConfig.DEBUG) {
|
||||||
|
Log.w(null, "Duplicate page: $page")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun <R> MirrorSwitchInterceptor.withMirrorSwitching(block: suspend () -> R): R {
|
private suspend fun <R> MirrorSwitchInterceptor.withMirrorSwitching(block: suspend () -> R): R {
|
||||||
if (!isEnabled) {
|
if (!isEnabled) {
|
||||||
return block()
|
return block()
|
||||||
@@ -137,14 +220,14 @@ class ParserMangaRepository(
|
|||||||
if (result.isValidResult()) {
|
if (result.isValidResult()) {
|
||||||
return result.getOrThrow()
|
return result.getOrThrow()
|
||||||
}
|
}
|
||||||
return if (trySwitchMirror(this@ParserMangaRepository)) {
|
return if (trySwitchMirror(this@RemoteMangaRepository)) {
|
||||||
val newResult = runCatchingCancellable {
|
val newResult = runCatchingCancellable {
|
||||||
block()
|
block()
|
||||||
}
|
}
|
||||||
if (newResult.isValidResult()) {
|
if (newResult.isValidResult()) {
|
||||||
return newResult.getOrThrow()
|
return newResult.getOrThrow()
|
||||||
} else {
|
} else {
|
||||||
rollback(this@ParserMangaRepository, initialMirror)
|
rollback(this@RemoteMangaRepository, initialMirror)
|
||||||
return result.getOrThrow()
|
return result.getOrThrow()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.parser.external
|
|
||||||
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.runInterruptible
|
|
||||||
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
|
||||||
import org.koitharu.kotatsu.core.parser.CachingMangaRepository
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
|
||||||
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.MangaPage
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
|
||||||
import java.util.EnumSet
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class ExternalMangaRepository(
|
|
||||||
private val contentResolver: ContentResolver,
|
|
||||||
override val source: ExternalMangaSource,
|
|
||||||
cache: MemoryContentCache,
|
|
||||||
) : CachingMangaRepository(cache) {
|
|
||||||
|
|
||||||
private val contentSource = ExternalPluginContentSource(contentResolver, source)
|
|
||||||
|
|
||||||
private val capabilities by lazy {
|
|
||||||
runCatching {
|
|
||||||
contentSource.getCapabilities()
|
|
||||||
}.onFailure {
|
|
||||||
it.printStackTraceDebug()
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override val sortOrders: Set<SortOrder>
|
|
||||||
get() = capabilities?.availableSortOrders ?: EnumSet.of(SortOrder.ALPHABETICAL)
|
|
||||||
|
|
||||||
override val states: Set<MangaState>
|
|
||||||
get() = capabilities?.availableStates.orEmpty()
|
|
||||||
|
|
||||||
override val contentRatings: Set<ContentRating>
|
|
||||||
get() = capabilities?.availableContentRating.orEmpty()
|
|
||||||
|
|
||||||
override var defaultSortOrder: SortOrder
|
|
||||||
get() = capabilities?.defaultSortOrder ?: SortOrder.ALPHABETICAL
|
|
||||||
set(value) = Unit
|
|
||||||
|
|
||||||
override val isMultipleTagsSupported: Boolean
|
|
||||||
get() = capabilities?.isMultipleTagsSupported ?: true
|
|
||||||
|
|
||||||
override val isTagsExclusionSupported: Boolean
|
|
||||||
get() = capabilities?.isTagsExclusionSupported ?: false
|
|
||||||
|
|
||||||
override val isSearchSupported: Boolean
|
|
||||||
get() = capabilities?.isSearchSupported ?: true
|
|
||||||
|
|
||||||
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> =
|
|
||||||
runInterruptible(Dispatchers.IO) {
|
|
||||||
contentSource.getList(offset, filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getDetailsImpl(manga: Manga): Manga = runInterruptible(Dispatchers.IO) {
|
|
||||||
contentSource.getDetails(manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getPagesImpl(chapter: MangaChapter): List<MangaPage> = runInterruptible(Dispatchers.IO) {
|
|
||||||
contentSource.getPages(chapter)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getPageUrl(page: MangaPage): String = page.url // TODO
|
|
||||||
|
|
||||||
override suspend fun getTags(): Set<MangaTag> = runInterruptible(Dispatchers.IO) {
|
|
||||||
contentSource.getTags()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getLocales(): Set<Locale> = emptySet() // TODO
|
|
||||||
|
|
||||||
override suspend fun getRelatedMangaImpl(seed: Manga): List<Manga> = emptyList() // TODO
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.parser.external
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
|
|
||||||
data class ExternalMangaSource(
|
|
||||||
val packageName: String,
|
|
||||||
val authority: String,
|
|
||||||
) : MangaSource {
|
|
||||||
|
|
||||||
override val name: String
|
|
||||||
get() = "content:$packageName/$authority"
|
|
||||||
|
|
||||||
private var cachedName: String? = null
|
|
||||||
|
|
||||||
fun isAvailable(context: Context): Boolean {
|
|
||||||
return context.packageManager.resolveContentProvider(authority, 0)?.isEnabled == true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun resolveName(context: Context): String {
|
|
||||||
cachedName?.let {
|
|
||||||
return it
|
|
||||||
}
|
|
||||||
val pm = context.packageManager
|
|
||||||
val info = pm.resolveContentProvider(authority, 0)
|
|
||||||
return info?.loadLabel(pm)?.toString()?.also {
|
|
||||||
cachedName = it
|
|
||||||
} ?: authority
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,291 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.parser.external
|
|
||||||
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.database.Cursor
|
|
||||||
import androidx.annotation.WorkerThread
|
|
||||||
import androidx.collection.ArraySet
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import org.jetbrains.annotations.Blocking
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.IncompatiblePluginException
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.toLocale
|
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
|
||||||
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.MangaPage
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
|
||||||
import org.koitharu.kotatsu.parsers.util.find
|
|
||||||
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
|
||||||
import org.koitharu.kotatsu.parsers.util.splitTwoParts
|
|
||||||
import java.util.EnumSet
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class ExternalPluginContentSource(
|
|
||||||
private val contentResolver: ContentResolver,
|
|
||||||
private val source: ExternalMangaSource,
|
|
||||||
) {
|
|
||||||
|
|
||||||
@Blocking
|
|
||||||
@WorkerThread
|
|
||||||
fun getList(offset: Int, filter: MangaListFilter?): List<Manga> = runCatchingCompatibility {
|
|
||||||
val uri = "content://${source.authority}/manga".toUri().buildUpon()
|
|
||||||
uri.appendQueryParameter("offset", offset.toString())
|
|
||||||
when (filter) {
|
|
||||||
is MangaListFilter.Advanced -> {
|
|
||||||
filter.tags.forEach { uri.appendQueryParameter("tag_include", it.key) }
|
|
||||||
filter.tagsExclude.forEach { uri.appendQueryParameter("tag_exclude", it.key) }
|
|
||||||
filter.states.forEach { uri.appendQueryParameter("state", it.name) }
|
|
||||||
filter.locale?.let { uri.appendQueryParameter("locale", it.language) }
|
|
||||||
filter.contentRating.forEach { uri.appendQueryParameter("content_rating", it.name) }
|
|
||||||
}
|
|
||||||
|
|
||||||
is MangaListFilter.Search -> {
|
|
||||||
uri.appendQueryParameter("query", filter.query)
|
|
||||||
}
|
|
||||||
|
|
||||||
null -> Unit
|
|
||||||
}
|
|
||||||
contentResolver.query(uri.build(), null, null, null, filter?.sortOrder?.name)
|
|
||||||
.safe()
|
|
||||||
.use { cursor ->
|
|
||||||
val result = ArrayList<Manga>(cursor.count)
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
do {
|
|
||||||
result += cursor.getManga()
|
|
||||||
} while (cursor.moveToNext())
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Blocking
|
|
||||||
@WorkerThread
|
|
||||||
fun getDetails(manga: Manga) = runCatchingCompatibility {
|
|
||||||
val chapters = queryChapters(manga.url)
|
|
||||||
val details = queryDetails(manga.url)
|
|
||||||
Manga(
|
|
||||||
id = manga.id,
|
|
||||||
title = details.title.ifBlank { manga.title },
|
|
||||||
altTitle = details.altTitle.ifNullOrEmpty { manga.altTitle },
|
|
||||||
url = details.url.ifEmpty { manga.url },
|
|
||||||
publicUrl = details.publicUrl.ifEmpty { manga.publicUrl },
|
|
||||||
rating = maxOf(details.rating, manga.rating),
|
|
||||||
isNsfw = details.isNsfw,
|
|
||||||
coverUrl = details.coverUrl.ifEmpty { manga.coverUrl },
|
|
||||||
tags = details.tags + manga.tags,
|
|
||||||
state = details.state ?: manga.state,
|
|
||||||
author = details.author.ifNullOrEmpty { manga.author },
|
|
||||||
largeCoverUrl = details.largeCoverUrl.ifNullOrEmpty { manga.largeCoverUrl },
|
|
||||||
description = details.description.ifNullOrEmpty { manga.description },
|
|
||||||
chapters = chapters,
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Blocking
|
|
||||||
@WorkerThread
|
|
||||||
fun getPages(chapter: MangaChapter): List<MangaPage> = runCatchingCompatibility {
|
|
||||||
val uri = "content://${source.authority}/chapters".toUri()
|
|
||||||
.buildUpon()
|
|
||||||
.appendPath(chapter.url)
|
|
||||||
.build()
|
|
||||||
contentResolver.query(uri, null, null, null, null)
|
|
||||||
.safe()
|
|
||||||
.use { cursor ->
|
|
||||||
val result = ArrayList<MangaPage>(cursor.count)
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
do {
|
|
||||||
result += MangaPage(
|
|
||||||
id = cursor.getLong(COLUMN_ID),
|
|
||||||
url = cursor.getString(COLUMN_URL),
|
|
||||||
preview = cursor.getStringOrNull(COLUMN_PREVIEW),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
} while (cursor.moveToNext())
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Blocking
|
|
||||||
@WorkerThread
|
|
||||||
fun getTags(): Set<MangaTag> = runCatchingCompatibility {
|
|
||||||
val uri = "content://${source.authority}/tags".toUri()
|
|
||||||
contentResolver.query(uri, null, null, null, null)
|
|
||||||
.safe()
|
|
||||||
.use { cursor ->
|
|
||||||
val result = ArraySet<MangaTag>(cursor.count)
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
do {
|
|
||||||
result += MangaTag(
|
|
||||||
key = cursor.getString(COLUMN_KEY),
|
|
||||||
title = cursor.getString(COLUMN_TITLE),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
} while (cursor.moveToNext())
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCapabilities(): MangaSourceCapabilities? {
|
|
||||||
val uri = "content://${source.authority}/capabilities".toUri()
|
|
||||||
return contentResolver.query(uri, null, null, null, null)
|
|
||||||
.safe()
|
|
||||||
.use { cursor ->
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
MangaSourceCapabilities(
|
|
||||||
availableSortOrders = cursor.getStringOrNull(COLUMN_SORT_ORDERS)
|
|
||||||
?.split(',')
|
|
||||||
?.mapNotNullTo(EnumSet.noneOf(SortOrder::class.java)) {
|
|
||||||
SortOrder.entries.find(it)
|
|
||||||
}.orEmpty(),
|
|
||||||
availableStates = cursor.getStringOrNull(COLUMN_STATES)
|
|
||||||
?.split(',')
|
|
||||||
?.mapNotNullTo(EnumSet.noneOf(MangaState::class.java)) {
|
|
||||||
MangaState.entries.find(it)
|
|
||||||
}.orEmpty(),
|
|
||||||
availableContentRating = cursor.getStringOrNull(COLUMN_CONTENT_RATING)
|
|
||||||
?.split(',')
|
|
||||||
?.mapNotNullTo(EnumSet.noneOf(ContentRating::class.java)) {
|
|
||||||
ContentRating.entries.find(it)
|
|
||||||
}.orEmpty(),
|
|
||||||
isMultipleTagsSupported = cursor.getBooleanOrDefault(COLUMN_MULTIPLE_TAGS_SUPPORTED, true),
|
|
||||||
isTagsExclusionSupported = cursor.getBooleanOrDefault(COLUMN_TAGS_EXCLUSION_SUPPORTED, false),
|
|
||||||
isSearchSupported = cursor.getBooleanOrDefault(COLUMN_SEARCH_SUPPORTED, true),
|
|
||||||
contentType = cursor.getStringOrNull(COLUMN_CONTENT_TYPE)?.let {
|
|
||||||
ContentType.entries.find(it)
|
|
||||||
} ?: ContentType.OTHER,
|
|
||||||
defaultSortOrder = cursor.getStringOrNull(COLUMN_DEFAULT_SORT_ORDER)?.let {
|
|
||||||
SortOrder.entries.find(it)
|
|
||||||
} ?: SortOrder.ALPHABETICAL,
|
|
||||||
sourceLocale = cursor.getStringOrNull(COLUMN_LOCALE)?.toLocale() ?: Locale.ROOT,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun queryDetails(url: String): Manga {
|
|
||||||
val uri = "content://${source.authority}/manga".toUri()
|
|
||||||
.buildUpon()
|
|
||||||
.appendPath(url)
|
|
||||||
.build()
|
|
||||||
return contentResolver.query(uri, null, null, null, null)
|
|
||||||
.safe()
|
|
||||||
.use { cursor ->
|
|
||||||
cursor.moveToFirst()
|
|
||||||
cursor.getManga()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun queryChapters(url: String): List<MangaChapter> {
|
|
||||||
val uri = "content://${source.authority}/manga/chapters".toUri()
|
|
||||||
.buildUpon()
|
|
||||||
.appendPath(url)
|
|
||||||
.build()
|
|
||||||
return contentResolver.query(uri, null, null, null, null)
|
|
||||||
.safe()
|
|
||||||
.use { cursor ->
|
|
||||||
val result = ArrayList<MangaChapter>(cursor.count)
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
do {
|
|
||||||
result += MangaChapter(
|
|
||||||
id = cursor.getLong(COLUMN_ID),
|
|
||||||
name = cursor.getString(COLUMN_NAME),
|
|
||||||
number = cursor.getFloatOrDefault(COLUMN_NUMBER, 0f),
|
|
||||||
volume = cursor.getIntOrDefault(COLUMN_VOLUME, 0),
|
|
||||||
url = cursor.getString(COLUMN_URL),
|
|
||||||
scanlator = cursor.getStringOrNull(COLUMN_SCANLATOR),
|
|
||||||
uploadDate = cursor.getLongOrDefault(COLUMN_UPLOAD_DATE, 0L),
|
|
||||||
branch = cursor.getStringOrNull(COLUMN_BRANCH),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
} while (cursor.moveToNext())
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun SafeCursor.getManga() = Manga(
|
|
||||||
id = getLong(COLUMN_ID),
|
|
||||||
title = getString(COLUMN_TITLE),
|
|
||||||
altTitle = getStringOrNull(COLUMN_ALT_TITLE),
|
|
||||||
url = getString(COLUMN_URL),
|
|
||||||
publicUrl = getString(COLUMN_PUBLIC_URL),
|
|
||||||
rating = getFloat(COLUMN_RATING),
|
|
||||||
isNsfw = getBooleanOrDefault(COLUMN_IS_NSFW, false),
|
|
||||||
coverUrl = getString(COLUMN_COVER_URL),
|
|
||||||
tags = getStringOrNull(COLUMN_TAGS)?.split(':')?.mapNotNullToSet {
|
|
||||||
val parts = it.splitTwoParts('=') ?: return@mapNotNullToSet null
|
|
||||||
MangaTag(key = parts.first, title = parts.second, source = source)
|
|
||||||
}.orEmpty(),
|
|
||||||
state = getStringOrNull(COLUMN_STATE)?.let { MangaState.entries.find(it) },
|
|
||||||
author = getStringOrNull(COLUMN_AUTHOR),
|
|
||||||
largeCoverUrl = getStringOrNull(COLUMN_LARGE_COVER_URL),
|
|
||||||
description = getStringOrNull(COLUMN_DESCRIPTION),
|
|
||||||
chapters = emptyList(),
|
|
||||||
source = source,
|
|
||||||
)
|
|
||||||
|
|
||||||
private inline fun <R> runCatchingCompatibility(block: () -> R): R = try {
|
|
||||||
block()
|
|
||||||
} catch (e: NoSuchElementException) { // unknown column name
|
|
||||||
throw IncompatiblePluginException(source.name, e)
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
throw IncompatiblePluginException(source.name, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Cursor?.safe() = SafeCursor(this ?: throw IncompatiblePluginException(source.name, null))
|
|
||||||
|
|
||||||
class MangaSourceCapabilities(
|
|
||||||
val availableSortOrders: Set<SortOrder>,
|
|
||||||
val availableStates: Set<MangaState>,
|
|
||||||
val availableContentRating: Set<ContentRating>,
|
|
||||||
val isMultipleTagsSupported: Boolean,
|
|
||||||
val isTagsExclusionSupported: Boolean,
|
|
||||||
val isSearchSupported: Boolean,
|
|
||||||
val contentType: ContentType,
|
|
||||||
val defaultSortOrder: SortOrder,
|
|
||||||
val sourceLocale: Locale,
|
|
||||||
)
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
|
|
||||||
const val COLUMN_SORT_ORDERS = "sort_orders"
|
|
||||||
const val COLUMN_STATES = "states"
|
|
||||||
const val COLUMN_CONTENT_RATING = "content_rating"
|
|
||||||
const val COLUMN_MULTIPLE_TAGS_SUPPORTED = "multiple_tags_supported"
|
|
||||||
const val COLUMN_TAGS_EXCLUSION_SUPPORTED = "tags_exclusion_supported"
|
|
||||||
const val COLUMN_SEARCH_SUPPORTED = "search_supported"
|
|
||||||
const val COLUMN_CONTENT_TYPE = "content_type"
|
|
||||||
const val COLUMN_DEFAULT_SORT_ORDER = "default_sort_order"
|
|
||||||
const val COLUMN_LOCALE = "locale"
|
|
||||||
const val COLUMN_ID = "id"
|
|
||||||
const val COLUMN_NAME = "name"
|
|
||||||
const val COLUMN_NUMBER = "number"
|
|
||||||
const val COLUMN_VOLUME = "volume"
|
|
||||||
const val COLUMN_URL = "url"
|
|
||||||
const val COLUMN_SCANLATOR = "scanlator"
|
|
||||||
const val COLUMN_UPLOAD_DATE = "upload_date"
|
|
||||||
const val COLUMN_BRANCH = "branch"
|
|
||||||
const val COLUMN_TITLE = "title"
|
|
||||||
const val COLUMN_ALT_TITLE = "alt_title"
|
|
||||||
const val COLUMN_PUBLIC_URL = "public_url"
|
|
||||||
const val COLUMN_RATING = "rating"
|
|
||||||
const val COLUMN_IS_NSFW = "is_nsfw"
|
|
||||||
const val COLUMN_COVER_URL = "cover_url"
|
|
||||||
const val COLUMN_TAGS = "tags"
|
|
||||||
const val COLUMN_STATE = "state"
|
|
||||||
const val COLUMN_AUTHOR = "author"
|
|
||||||
const val COLUMN_LARGE_COVER_URL = "large_cover_url"
|
|
||||||
const val COLUMN_DESCRIPTION = "description"
|
|
||||||
const val COLUMN_PREVIEW = "preview"
|
|
||||||
const val COLUMN_KEY = "key"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.parser.external
|
|
||||||
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.database.CursorWrapper
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.getBoolean
|
|
||||||
|
|
||||||
class SafeCursor(cursor: Cursor) : CursorWrapper(cursor) {
|
|
||||||
|
|
||||||
fun getString(columnName: String): String {
|
|
||||||
return getString(getColumnIndexOrThrow(columnName))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getStringOrNull(columnName: String): String? {
|
|
||||||
val columnIndex = getColumnIndex(columnName)
|
|
||||||
return when {
|
|
||||||
columnIndex < 0 -> null
|
|
||||||
isNull(columnIndex) -> null
|
|
||||||
else -> getString(columnIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getBoolean(columnName: String): Boolean {
|
|
||||||
return getBoolean(getColumnIndexOrThrow(columnName))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getBooleanOrDefault(columnName: String, defaultValue: Boolean): Boolean {
|
|
||||||
val columnIndex = getColumnIndex(columnName)
|
|
||||||
return when {
|
|
||||||
columnIndex < 0 -> defaultValue
|
|
||||||
isNull(columnIndex) -> defaultValue
|
|
||||||
else -> getBoolean(columnIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getInt(columnName: String): Int {
|
|
||||||
return getInt(getColumnIndexOrThrow(columnName))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getIntOrDefault(columnName: String, defaultValue: Int): Int {
|
|
||||||
val columnIndex = getColumnIndex(columnName)
|
|
||||||
return when {
|
|
||||||
columnIndex < 0 -> defaultValue
|
|
||||||
isNull(columnIndex) -> defaultValue
|
|
||||||
else -> getInt(columnIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getLong(columnName: String): Long {
|
|
||||||
return getLong(getColumnIndexOrThrow(columnName))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getLongOrDefault(columnName: String, defaultValue: Long): Long {
|
|
||||||
val columnIndex = getColumnIndex(columnName)
|
|
||||||
return when {
|
|
||||||
columnIndex < 0 -> defaultValue
|
|
||||||
isNull(columnIndex) -> defaultValue
|
|
||||||
else -> getLong(columnIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFloat(columnName: String): Float {
|
|
||||||
return getFloat(getColumnIndexOrThrow(columnName))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFloatOrDefault(columnName: String, defaultValue: Float): Float {
|
|
||||||
val columnIndex = getColumnIndex(columnName)
|
|
||||||
return when {
|
|
||||||
columnIndex < 0 -> defaultValue
|
|
||||||
isNull(columnIndex) -> defaultValue
|
|
||||||
else -> getFloat(columnIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,12 @@
|
|||||||
package org.koitharu.kotatsu.core.parser.favicon
|
package org.koitharu.kotatsu.core.parser.favicon
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.drawable.AdaptiveIconDrawable
|
|
||||||
import android.graphics.drawable.ColorDrawable
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.graphics.drawable.LayerDrawable
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.decode.DataSource
|
import coil.decode.DataSource
|
||||||
import coil.decode.ImageSource
|
import coil.decode.ImageSource
|
||||||
import coil.disk.DiskCache
|
import coil.disk.DiskCache
|
||||||
import coil.fetch.DrawableResult
|
|
||||||
import coil.fetch.FetchResult
|
import coil.fetch.FetchResult
|
||||||
import coil.fetch.Fetcher
|
import coil.fetch.Fetcher
|
||||||
import coil.fetch.SourceResult
|
import coil.fetch.SourceResult
|
||||||
@@ -21,9 +14,7 @@ import coil.network.HttpException
|
|||||||
import coil.request.Options
|
import coil.request.Options
|
||||||
import coil.size.Size
|
import coil.size.Size
|
||||||
import coil.size.pxOrElse
|
import coil.size.pxOrElse
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.ensureActive
|
import kotlinx.coroutines.ensureActive
|
||||||
import kotlinx.coroutines.runInterruptible
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@@ -33,11 +24,8 @@ import okio.Closeable
|
|||||||
import okio.buffer
|
import okio.buffer
|
||||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
import org.koitharu.kotatsu.core.parser.EmptyMangaRepository
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
|
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||||
import org.koitharu.kotatsu.core.parser.external.ExternalMangaRepository
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.requireBody
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
|
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
|
||||||
import org.koitharu.kotatsu.local.data.CacheDir
|
import org.koitharu.kotatsu.local.data.CacheDir
|
||||||
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
|
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
|
||||||
@@ -57,27 +45,14 @@ class FaviconFetcher(
|
|||||||
) : Fetcher {
|
) : Fetcher {
|
||||||
|
|
||||||
private val diskCacheKey
|
private val diskCacheKey
|
||||||
get() = options.diskCacheKey ?: "${mangaSource.name}x${options.size.toCacheKey()}"
|
get() = options.diskCacheKey ?: "${mangaSource.name}[${mangaSource.ordinal}]x${options.size.toCacheKey()}"
|
||||||
|
|
||||||
private val fileSystem
|
private val fileSystem
|
||||||
get() = checkNotNull(diskCache.value).fileSystem
|
get() = checkNotNull(diskCache.value).fileSystem
|
||||||
|
|
||||||
override suspend fun fetch(): FetchResult {
|
override suspend fun fetch(): FetchResult {
|
||||||
getCached(options)?.let { return it }
|
getCached(options)?.let { return it }
|
||||||
return when (val repo = mangaRepositoryFactory.create(mangaSource)) {
|
val repo = mangaRepositoryFactory.create(mangaSource) as RemoteMangaRepository
|
||||||
is ParserMangaRepository -> fetchParserFavicon(repo)
|
|
||||||
is ExternalMangaRepository -> fetchPluginIcon(repo)
|
|
||||||
is EmptyMangaRepository -> DrawableResult(
|
|
||||||
drawable = ColorDrawable(Color.WHITE),
|
|
||||||
isSampled = false,
|
|
||||||
dataSource = DataSource.MEMORY,
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> throw IllegalArgumentException("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun fetchParserFavicon(repo: ParserMangaRepository): FetchResult {
|
|
||||||
val sizePx = maxOf(
|
val sizePx = maxOf(
|
||||||
options.size.width.pxOrElse { FALLBACK_SIZE },
|
options.size.width.pxOrElse { FALLBACK_SIZE },
|
||||||
options.size.height.pxOrElse { FALLBACK_SIZE },
|
options.size.height.pxOrElse { FALLBACK_SIZE },
|
||||||
@@ -124,20 +99,6 @@ class FaviconFetcher(
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun fetchPluginIcon(repository: ExternalMangaRepository): FetchResult {
|
|
||||||
val source = repository.source
|
|
||||||
val pm = options.context.packageManager
|
|
||||||
val icon = runInterruptible(Dispatchers.IO) {
|
|
||||||
val provider = pm.resolveContentProvider(source.authority, 0)
|
|
||||||
provider?.loadIcon(pm) ?: pm.getApplicationIcon(source.packageName)
|
|
||||||
}
|
|
||||||
return DrawableResult(
|
|
||||||
drawable = icon.nonAdaptive(),
|
|
||||||
isSampled = false,
|
|
||||||
dataSource = DataSource.DISK,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getCached(options: Options): SourceResult? {
|
private fun getCached(options: Options): SourceResult? {
|
||||||
if (!options.diskCachePolicy.readEnabled) {
|
if (!options.diskCachePolicy.readEnabled) {
|
||||||
return null
|
return null
|
||||||
@@ -189,6 +150,10 @@ class FaviconFetcher(
|
|||||||
return if (networkResponse != null) DataSource.NETWORK else DataSource.DISK
|
return if (networkResponse != null) DataSource.NETWORK else DataSource.DISK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Response.requireBody(): ResponseBody {
|
||||||
|
return checkNotNull(body) { "response body == null" }
|
||||||
|
}
|
||||||
|
|
||||||
private fun Size.toCacheKey() = buildString {
|
private fun Size.toCacheKey() = buildString {
|
||||||
append(width.toString())
|
append(width.toString())
|
||||||
append('x')
|
append('x')
|
||||||
@@ -203,13 +168,6 @@ class FaviconFetcher(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Drawable.nonAdaptive() =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && this is AdaptiveIconDrawable) {
|
|
||||||
LayerDrawable(arrayOf(background, foreground))
|
|
||||||
} else {
|
|
||||||
this
|
|
||||||
}
|
|
||||||
|
|
||||||
class Factory(
|
class Factory(
|
||||||
context: Context,
|
context: Context,
|
||||||
okHttpClientLazy: Lazy<OkHttpClient>,
|
okHttpClientLazy: Lazy<OkHttpClient>,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.Proxy
|
import java.net.Proxy
|
||||||
import java.util.EnumSet
|
import java.util.EnumSet
|
||||||
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -154,9 +155,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
val isTrackerNotificationsEnabled: Boolean
|
val isTrackerNotificationsEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_TRACKER_NOTIFICATIONS, true)
|
get() = prefs.getBoolean(KEY_TRACKER_NOTIFICATIONS, true)
|
||||||
|
|
||||||
val isTrackerNsfwDisabled: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_TRACKER_NO_NSFW, false)
|
|
||||||
|
|
||||||
var notificationSound: Uri
|
var notificationSound: Uri
|
||||||
get() = prefs.getString(KEY_NOTIFICATIONS_SOUND, null)?.toUriOrNull()
|
get() = prefs.getString(KEY_NOTIFICATIONS_SOUND, null)?.toUriOrNull()
|
||||||
?: Settings.System.DEFAULT_NOTIFICATION_URI
|
?: Settings.System.DEFAULT_NOTIFICATION_URI
|
||||||
@@ -192,8 +190,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
get() = prefs.getBoolean(KEY_FEED_HEADER, true)
|
get() = prefs.getBoolean(KEY_FEED_HEADER, true)
|
||||||
set(value) = prefs.edit { putBoolean(KEY_FEED_HEADER, value) }
|
set(value) = prefs.edit { putBoolean(KEY_FEED_HEADER, value) }
|
||||||
|
|
||||||
val progressIndicatorMode: ProgressIndicatorMode
|
val isReadingIndicatorsEnabled: Boolean
|
||||||
get() = prefs.getEnumValue(KEY_PROGRESS_INDICATORS, ProgressIndicatorMode.PERCENT_READ)
|
get() = prefs.getBoolean(KEY_READING_INDICATORS, true)
|
||||||
|
|
||||||
val isHistoryExcludeNsfw: Boolean
|
val isHistoryExcludeNsfw: Boolean
|
||||||
get() = prefs.getBoolean(KEY_HISTORY_EXCLUDE_NSFW, false)
|
get() = prefs.getBoolean(KEY_HISTORY_EXCLUDE_NSFW, false)
|
||||||
@@ -289,15 +287,17 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
get() = prefs.getBoolean(KEY_SOURCES_GRID, true)
|
get() = prefs.getBoolean(KEY_SOURCES_GRID, true)
|
||||||
set(value) = prefs.edit { putBoolean(KEY_SOURCES_GRID, value) }
|
set(value) = prefs.edit { putBoolean(KEY_SOURCES_GRID, value) }
|
||||||
|
|
||||||
var sourcesVersion: Int
|
val isNewSourcesTipEnabled: Boolean
|
||||||
get() = prefs.getInt(KEY_SOURCES_VERSION, 0)
|
get() = prefs.getBoolean(KEY_SOURCES_NEW, true)
|
||||||
set(value) = prefs.edit { putInt(KEY_SOURCES_VERSION, value) }
|
|
||||||
|
|
||||||
val isPagesNumbersEnabled: Boolean
|
val isPagesNumbersEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false)
|
get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false)
|
||||||
|
|
||||||
val screenshotsPolicy: ScreenshotsPolicy
|
val screenshotsPolicy: ScreenshotsPolicy
|
||||||
get() = prefs.getEnumValue(KEY_SCREENSHOTS_POLICY, ScreenshotsPolicy.ALLOW)
|
get() = runCatching {
|
||||||
|
val key = prefs.getString(KEY_SCREENSHOTS_POLICY, null)?.uppercase(Locale.ROOT)
|
||||||
|
if (key == null) ScreenshotsPolicy.ALLOW else ScreenshotsPolicy.valueOf(key)
|
||||||
|
}.getOrDefault(ScreenshotsPolicy.ALLOW)
|
||||||
|
|
||||||
var userSpecifiedMangaDirectories: Set<File>
|
var userSpecifiedMangaDirectories: Set<File>
|
||||||
get() {
|
get() {
|
||||||
@@ -380,18 +380,14 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val imagesProxy: Int
|
val isImagesProxyEnabled: Boolean
|
||||||
get() {
|
get() = prefs.getBoolean(KEY_IMAGES_PROXY, false)
|
||||||
val raw = prefs.getString(KEY_IMAGES_PROXY, null)?.toIntOrNull()
|
|
||||||
return raw ?: if (prefs.getBoolean(KEY_IMAGES_PROXY_OLD, false)) 0 else -1
|
|
||||||
}
|
|
||||||
|
|
||||||
val dnsOverHttps: DoHProvider
|
val dnsOverHttps: DoHProvider
|
||||||
get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
|
get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
|
||||||
|
|
||||||
var isSSLBypassEnabled: Boolean
|
val isSSLBypassEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_SSL_BYPASS, false)
|
get() = prefs.getBoolean(KEY_SSL_BYPASS, false)
|
||||||
set(value) = prefs.edit { putBoolean(KEY_SSL_BYPASS, value) }
|
|
||||||
|
|
||||||
val proxyType: Proxy.Type
|
val proxyType: Proxy.Type
|
||||||
get() {
|
get() {
|
||||||
@@ -484,15 +480,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
val isAutoLocalChaptersCleanupEnabled: Boolean
|
val isAutoLocalChaptersCleanupEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_CHAPTERS_CLEAR_AUTO, false)
|
get() = prefs.getBoolean(KEY_CHAPTERS_CLEAR_AUTO, false)
|
||||||
|
|
||||||
fun isPagesCropEnabled(mode: ReaderMode): Boolean {
|
|
||||||
val rawValue = prefs.getStringSet(KEY_READER_CROP, emptySet())
|
|
||||||
if (rawValue.isNullOrEmpty()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val needle = if (mode == ReaderMode.WEBTOON) READER_CROP_WEBTOON else READER_CROP_PAGED
|
|
||||||
return needle.toString() in rawValue
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isTipEnabled(tip: String): Boolean {
|
fun isTipEnabled(tip: String): Boolean {
|
||||||
return prefs.getStringSet(KEY_TIPS_CLOSED, emptySet())?.contains(tip) != true
|
return prefs.getStringSet(KEY_TIPS_CLOSED, emptySet())?.contains(tip) != true
|
||||||
}
|
}
|
||||||
@@ -560,6 +547,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
const val PAGE_SWITCH_VOLUME_KEYS = "volume"
|
||||||
|
|
||||||
const val TRACK_HISTORY = "history"
|
const val TRACK_HISTORY = "history"
|
||||||
const val TRACK_FAVOURITES = "favourites"
|
const val TRACK_FAVOURITES = "favourites"
|
||||||
|
|
||||||
@@ -596,7 +585,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_TRACK_CATEGORIES = "track_categories"
|
const val KEY_TRACK_CATEGORIES = "track_categories"
|
||||||
const val KEY_TRACK_WARNING = "track_warning"
|
const val KEY_TRACK_WARNING = "track_warning"
|
||||||
const val KEY_TRACKER_NOTIFICATIONS = "tracker_notifications"
|
const val KEY_TRACKER_NOTIFICATIONS = "tracker_notifications"
|
||||||
const val KEY_TRACKER_NO_NSFW = "tracker_no_nsfw"
|
|
||||||
const val KEY_NOTIFICATIONS_SETTINGS = "notifications_settings"
|
const val KEY_NOTIFICATIONS_SETTINGS = "notifications_settings"
|
||||||
const val KEY_NOTIFICATIONS_SOUND = "notifications_sound"
|
const val KEY_NOTIFICATIONS_SOUND = "notifications_sound"
|
||||||
const val KEY_NOTIFICATIONS_VIBRATE = "notifications_vibrate"
|
const val KEY_NOTIFICATIONS_VIBRATE = "notifications_vibrate"
|
||||||
@@ -605,11 +593,11 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_READER_ANIMATION = "reader_animation2"
|
const val KEY_READER_ANIMATION = "reader_animation2"
|
||||||
const val KEY_READER_MODE = "reader_mode"
|
const val KEY_READER_MODE = "reader_mode"
|
||||||
const val KEY_READER_MODE_DETECT = "reader_mode_detect"
|
const val KEY_READER_MODE_DETECT = "reader_mode_detect"
|
||||||
const val KEY_READER_CROP = "reader_crop"
|
|
||||||
const val KEY_APP_PASSWORD = "app_password"
|
const val KEY_APP_PASSWORD = "app_password"
|
||||||
const val KEY_APP_PASSWORD_NUMERIC = "app_password_num"
|
const val KEY_APP_PASSWORD_NUMERIC = "app_password_num"
|
||||||
const val KEY_PROTECT_APP = "protect_app"
|
const val KEY_PROTECT_APP = "protect_app"
|
||||||
const val KEY_PROTECT_APP_BIOMETRIC = "protect_app_bio"
|
const val KEY_PROTECT_APP_BIOMETRIC = "protect_app_bio"
|
||||||
|
const val KEY_APP_VERSION = "app_version"
|
||||||
const val KEY_ZOOM_MODE = "zoom_mode"
|
const val KEY_ZOOM_MODE = "zoom_mode"
|
||||||
const val KEY_BACKUP = "backup"
|
const val KEY_BACKUP = "backup"
|
||||||
const val KEY_RESTORE = "restore"
|
const val KEY_RESTORE = "restore"
|
||||||
@@ -619,7 +607,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_BACKUP_PERIODICAL_LAST = "backup_periodic_last"
|
const val KEY_BACKUP_PERIODICAL_LAST = "backup_periodic_last"
|
||||||
const val KEY_HISTORY_GROUPING = "history_grouping"
|
const val KEY_HISTORY_GROUPING = "history_grouping"
|
||||||
const val KEY_UPDATED_GROUPING = "updated_grouping"
|
const val KEY_UPDATED_GROUPING = "updated_grouping"
|
||||||
const val KEY_PROGRESS_INDICATORS = "progress_indicators"
|
const val KEY_READING_INDICATORS = "reading_indicators"
|
||||||
const val KEY_REVERSE_CHAPTERS = "reverse_chapters"
|
const val KEY_REVERSE_CHAPTERS = "reverse_chapters"
|
||||||
const val KEY_GRID_VIEW_CHAPTERS = "grid_view_chapters"
|
const val KEY_GRID_VIEW_CHAPTERS = "grid_view_chapters"
|
||||||
const val KEY_HISTORY_EXCLUDE_NSFW = "history_exclude_nsfw"
|
const val KEY_HISTORY_EXCLUDE_NSFW = "history_exclude_nsfw"
|
||||||
@@ -659,7 +647,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_PREFETCH_CONTENT = "prefetch_content"
|
const val KEY_PREFETCH_CONTENT = "prefetch_content"
|
||||||
const val KEY_APP_LOCALE = "app_locale"
|
const val KEY_APP_LOCALE = "app_locale"
|
||||||
const val KEY_LOGGING_ENABLED = "logging"
|
const val KEY_LOGGING_ENABLED = "logging"
|
||||||
|
const val KEY_LOGS_SHARE = "logs_share"
|
||||||
const val KEY_SOURCES_GRID = "sources_grid"
|
const val KEY_SOURCES_GRID = "sources_grid"
|
||||||
|
const val KEY_SOURCES_NEW = "sources_new"
|
||||||
const val KEY_UPDATES_UNSTABLE = "updates_unstable"
|
const val KEY_UPDATES_UNSTABLE = "updates_unstable"
|
||||||
const val KEY_TIPS_CLOSED = "tips_closed"
|
const val KEY_TIPS_CLOSED = "tips_closed"
|
||||||
const val KEY_SSL_BYPASS = "ssl_bypass"
|
const val KEY_SSL_BYPASS = "ssl_bypass"
|
||||||
@@ -672,7 +662,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_PROXY_AUTH = "proxy_auth"
|
const val KEY_PROXY_AUTH = "proxy_auth"
|
||||||
const val KEY_PROXY_LOGIN = "proxy_login"
|
const val KEY_PROXY_LOGIN = "proxy_login"
|
||||||
const val KEY_PROXY_PASSWORD = "proxy_password"
|
const val KEY_PROXY_PASSWORD = "proxy_password"
|
||||||
const val KEY_IMAGES_PROXY = "images_proxy_2"
|
const val KEY_IMAGES_PROXY = "images_proxy"
|
||||||
const val KEY_LOCAL_MANGA_DIRS = "local_manga_dirs"
|
const val KEY_LOCAL_MANGA_DIRS = "local_manga_dirs"
|
||||||
const val KEY_DISABLE_NSFW = "no_nsfw"
|
const val KEY_DISABLE_NSFW = "no_nsfw"
|
||||||
const val KEY_RELATED_MANGA = "related_manga"
|
const val KEY_RELATED_MANGA = "related_manga"
|
||||||
@@ -686,6 +676,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_CF_CONTRAST = "cf_contrast"
|
const val KEY_CF_CONTRAST = "cf_contrast"
|
||||||
const val KEY_CF_INVERTED = "cf_inverted"
|
const val KEY_CF_INVERTED = "cf_inverted"
|
||||||
const val KEY_CF_GRAYSCALE = "cf_grayscale"
|
const val KEY_CF_GRAYSCALE = "cf_grayscale"
|
||||||
|
const val KEY_IGNORE_DOZE = "ignore_dose"
|
||||||
const val KEY_PAGES_TAB = "pages_tab"
|
const val KEY_PAGES_TAB = "pages_tab"
|
||||||
const val KEY_DETAILS_TAB = "details_tab"
|
const val KEY_DETAILS_TAB = "details_tab"
|
||||||
const val KEY_DETAILS_LAST_TAB = "details_last_tab"
|
const val KEY_DETAILS_LAST_TAB = "details_last_tab"
|
||||||
@@ -693,24 +684,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_PAGES_SAVE_DIR = "pages_dir"
|
const val KEY_PAGES_SAVE_DIR = "pages_dir"
|
||||||
const val KEY_PAGES_SAVE_ASK = "pages_dir_ask"
|
const val KEY_PAGES_SAVE_ASK = "pages_dir_ask"
|
||||||
const val KEY_STATS_ENABLED = "stats_on"
|
const val KEY_STATS_ENABLED = "stats_on"
|
||||||
const val KEY_FEED_HEADER = "feed_header"
|
|
||||||
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
|
|
||||||
const val KEY_SOURCES_VERSION = "sources_version"
|
|
||||||
|
|
||||||
// keys for non-persistent preferences
|
|
||||||
const val KEY_APP_VERSION = "app_version"
|
|
||||||
const val KEY_IGNORE_DOZE = "ignore_dose"
|
|
||||||
const val KEY_TRACKER_DEBUG = "tracker_debug"
|
|
||||||
const val KEY_LOGS_SHARE = "logs_share"
|
|
||||||
const val KEY_APP_UPDATE = "app_update"
|
const val KEY_APP_UPDATE = "app_update"
|
||||||
const val KEY_APP_TRANSLATION = "about_app_translation"
|
const val KEY_APP_TRANSLATION = "about_app_translation"
|
||||||
const val PROXY_TEST = "proxy_test"
|
const val KEY_FEED_HEADER = "feed_header"
|
||||||
|
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
|
||||||
// old keys are for migration only
|
|
||||||
private const val KEY_IMAGES_PROXY_OLD = "images_proxy"
|
|
||||||
|
|
||||||
// values
|
|
||||||
private const val READER_CROP_PAGED = 1
|
|
||||||
private const val READER_CROP_WEBTOON = 2
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.prefs
|
|
||||||
|
|
||||||
enum class ProgressIndicatorMode {
|
|
||||||
|
|
||||||
NONE, PERCENT_READ, PERCENT_LEFT, CHAPTERS_READ, CHAPTERS_LEFT;
|
|
||||||
}
|
|
||||||
@@ -3,5 +3,5 @@ package org.koitharu.kotatsu.core.prefs
|
|||||||
enum class ScreenshotsPolicy {
|
enum class ScreenshotsPolicy {
|
||||||
|
|
||||||
// Do not rename this
|
// Do not rename this
|
||||||
ALLOW, BLOCK_NSFW, BLOCK_INCOGNITO, BLOCK_ALL;
|
ALLOW, BLOCK_NSFW, BLOCK_ALL;
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,5 @@ enum class SearchSuggestionType(
|
|||||||
QUERIES_SUGGEST(R.string.suggested_queries),
|
QUERIES_SUGGEST(R.string.suggested_queries),
|
||||||
MANGA(R.string.content_type_manga),
|
MANGA(R.string.content_type_manga),
|
||||||
SOURCES(R.string.remote_sources),
|
SOURCES(R.string.remote_sources),
|
||||||
RECENT_SOURCES(R.string.recent_sources),
|
|
||||||
AUTHORS(R.string.authors),
|
AUTHORS(R.string.authors),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.core.prefs
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
|
import okhttp3.internal.isSensitiveHeader
|
||||||
import org.koitharu.kotatsu.core.util.ext.getEnumValue
|
import org.koitharu.kotatsu.core.util.ext.getEnumValue
|
||||||
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
||||||
import org.koitharu.kotatsu.core.util.ext.putEnumValue
|
import org.koitharu.kotatsu.core.util.ext.putEnumValue
|
||||||
@@ -11,7 +12,6 @@ import org.koitharu.kotatsu.parsers.config.ConfigKey
|
|||||||
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
import org.koitharu.kotatsu.settings.utils.validation.DomainValidator
|
|
||||||
|
|
||||||
class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig {
|
class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig {
|
||||||
|
|
||||||
@@ -31,14 +31,9 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig
|
|||||||
.ifNullOrEmpty { key.defaultValue }
|
.ifNullOrEmpty { key.defaultValue }
|
||||||
.sanitizeHeaderValue()
|
.sanitizeHeaderValue()
|
||||||
|
|
||||||
is ConfigKey.Domain -> prefs.getString(key.key, key.defaultValue)
|
is ConfigKey.Domain -> prefs.getString(key.key, key.defaultValue).ifNullOrEmpty { key.defaultValue }
|
||||||
?.trim()
|
|
||||||
?.takeIf { DomainValidator.isValidDomain(it) }
|
|
||||||
?: key.defaultValue
|
|
||||||
|
|
||||||
is ConfigKey.ShowSuspiciousContent -> prefs.getBoolean(key.key, key.defaultValue)
|
is ConfigKey.ShowSuspiciousContent -> prefs.getBoolean(key.key, key.defaultValue)
|
||||||
is ConfigKey.SplitByTranslations -> prefs.getBoolean(key.key, key.defaultValue)
|
is ConfigKey.SplitByTranslations -> prefs.getBoolean(key.key, key.defaultValue)
|
||||||
is ConfigKey.PreferredImageServer -> prefs.getString(key.key, key.defaultValue)?.takeUnless(String::isEmpty)
|
|
||||||
} as T
|
} as T
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +43,6 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig
|
|||||||
is ConfigKey.ShowSuspiciousContent -> putBoolean(key.key, value as Boolean)
|
is ConfigKey.ShowSuspiciousContent -> putBoolean(key.key, value as Boolean)
|
||||||
is ConfigKey.UserAgent -> putString(key.key, (value as String?)?.sanitizeHeaderValue())
|
is ConfigKey.UserAgent -> putString(key.key, (value as String?)?.sanitizeHeaderValue())
|
||||||
is ConfigKey.SplitByTranslations -> putBoolean(key.key, value as Boolean)
|
is ConfigKey.SplitByTranslations -> putBoolean(key.key, value as Boolean)
|
||||||
is ConfigKey.PreferredImageServer -> putString(key.key, value as String? ?: "")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ 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
|
||||||
@@ -19,8 +18,6 @@ import dagger.hilt.EntryPoint
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.EntryPointAccessors
|
import dagger.hilt.android.EntryPointAccessors
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.flowOf
|
|
||||||
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
|
||||||
@@ -28,12 +25,10 @@ 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.WindowInsetsDelegate
|
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
|
||||||
import org.koitharu.kotatsu.core.util.ext.isWebViewUnavailable
|
import org.koitharu.kotatsu.core.util.ext.isWebViewUnavailable
|
||||||
import org.koitharu.kotatsu.main.ui.protect.ScreenshotPolicyHelper
|
|
||||||
|
|
||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis")
|
||||||
abstract class BaseActivity<B : ViewBinding> :
|
abstract class BaseActivity<B : ViewBinding> :
|
||||||
AppCompatActivity(),
|
AppCompatActivity(),
|
||||||
ScreenshotPolicyHelper.ContentContainer,
|
|
||||||
WindowInsetsDelegate.WindowInsetsListener {
|
WindowInsetsDelegate.WindowInsetsListener {
|
||||||
|
|
||||||
private var isAmoledTheme = false
|
private var isAmoledTheme = false
|
||||||
@@ -97,20 +92,10 @@ abstract class BaseActivity<B : ViewBinding> :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSupportNavigateUp(): Boolean {
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
if (supportFragmentManager.popBackStackImmediate()) {
|
||||||
// TODO fix behavior on Android 14
|
|
||||||
dispatchNavigateUp()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
val fm = supportFragmentManager
|
|
||||||
if (fm.isStateSaved) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (fm.backStackEntryCount > 0) {
|
dispatchNavigateUp()
|
||||||
fm.popBackStack()
|
|
||||||
} else {
|
|
||||||
dispatchNavigateUp()
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,8 +140,6 @@ abstract class BaseActivity<B : ViewBinding> :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isNsfwContent(): Flow<Boolean> = flowOf(false)
|
|
||||||
|
|
||||||
private fun putDataToExtras(intent: Intent?) {
|
private fun putDataToExtras(intent: Intent?) {
|
||||||
intent?.putExtra(EXTRA_DATA, intent.data)
|
intent?.putExtra(EXTRA_DATA, intent.data)
|
||||||
}
|
}
|
||||||
@@ -176,8 +159,6 @@ abstract class BaseActivity<B : ViewBinding> :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun hasViewBinding() = ::viewBinding.isInitialized
|
|
||||||
|
|
||||||
@EntryPoint
|
@EntryPoint
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface BaseActivityEntryPoint {
|
interface BaseActivityEntryPoint {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun setTitle(title: CharSequence?) {
|
protected fun setTitle(title: CharSequence?) {
|
||||||
(activity as? SettingsActivity)?.setSectionTitle(title)
|
(activity as? SettingsActivity)?.setSectionTitle(title)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ abstract class BaseViewModel : ViewModel() {
|
|||||||
errorEvent.call(error)
|
errorEvent.call(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected inline fun <T> withLoading(block: () -> T): T = try {
|
protected inline suspend fun <T> withLoading(block: () -> T): T = try {
|
||||||
loadingCounter.increment()
|
loadingCounter.increment()
|
||||||
block()
|
block()
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.ui.image
|
|
||||||
|
|
||||||
import android.animation.TimeAnimator
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.drawable.Animatable
|
|
||||||
import androidx.annotation.StyleRes
|
|
||||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
|
||||||
import com.google.android.material.animation.ArgbEvaluatorCompat
|
|
||||||
import com.google.android.material.color.MaterialColors
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.core.util.KotatsuColors
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
|
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
class AnimatedFaviconDrawable(
|
|
||||||
context: Context,
|
|
||||||
@StyleRes styleResId: Int,
|
|
||||||
name: String,
|
|
||||||
) : FaviconDrawable(context, styleResId, name), Animatable, TimeAnimator.TimeListener {
|
|
||||||
|
|
||||||
private val interpolator = FastOutSlowInInterpolator()
|
|
||||||
private val period = context.getAnimationDuration(R.integer.config_longAnimTime) * 2
|
|
||||||
private val timeAnimator = TimeAnimator()
|
|
||||||
|
|
||||||
private val colorHigh = MaterialColors.harmonize(KotatsuColors.random(name), colorBackground)
|
|
||||||
private val colorLow = ArgbEvaluatorCompat.getInstance().evaluate(0.3f, colorHigh, colorBackground)
|
|
||||||
|
|
||||||
init {
|
|
||||||
timeAnimator.setTimeListener(this)
|
|
||||||
updateColor()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun draw(canvas: Canvas) {
|
|
||||||
if (!isRunning && period > 0) {
|
|
||||||
updateColor()
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
super.draw(canvas)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setAlpha(alpha: Int) = Unit
|
|
||||||
|
|
||||||
override fun getAlpha(): Int = 255
|
|
||||||
|
|
||||||
override fun onTimeUpdate(animation: TimeAnimator?, totalTime: Long, deltaTime: Long) {
|
|
||||||
callback?.also {
|
|
||||||
updateColor()
|
|
||||||
it.invalidateDrawable(this)
|
|
||||||
} ?: stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun start() {
|
|
||||||
timeAnimator.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stop() {
|
|
||||||
timeAnimator.end()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isRunning(): Boolean = timeAnimator.isStarted
|
|
||||||
|
|
||||||
private fun updateColor() {
|
|
||||||
if (period <= 0f) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val ph = period / 2
|
|
||||||
val fraction = abs((System.currentTimeMillis() % period) - ph) / ph.toFloat()
|
|
||||||
colorForeground = ArgbEvaluatorCompat.getInstance()
|
|
||||||
.evaluate(interpolator.getInterpolation(fraction), colorLow, colorHigh)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,18 +17,18 @@ import com.google.android.material.color.MaterialColors
|
|||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.util.KotatsuColors
|
import org.koitharu.kotatsu.core.util.KotatsuColors
|
||||||
|
|
||||||
open class FaviconDrawable(
|
class FaviconDrawable(
|
||||||
context: Context,
|
context: Context,
|
||||||
@StyleRes styleResId: Int,
|
@StyleRes styleResId: Int,
|
||||||
name: String,
|
name: String,
|
||||||
) : Drawable() {
|
) : Drawable() {
|
||||||
|
|
||||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||||
protected var colorBackground = Color.WHITE
|
private var colorBackground = Color.WHITE
|
||||||
protected var colorForeground = Color.DKGRAY
|
|
||||||
private var colorStroke = Color.LTGRAY
|
private var colorStroke = Color.LTGRAY
|
||||||
private val letter = name.take(1).uppercase()
|
private val letter = name.take(1).uppercase()
|
||||||
private var cornerSize = 0f
|
private var cornerSize = 0f
|
||||||
|
private var colorForeground = Color.DKGRAY
|
||||||
private val textBounds = Rect()
|
private val textBounds = Rect()
|
||||||
private val tempRect = Rect()
|
private val tempRect = Rect()
|
||||||
private val boundsF = RectF()
|
private val boundsF = RectF()
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
package org.koitharu.kotatsu.core.ui.image
|
package org.koitharu.kotatsu.core.ui.image
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.core.graphics.alpha
|
||||||
|
import androidx.core.graphics.blue
|
||||||
import androidx.core.graphics.get
|
import androidx.core.graphics.get
|
||||||
|
import androidx.core.graphics.green
|
||||||
|
import androidx.core.graphics.red
|
||||||
import coil.size.Size
|
import coil.size.Size
|
||||||
import coil.transform.Transformation
|
import coil.transform.Transformation
|
||||||
import org.koitharu.kotatsu.reader.domain.EdgeDetector.Companion.isColorTheSame
|
import kotlin.math.abs
|
||||||
|
|
||||||
class TrimTransformation(
|
class TrimTransformation(
|
||||||
private val tolerance: Int = 20,
|
private val tolerance: Int = 20,
|
||||||
@@ -23,7 +28,7 @@ class TrimTransformation(
|
|||||||
var isColBlank = true
|
var isColBlank = true
|
||||||
val prevColor = input[x, 0]
|
val prevColor = input[x, 0]
|
||||||
for (y in 1 until input.height) {
|
for (y in 1 until input.height) {
|
||||||
if (!isColorTheSame(input[x, y], prevColor, tolerance)) {
|
if (!isColorTheSame(input[x, y], prevColor)) {
|
||||||
isColBlank = false
|
isColBlank = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -42,7 +47,7 @@ class TrimTransformation(
|
|||||||
var isColBlank = true
|
var isColBlank = true
|
||||||
val prevColor = input[x, 0]
|
val prevColor = input[x, 0]
|
||||||
for (y in 1 until input.height) {
|
for (y in 1 until input.height) {
|
||||||
if (!isColorTheSame(input[x, y], prevColor, tolerance)) {
|
if (!isColorTheSame(input[x, y], prevColor)) {
|
||||||
isColBlank = false
|
isColBlank = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -58,7 +63,7 @@ class TrimTransformation(
|
|||||||
var isRowBlank = true
|
var isRowBlank = true
|
||||||
val prevColor = input[0, y]
|
val prevColor = input[0, y]
|
||||||
for (x in 1 until input.width) {
|
for (x in 1 until input.width) {
|
||||||
if (!isColorTheSame(input[x, y], prevColor, tolerance)) {
|
if (!isColorTheSame(input[x, y], prevColor)) {
|
||||||
isRowBlank = false
|
isRowBlank = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -74,7 +79,7 @@ class TrimTransformation(
|
|||||||
var isRowBlank = true
|
var isRowBlank = true
|
||||||
val prevColor = input[0, y]
|
val prevColor = input[0, y]
|
||||||
for (x in 1 until input.width) {
|
for (x in 1 until input.width) {
|
||||||
if (!isColorTheSame(input[x, y], prevColor, tolerance)) {
|
if (!isColorTheSame(input[x, y], prevColor)) {
|
||||||
isRowBlank = false
|
isRowBlank = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -93,6 +98,13 @@ class TrimTransformation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isColorTheSame(@ColorInt a: Int, @ColorInt b: Int): Boolean {
|
||||||
|
return abs(a.red - b.red) <= tolerance &&
|
||||||
|
abs(a.green - b.green) <= tolerance &&
|
||||||
|
abs(a.blue - b.blue) <= tolerance &&
|
||||||
|
abs(a.alpha - b.alpha) <= tolerance
|
||||||
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
return this === other || (other is TrimTransformation && other.tolerance == tolerance)
|
return this === other || (other is TrimTransformation && other.tolerance == tolerance)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package org.koitharu.kotatsu.core.ui.list
|
package org.koitharu.kotatsu.core.ui.list
|
||||||
|
|
||||||
|
import android.app.Notification.Action
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.collection.LongSet
|
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
@@ -14,8 +14,6 @@ import androidx.savedstate.SavedStateRegistry
|
|||||||
import androidx.savedstate.SavedStateRegistryOwner
|
import androidx.savedstate.SavedStateRegistryOwner
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import org.koitharu.kotatsu.core.ui.list.decor.AbstractSelectionItemDecoration
|
import org.koitharu.kotatsu.core.ui.list.decor.AbstractSelectionItemDecoration
|
||||||
import org.koitharu.kotatsu.core.util.ext.toLongArray
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.toSet
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
private const val KEY_SELECTION = "selection"
|
private const val KEY_SELECTION = "selection"
|
||||||
@@ -37,9 +35,11 @@ class ListSelectionController(
|
|||||||
registryOwner.lifecycle.addObserver(StateEventObserver())
|
registryOwner.lifecycle.addObserver(StateEventObserver())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun snapshot(): Set<Long> = peekCheckedIds().toSet()
|
fun snapshot(): Set<Long> {
|
||||||
|
return peekCheckedIds().toSet()
|
||||||
|
}
|
||||||
|
|
||||||
fun peekCheckedIds(): LongSet {
|
fun peekCheckedIds(): Set<Long> {
|
||||||
return decoration.checkedItemsIds
|
return decoration.checkedItemsIds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import android.graphics.Canvas
|
|||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.graphics.RectF
|
import android.graphics.RectF
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.collection.LongSet
|
|
||||||
import androidx.collection.MutableLongSet
|
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.RecyclerView.NO_ID
|
import androidx.recyclerview.widget.RecyclerView.NO_ID
|
||||||
@@ -14,7 +12,7 @@ abstract class AbstractSelectionItemDecoration : RecyclerView.ItemDecoration() {
|
|||||||
|
|
||||||
private val bounds = Rect()
|
private val bounds = Rect()
|
||||||
private val boundsF = RectF()
|
private val boundsF = RectF()
|
||||||
protected val selection = MutableLongSet()
|
protected val selection = HashSet<Long>()
|
||||||
|
|
||||||
protected var hasBackground: Boolean = true
|
protected var hasBackground: Boolean = true
|
||||||
protected var hasForeground: Boolean = false
|
protected var hasForeground: Boolean = false
|
||||||
@@ -23,7 +21,7 @@ abstract class AbstractSelectionItemDecoration : RecyclerView.ItemDecoration() {
|
|||||||
val checkedItemsCount: Int
|
val checkedItemsCount: Int
|
||||||
get() = selection.size
|
get() = selection.size
|
||||||
|
|
||||||
val checkedItemsIds: LongSet
|
val checkedItemsIds: Set<Long>
|
||||||
get() = selection
|
get() = selection
|
||||||
|
|
||||||
fun toggleItemChecked(id: Long) {
|
fun toggleItemChecked(id: Long) {
|
||||||
@@ -41,9 +39,7 @@ abstract class AbstractSelectionItemDecoration : RecyclerView.ItemDecoration() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun checkAll(ids: Collection<Long>) {
|
fun checkAll(ids: Collection<Long>) {
|
||||||
for (id in ids) {
|
selection.addAll(ids)
|
||||||
selection.add(id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearSelection() {
|
fun clearSelection() {
|
||||||
|
|||||||
@@ -52,16 +52,6 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
|||||||
fastScroller.visibility = if (isFastScrollerEnabled) visibility else GONE
|
fastScroller.visibility = if (isFastScrollerEnabled) visibility else GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
|
|
||||||
super.setPadding(left, top, right, bottom)
|
|
||||||
fastScroller.setPadding(left, top, right, bottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) {
|
|
||||||
super.setPaddingRelative(start, top, end, bottom)
|
|
||||||
fastScroller.setPaddingRelative(start, top, end, bottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
super.onAttachedToWindow()
|
super.onAttachedToWindow()
|
||||||
fastScroller.attachRecyclerView(this)
|
fastScroller.attachRecyclerView(this)
|
||||||
|
|||||||
@@ -1,34 +1,22 @@
|
|||||||
package org.koitharu.kotatsu.core.ui.sheet
|
package org.koitharu.kotatsu.core.ui.sheet
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.activity.BackEventCompat
|
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
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 com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
|
|
||||||
|
|
||||||
class BottomSheetCollapseCallback(
|
class BottomSheetCollapseCallback(
|
||||||
private val sheet: ViewGroup,
|
private val behavior: BottomSheetBehavior<*>,
|
||||||
private val behavior: BottomSheetBehavior<*> = BottomSheetBehavior.from(sheet),
|
) : OnBackPressedCallback(behavior.state == STATE_EXPANDED) {
|
||||||
) : OnBackPressedCallback(behavior.state == STATE_EXPANDED || behavior.state == STATE_HALF_EXPANDED) {
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
behavior.addBottomSheetCallback(
|
behavior.addBottomSheetCallback(
|
||||||
object : BottomSheetBehavior.BottomSheetCallback() {
|
object : BottomSheetBehavior.BottomSheetCallback() {
|
||||||
|
|
||||||
@SuppressLint("SwitchIntDef")
|
|
||||||
override fun onStateChanged(view: View, state: Int) {
|
override fun onStateChanged(view: View, state: Int) {
|
||||||
when (state) {
|
isEnabled = state == STATE_EXPANDED || state == STATE_HALF_EXPANDED
|
||||||
STATE_EXPANDED,
|
|
||||||
STATE_HALF_EXPANDED -> isEnabled = true
|
|
||||||
|
|
||||||
STATE_COLLAPSED,
|
|
||||||
STATE_HIDDEN -> isEnabled = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSlide(p0: View, p1: Float) = Unit
|
override fun onSlide(p0: View, p1: Float) = Unit
|
||||||
@@ -36,11 +24,7 @@ class BottomSheetCollapseCallback(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleOnBackPressed() = behavior.handleBackInvoked()
|
override fun handleOnBackPressed() {
|
||||||
|
behavior.state = STATE_COLLAPSED
|
||||||
override fun handleOnBackCancelled() = behavior.cancelBackProgress()
|
}
|
||||||
|
|
||||||
override fun handleOnBackProgressed(backEvent: BackEventCompat) = behavior.updateBackProgress(backEvent)
|
|
||||||
|
|
||||||
override fun handleOnBackStarted(backEvent: BackEventCompat) = behavior.startBackProgress(backEvent)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ import com.google.android.material.chip.ChipGroup
|
|||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.util.ext.castOrNull
|
import org.koitharu.kotatsu.core.util.ext.castOrNull
|
||||||
|
|
||||||
import com.google.android.material.R as materialR
|
|
||||||
|
|
||||||
class ChipsView @JvmOverloads constructor(
|
class ChipsView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
@@ -26,9 +24,7 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
onChipClickListener?.onChipClick(it as Chip, it.tag)
|
onChipClickListener?.onChipClick(it as Chip, it.tag)
|
||||||
}
|
}
|
||||||
private val chipOnCloseListener = OnClickListener {
|
private val chipOnCloseListener = OnClickListener {
|
||||||
val chip = it as Chip
|
onChipCloseClickListener?.onChipCloseClick(it as Chip, it.tag)
|
||||||
val data = it.tag
|
|
||||||
onChipCloseClickListener?.onChipCloseClick(chip, data) ?: onChipClickListener?.onChipClick(chip, data)
|
|
||||||
}
|
}
|
||||||
private val chipStyle: Int
|
private val chipStyle: Int
|
||||||
var onChipClickListener: OnChipClickListener? = null
|
var onChipClickListener: OnChipClickListener? = null
|
||||||
@@ -52,7 +48,7 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
if (isInEditMode) {
|
if (isInEditMode) {
|
||||||
setChips(
|
setChips(
|
||||||
List(5) {
|
List(5) {
|
||||||
ChipModel(title = "Chip $it")
|
ChipModel(0, "Chip $it", 0, isCheckable = false, isChecked = false)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -103,15 +99,6 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
chip.isChipIconVisible = true
|
chip.isChipIconVisible = true
|
||||||
}
|
}
|
||||||
chip.isChecked = model.isChecked
|
chip.isChecked = model.isChecked
|
||||||
chip.isCheckedIconVisible = chip.isCheckable && model.icon == 0
|
|
||||||
chip.isCloseIconVisible = if (onChipCloseClickListener != null || model.isDropdown) {
|
|
||||||
chip.setCloseIconResource(
|
|
||||||
if (model.isDropdown) R.drawable.ic_expand_more else materialR.drawable.ic_m3_chip_close,
|
|
||||||
)
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
chip.tag = model.data
|
chip.tag = model.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,11 +106,12 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
val chip = Chip(context)
|
val chip = Chip(context)
|
||||||
val drawable = ChipDrawable.createFromAttributes(context, null, 0, chipStyle)
|
val drawable = ChipDrawable.createFromAttributes(context, null, 0, chipStyle)
|
||||||
chip.setChipDrawable(drawable)
|
chip.setChipDrawable(drawable)
|
||||||
|
chip.isCheckedIconVisible = true
|
||||||
chip.isChipIconVisible = false
|
chip.isChipIconVisible = false
|
||||||
|
chip.isCloseIconVisible = onChipCloseClickListener != null
|
||||||
chip.setOnCloseIconClickListener(chipOnCloseListener)
|
chip.setOnCloseIconClickListener(chipOnCloseListener)
|
||||||
chip.setEnsureMinTouchTargetSize(false)
|
chip.setEnsureMinTouchTargetSize(false)
|
||||||
chip.setOnClickListener(chipOnClickListener)
|
chip.setOnClickListener(chipOnClickListener)
|
||||||
chip.isElegantTextHeight = false
|
|
||||||
addView(chip)
|
addView(chip)
|
||||||
return chip
|
return chip
|
||||||
}
|
}
|
||||||
@@ -139,12 +127,11 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class ChipModel(
|
data class ChipModel(
|
||||||
|
@ColorRes val tint: Int,
|
||||||
val title: CharSequence,
|
val title: CharSequence,
|
||||||
@DrawableRes val icon: Int = 0,
|
@DrawableRes val icon: Int,
|
||||||
val isCheckable: Boolean = false,
|
val isCheckable: Boolean,
|
||||||
@ColorRes val tint: Int = 0,
|
val isChecked: Boolean,
|
||||||
val isChecked: Boolean = false,
|
|
||||||
val isDropdown: Boolean = false,
|
|
||||||
val data: Any? = null,
|
val data: Any? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -23,16 +23,11 @@ class SelectableTextView @JvmOverloads constructor(
|
|||||||
private fun fixSelectionRange() {
|
private fun fixSelectionRange() {
|
||||||
if (selectionStart < 0 || selectionEnd < 0) {
|
if (selectionStart < 0 || selectionEnd < 0) {
|
||||||
val spannableText = text as? Spannable ?: return
|
val spannableText = text as? Spannable ?: return
|
||||||
Selection.setSelection(spannableText, spannableText.length)
|
Selection.setSelection(spannableText, text.length)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun scrollTo(x: Int, y: Int) {
|
override fun scrollTo(x: Int, y: Int) {
|
||||||
super.scrollTo(0, 0)
|
super.scrollTo(0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectAll() {
|
|
||||||
val spannableText = text as? Spannable ?: return
|
|
||||||
Selection.selectAll(spannableText)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,14 @@
|
|||||||
package org.koitharu.kotatsu.core.util
|
package org.koitharu.kotatsu.core.util
|
||||||
|
|
||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
import org.koitharu.kotatsu.core.util.ext.iterator
|
import org.koitharu.kotatsu.core.util.ext.map
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class LocaleComparator : Comparator<Locale> {
|
class LocaleComparator : Comparator<Locale> {
|
||||||
|
|
||||||
private val deviceLocales: List<String>
|
private val deviceLocales = LocaleListCompat.getAdjustedDefault()//LocaleManagerCompat.getSystemLocales(context)
|
||||||
|
.map { it.language }
|
||||||
init {
|
.distinct()
|
||||||
val localeList = LocaleListCompat.getAdjustedDefault()
|
|
||||||
deviceLocales = buildList(localeList.size() + 1) {
|
|
||||||
add("")
|
|
||||||
val set = HashSet<String>(localeList.size() + 1)
|
|
||||||
set.add("")
|
|
||||||
for (locale in localeList) {
|
|
||||||
val lang = locale.language
|
|
||||||
if (set.add(lang)) {
|
|
||||||
add(lang)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun compare(a: Locale, b: Locale): Int {
|
override fun compare(a: Locale, b: Locale): Int {
|
||||||
val indexA = deviceLocales.indexOf(a.language)
|
val indexA = deviceLocales.indexOf(a.language)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package org.koitharu.kotatsu.core.util.ext
|
|||||||
|
|
||||||
import androidx.collection.ArrayMap
|
import androidx.collection.ArrayMap
|
||||||
import androidx.collection.ArraySet
|
import androidx.collection.ArraySet
|
||||||
import androidx.collection.LongSet
|
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.EnumSet
|
import java.util.EnumSet
|
||||||
@@ -70,24 +69,4 @@ fun <T> Iterable<T>.sortedWithSafe(comparator: Comparator<in T>): List<T> = try
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Collection<*>?.sizeOrZero() = this?.size ?: 0
|
fun Collection<*>?.sizeOrZero() = if (this == null) 0 else size
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
inline fun <T, reified R> Collection<T>.mapToArray(transform: (T) -> R): Array<R> {
|
|
||||||
val result = arrayOfNulls<R>(size)
|
|
||||||
forEachIndexed { index, t -> result[index] = transform(t) }
|
|
||||||
return result as Array<R>
|
|
||||||
}
|
|
||||||
|
|
||||||
fun LongSet.toLongArray(): LongArray {
|
|
||||||
val result = LongArray(size)
|
|
||||||
var i = 0
|
|
||||||
forEach { result[i++] = it }
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
fun LongSet.toSet(): Set<Long> = toCollection(ArraySet<Long>(size))
|
|
||||||
|
|
||||||
fun <R : MutableCollection<Long>> LongSet.toCollection(out: R): R = out.also { result ->
|
|
||||||
forEach(result::add)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,16 +12,12 @@ import kotlinx.coroutines.CancellableContinuation
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.joinAll
|
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import org.koitharu.kotatsu.core.util.AcraCoroutineErrorHandler
|
import org.koitharu.kotatsu.core.util.AcraCoroutineErrorHandler
|
||||||
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
|
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
|
||||||
import org.koitharu.kotatsu.parsers.util.cancelAll
|
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
@@ -94,10 +90,3 @@ fun <T> Deferred<T>.peek(): T? = if (isCompleted) {
|
|||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("SuspendFunctionOnCoroutineScope")
|
|
||||||
suspend fun CoroutineScope.cancelChildrenAndJoin(cause: CancellationException? = null) {
|
|
||||||
val jobs = coroutineContext[Job]?.children?.toList() ?: return
|
|
||||||
jobs.cancelAll(cause)
|
|
||||||
jobs.joinAll()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -37,5 +37,3 @@ fun JSONObject.toContentValues(): ContentValues {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun String.escapeName() = "`$this`"
|
private fun String.escapeName() = "`$this`"
|
||||||
|
|
||||||
fun Cursor.getBoolean(columnIndex: Int) = getInt(columnIndex) > 0
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@@ -12,9 +11,3 @@ fun Rect.scale(factor: Double) {
|
|||||||
(height() - newHeight) / 2,
|
(height() - newHeight) / 2,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <R> Bitmap.use(block: (Bitmap) -> R) = try {
|
|
||||||
block(this)
|
|
||||||
} finally {
|
|
||||||
recycle()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
import okhttp3.Cookie
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.ResponseBody
|
|
||||||
import okhttp3.internal.closeQuietly
|
import okhttp3.internal.closeQuietly
|
||||||
|
import okhttp3.internal.isSensitiveHeader
|
||||||
import okio.IOException
|
import okio.IOException
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.jsoup.HttpStatusException
|
import org.jsoup.HttpStatusException
|
||||||
@@ -41,8 +42,6 @@ fun Response.ensureSuccess() = apply {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Response.requireBody(): ResponseBody = checkNotNull(body) { "Response body is null" }
|
|
||||||
|
|
||||||
fun Cookie.newBuilder(): Cookie.Builder = Cookie.Builder().also { c ->
|
fun Cookie.newBuilder(): Cookie.Builder = Cookie.Builder().also { c ->
|
||||||
c.name(name)
|
c.name(name)
|
||||||
c.value(value)
|
c.value(value)
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ fun LocaleListCompat.getOrThrow(index: Int) = get(index) ?: throw NoSuchElementE
|
|||||||
|
|
||||||
fun String.toLocale() = Locale(this)
|
fun String.toLocale() = Locale(this)
|
||||||
|
|
||||||
fun Locale?.getDisplayName(context: Context): String = when (this) {
|
fun Locale?.getDisplayName(context: Context): String {
|
||||||
null -> context.getString(R.string.all_languages)
|
if (this == null) {
|
||||||
Locale.ROOT -> context.getString(R.string.various_languages)
|
return context.getString(R.string.various_languages)
|
||||||
else -> getDisplayLanguage(this).toTitleCase(this)
|
}
|
||||||
|
return getDisplayLanguage(this).toTitleCase(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private class LocaleListCompatIterator(private val list: LocaleListCompat) : ListIterator<Locale> {
|
private class LocaleListCompatIterator(private val list: LocaleListCompat) : ListIterator<Locale> {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import org.koitharu.kotatsu.core.exceptions.CaughtException
|
|||||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException
|
import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException
|
||||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||||
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
|
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
|
||||||
import org.koitharu.kotatsu.core.exceptions.IncompatiblePluginException
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException
|
import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException
|
||||||
import org.koitharu.kotatsu.core.exceptions.SyncApiException
|
import org.koitharu.kotatsu.core.exceptions.SyncApiException
|
||||||
import org.koitharu.kotatsu.core.exceptions.TooManyRequestExceptions
|
import org.koitharu.kotatsu.core.exceptions.TooManyRequestExceptions
|
||||||
@@ -61,7 +60,7 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
|
|||||||
-> resources.getString(R.string.network_error)
|
-> resources.getString(R.string.network_error)
|
||||||
|
|
||||||
is NoDataReceivedException -> resources.getString(R.string.error_no_data_received)
|
is NoDataReceivedException -> resources.getString(R.string.error_no_data_received)
|
||||||
is IncompatiblePluginException -> resources.getString(R.string.plugin_incompatible)
|
|
||||||
is WrongPasswordException -> resources.getString(R.string.wrong_password)
|
is WrongPasswordException -> resources.getString(R.string.wrong_password)
|
||||||
is NotFoundException -> resources.getString(R.string.not_found_404)
|
is NotFoundException -> resources.getString(R.string.not_found_404)
|
||||||
is UnsupportedSourceException -> resources.getString(R.string.unsupported_source)
|
is UnsupportedSourceException -> resources.getString(R.string.unsupported_source)
|
||||||
|
|||||||
@@ -55,11 +55,11 @@ class DetailsLoadUseCase @Inject constructor(
|
|||||||
try {
|
try {
|
||||||
val details = getDetails(manga)
|
val details = getDetails(manga)
|
||||||
launch { updateTracker(details) }
|
launch { updateTracker(details) }
|
||||||
send(MangaDetails(details, local?.peek(), details.description?.parseAsHtml(withImages = false)?.trim(), false))
|
send(MangaDetails(details, local?.peek(), details.description?.parseAsHtml(withImages = false), false))
|
||||||
send(MangaDetails(details, local?.await(), details.description?.parseAsHtml(withImages = true)?.trim(), true))
|
send(MangaDetails(details, local?.await(), details.description?.parseAsHtml(withImages = true), true))
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
local?.await()?.manga?.also { localManga ->
|
local?.await()?.manga?.also { localManga ->
|
||||||
send(MangaDetails(localManga, null, localManga.description?.parseAsHtml(withImages = false)?.trim(), true))
|
send(MangaDetails(localManga, null, localManga.description?.parseAsHtml(withImages = false), true))
|
||||||
} ?: close(e)
|
} ?: close(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ 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.MemoryContentCache
|
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||||
import org.koitharu.kotatsu.core.model.LocalMangaSource
|
|
||||||
import org.koitharu.kotatsu.core.model.findById
|
import org.koitharu.kotatsu.core.model.findById
|
||||||
import org.koitharu.kotatsu.core.model.isLocal
|
|
||||||
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
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
@@ -64,7 +62,7 @@ class MangaPrefetchService : CoroutineIntentService() {
|
|||||||
|
|
||||||
private suspend fun prefetchLast() {
|
private suspend fun prefetchLast() {
|
||||||
val last = historyRepository.getLastOrNull() ?: return
|
val last = historyRepository.getLastOrNull() ?: return
|
||||||
if (last.isLocal) return
|
if (last.source == MangaSource.LOCAL) return
|
||||||
val repo = mangaRepositoryFactory.create(last.source)
|
val repo = mangaRepositoryFactory.create(last.source)
|
||||||
val details = runCatchingCancellable { repo.getDetails(last) }.getOrNull() ?: return
|
val details = runCatchingCancellable { repo.getDetails(last) }.getOrNull() ?: return
|
||||||
val chapters = details.chapters
|
val chapters = details.chapters
|
||||||
@@ -112,7 +110,7 @@ class MangaPrefetchService : CoroutineIntentService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun isPrefetchAvailable(context: Context, source: MangaSource?): Boolean {
|
private fun isPrefetchAvailable(context: Context, source: MangaSource?): Boolean {
|
||||||
if (source == LocalMangaSource || context.isPowerSaveMode()) {
|
if (source == MangaSource.LOCAL || context.isPowerSaveMode()) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
val entryPoint = EntryPointAccessors.fromApplication(
|
val entryPoint = EntryPointAccessors.fromApplication(
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user