diff --git a/.gitignore b/.gitignore
index dcbfd2e89..6155175a9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,4 @@
.externalNativeBuild
.cxx
/.idea/deviceManager.xml
+/.kotlin/
diff --git a/.idea/.gitignore b/.idea/.gitignore
index 9f674b306..5506c50c7 100644
--- a/.idea/.gitignore
+++ b/.idea/.gitignore
@@ -2,3 +2,4 @@
/shelf/
/workspace.xml
/migrations.xml
+/runConfigurations.xml
diff --git a/app/build.gradle b/app/build.gradle
index bb59d1276..b54b11785 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -15,9 +15,9 @@ android {
defaultConfig {
applicationId 'org.koitharu.kotatsu'
minSdk = 21
- targetSdk = 34
- versionCode = 651
- versionName = '7.3'
+ targetSdk = 35
+ versionCode = 657
+ versionName = '7.4'
generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp {
@@ -82,23 +82,23 @@ afterEvaluate {
}
dependencies {
//noinspection GradleDependency
- implementation('com.github.KotatsuApp:kotatsu-parsers:74b8aaa94e') {
+ implementation('com.github.KotatsuApp:kotatsu-parsers:a9fc534ea7') {
exclude group: 'org.json', module: 'json'
}
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
- implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.24'
- implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1'
+ implementation 'org.jetbrains.kotlin:kotlin-stdlib:2.0.10-RC'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0-RC'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.core:core-ktx:1.13.1'
- implementation 'androidx.activity:activity-ktx:1.9.0'
- implementation 'androidx.fragment:fragment-ktx:1.8.1'
- implementation 'androidx.transition:transition-ktx:1.5.0'
- implementation 'androidx.collection:collection-ktx:1.4.0'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3'
- implementation 'androidx.lifecycle:lifecycle-service:2.8.3'
- implementation 'androidx.lifecycle:lifecycle-process:2.8.3'
+ implementation 'androidx.activity:activity-ktx:1.9.1'
+ implementation 'androidx.fragment:fragment-ktx:1.8.2'
+ implementation 'androidx.transition:transition-ktx:1.5.1'
+ implementation 'androidx.collection:collection-ktx:1.4.2'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4'
+ implementation 'androidx.lifecycle:lifecycle-service:2.8.4'
+ implementation 'androidx.lifecycle:lifecycle-process:2.8.4'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
@@ -106,7 +106,7 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
implementation 'com.google.android.material:material:1.12.0'
- implementation 'androidx.lifecycle:lifecycle-common-java8:2.8.3'
+ implementation 'androidx.lifecycle:lifecycle-common-java8:2.8.4'
implementation 'androidx.webkit:webkit:1.11.0'
implementation 'androidx.work:work-runtime:2.9.0'
@@ -134,8 +134,8 @@ dependencies {
implementation 'androidx.hilt:hilt-work:1.2.0'
kapt 'androidx.hilt:hilt-compiler:1.2.0'
- implementation 'io.coil-kt:coil-base:2.6.0'
- implementation 'io.coil-kt:coil-svg:2.6.0'
+ implementation 'io.coil-kt:coil-base:2.7.0'
+ implementation 'io.coil-kt:coil-svg:2.7.0'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:882bc0620c'
implementation 'com.github.solkin:disk-lru-cache:1.4'
implementation 'io.noties.markwon:core:4.6.2'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3d68b4970..cfdc81f88 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -20,6 +20,10 @@
+
+
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/domain/AlternativesUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/domain/AlternativesUseCase.kt
index 6985bad9a..a993eb31c 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/domain/AlternativesUseCase.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/domain/AlternativesUseCase.kt
@@ -12,6 +12,7 @@ import org.koitharu.kotatsu.core.util.ext.almostEquals
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.parsers.model.Manga
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.util.runCatchingCancellable
import javax.inject.Inject
@@ -57,7 +58,7 @@ class AlternativesUseCase @Inject constructor(
}
private suspend fun getSources(ref: MangaSource): List {
- val result = ArrayList(MangaSource.entries.size - 2)
+ val result = ArrayList(MangaParserSource.entries.size - 2)
result.addAll(sourcesRepository.getEnabledSources())
result.sortByDescending { it.priority(ref) }
result.addAll(sourcesRepository.getDisabledSources().sortedByDescending { it.priority(ref) })
@@ -78,8 +79,10 @@ class AlternativesUseCase @Inject constructor(
private fun MangaSource.priority(ref: MangaSource): Int {
var res = 0
- if (locale == ref.locale) res += 2
- if (contentType == ref.contentType) res++
+ if (this is MangaParserSource && ref is MangaParserSource) {
+ if (locale == ref.locale) res += 2
+ if (contentType == ref.contentType) res++
+ }
return res
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/domain/MigrateUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/domain/MigrateUseCase.kt
index 12d92bcf0..503e9495e 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/domain/MigrateUseCase.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/domain/MigrateUseCase.kt
@@ -18,178 +18,178 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity
import javax.inject.Inject
class MigrateUseCase
- @Inject
- constructor(
- private val mangaRepositoryFactory: MangaRepository.Factory,
- private val mangaDataRepository: MangaDataRepository,
- private val database: MangaDatabase,
- private val progressUpdateUseCase: ProgressUpdateUseCase,
- private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
+@Inject
+constructor(
+ private val mangaRepositoryFactory: MangaRepository.Factory,
+ private val mangaDataRepository: MangaDataRepository,
+ private val database: MangaDatabase,
+ private val progressUpdateUseCase: ProgressUpdateUseCase,
+ private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
+) {
+ suspend operator fun invoke(
+ oldManga: Manga,
+ newManga: Manga,
) {
- suspend operator fun invoke(
- oldManga: Manga,
- newManga: Manga,
- ) {
- val oldDetails =
- if (oldManga.chapters.isNullOrEmpty()) {
- runCatchingCancellable {
- mangaRepositoryFactory.create(oldManga.source).getDetails(oldManga)
- }.getOrDefault(oldManga)
- } else {
- oldManga
+ val oldDetails =
+ if (oldManga.chapters.isNullOrEmpty()) {
+ runCatchingCancellable {
+ mangaRepositoryFactory.create(oldManga.source).getDetails(oldManga)
+ }.getOrDefault(oldManga)
+ } else {
+ oldManga
+ }
+ val newDetails =
+ if (newManga.chapters.isNullOrEmpty()) {
+ mangaRepositoryFactory.create(newManga.source).getDetails(newManga)
+ } else {
+ newManga
+ }
+ mangaDataRepository.storeManga(newDetails)
+ database.withTransaction {
+ // replace favorites
+ val favoritesDao = database.getFavouritesDao()
+ val oldFavourites = favoritesDao.findAllRaw(oldDetails.id)
+ if (oldFavourites.isNotEmpty()) {
+ favoritesDao.delete(oldManga.id)
+ for (f in oldFavourites) {
+ val e =
+ f.copy(
+ mangaId = newManga.id,
+ )
+ favoritesDao.upsert(e)
}
- val newDetails =
- if (newManga.chapters.isNullOrEmpty()) {
- mangaRepositoryFactory.create(newManga.source).getDetails(newManga)
+ }
+ // replace history
+ val historyDao = database.getHistoryDao()
+ val oldHistory = historyDao.find(oldDetails.id)
+ val newHistory =
+ if (oldHistory != null) {
+ val newHistory = makeNewHistory(oldDetails, newDetails, oldHistory)
+ historyDao.delete(oldDetails.id)
+ historyDao.upsert(newHistory)
+ newHistory
} else {
- newManga
+ null
}
- mangaDataRepository.storeManga(newDetails)
- database.withTransaction {
- // replace favorites
- val favoritesDao = database.getFavouritesDao()
- val oldFavourites = favoritesDao.findAllRaw(oldDetails.id)
- if (oldFavourites.isNotEmpty()) {
- favoritesDao.delete(oldManga.id)
- for (f in oldFavourites) {
- val e =
- f.copy(
- mangaId = newManga.id,
- )
- favoritesDao.upsert(e)
- }
- }
- // replace history
- val historyDao = database.getHistoryDao()
- val oldHistory = historyDao.find(oldDetails.id)
- val newHistory =
- if (oldHistory != null) {
- val newHistory = makeNewHistory(oldDetails, newDetails, oldHistory)
- historyDao.delete(oldDetails.id)
- historyDao.upsert(newHistory)
- newHistory
- } else {
- null
- }
- // track
- val tracksDao = database.getTracksDao()
- val oldTrack = tracksDao.find(oldDetails.id)
- if (oldTrack != null) {
- val lastChapter = newDetails.chapters?.lastOrNull()
- val newTrack =
- TrackEntity(
- mangaId = newDetails.id,
- lastChapterId = lastChapter?.id ?: 0L,
- newChapters = 0,
- lastCheckTime = System.currentTimeMillis(),
- lastChapterDate = lastChapter?.uploadDate ?: 0L,
- lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION,
- lastError = null,
- )
- tracksDao.delete(oldDetails.id)
- 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(
+ // track
+ val tracksDao = database.getTracksDao()
+ val oldTrack = tracksDao.find(oldDetails.id)
+ if (oldTrack != null) {
+ val lastChapter = newDetails.chapters?.lastOrNull()
+ val newTrack =
+ TrackEntity(
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,
+ lastChapterId = lastChapter?.id ?: 0L,
+ newChapters = 0,
+ lastCheckTime = System.currentTimeMillis(),
+ lastChapterDate = lastChapter?.uploadDate ?: 0L,
+ lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION,
+ lastError = null,
+ )
+ tracksDao.delete(oldDetails.id)
+ 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,
)
- if (newHistory != null) {
- scrobbler.scrobble(
- manga = newDetails,
- chapterId = newHistory.chapterId,
- )
- }
}
}
- progressUpdateUseCase(newManga)
}
+ progressUpdateUseCase(newManga)
+ }
- private fun makeNewHistory(
- oldManga: Manga,
- newManga: Manga,
- history: HistoryEntity,
- ): HistoryEntity {
- if (oldManga.chapters.isNullOrEmpty()) { // probably broken manga/source
- val branch = newManga.getPreferredBranch(null)
- val chapters = checkNotNull(newManga.getChapters(branch))
- val currentChapter =
- if (history.percent in 0f..1f) {
- chapters[(chapters.lastIndex * history.percent).toInt()]
- } else {
- chapters.first()
- }
- return HistoryEntity(
- mangaId = newManga.id,
- createdAt = history.createdAt,
- updatedAt = System.currentTimeMillis(),
- chapterId = currentChapter.id,
- page = history.page,
- scroll = history.scroll,
- percent = history.percent,
- deletedAt = 0,
- chaptersCount = chapters.size,
- )
- }
- val branch = oldManga.getPreferredBranch(history.toMangaHistory())
- val oldChapters = checkNotNull(oldManga.getChapters(branch))
- var index = oldChapters.indexOfFirst { it.id == history.chapterId }
- if (index < 0) {
- index =
- if (history.percent in 0f..1f) {
- (oldChapters.lastIndex * history.percent).toInt()
- } else {
- 0
- }
- }
- val newChapters = checkNotNull(newManga.chapters).groupBy { it.branch }
- val newBranch =
- if (newChapters.containsKey(branch)) {
- branch
+ private fun makeNewHistory(
+ oldManga: Manga,
+ newManga: Manga,
+ history: HistoryEntity,
+ ): HistoryEntity {
+ if (oldManga.chapters.isNullOrEmpty()) { // probably broken manga/source
+ val branch = newManga.getPreferredBranch(null)
+ val chapters = checkNotNull(newManga.getChapters(branch))
+ val currentChapter =
+ if (history.percent in 0f..1f) {
+ chapters[(chapters.lastIndex * history.percent).toInt()]
} else {
- newManga.getPreferredBranch(null)
+ chapters.first()
}
- val newChapterId =
- checkNotNull(newChapters[newBranch])
- .let {
- val oldChapter = oldChapters[index]
- it.findByNumber(oldChapter.volume, oldChapter.number) ?: it.getOrNull(index) ?: it.last()
- }.id
-
return HistoryEntity(
mangaId = newManga.id,
createdAt = history.createdAt,
updatedAt = System.currentTimeMillis(),
- chapterId = newChapterId,
+ chapterId = currentChapter.id,
page = history.page,
scroll = history.scroll,
- percent = PROGRESS_NONE,
+ percent = history.percent,
deletedAt = 0,
- chaptersCount = checkNotNull(newChapters[newBranch]).size,
+ chaptersCount = chapters.count { it.branch == currentChapter.branch },
)
}
-
- private fun List.findByNumber(
- volume: Int,
- number: Float,
- ): MangaChapter? =
- if (number <= 0f) {
- null
+ val branch = oldManga.getPreferredBranch(history.toMangaHistory())
+ val oldChapters = checkNotNull(oldManga.getChapters(branch))
+ var index = oldChapters.indexOfFirst { it.id == history.chapterId }
+ if (index < 0) {
+ index =
+ if (history.percent in 0f..1f) {
+ (oldChapters.lastIndex * history.percent).toInt()
+ } else {
+ 0
+ }
+ }
+ val newChapters = checkNotNull(newManga.chapters).groupBy { it.branch }
+ val newBranch =
+ if (newChapters.containsKey(branch)) {
+ branch
} else {
- firstOrNull { it.volume == volume && it.number == number }
+ newManga.getPreferredBranch(null)
}
+ val newChapterId =
+ checkNotNull(newChapters[newBranch])
+ .let {
+ val oldChapter = oldChapters[index]
+ it.findByNumber(oldChapter.volume, oldChapter.number) ?: it.getOrNull(index) ?: it.last()
+ }.id
+
+ return HistoryEntity(
+ mangaId = newManga.id,
+ createdAt = history.createdAt,
+ updatedAt = System.currentTimeMillis(),
+ chapterId = newChapterId,
+ page = history.page,
+ scroll = history.scroll,
+ percent = PROGRESS_NONE,
+ deletedAt = 0,
+ chaptersCount = checkNotNull(newChapters[newBranch]).size,
+ )
}
+
+ private fun List.findByNumber(
+ volume: Int,
+ number: Float,
+ ): MangaChapter? =
+ if (number <= 0f) {
+ null
+ } else {
+ firstOrNull { it.volume == volume && it.number == number }
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AlternativeAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AlternativeAD.kt
index fa467541e..d903b4aa1 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AlternativeAD.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AlternativeAD.kt
@@ -10,6 +10,7 @@ import coil.request.ImageRequest
import coil.transform.CircleCropTransformation
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
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.ui.image.ChipIconTarget
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
@@ -61,9 +62,9 @@ fun alternativeAD(
}
}
}
- binding.progressView.setPercent(item.progress, ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED in payloads)
+ binding.progressView.setProgress(item.progress, ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED in payloads)
binding.chipSource.also { chip ->
- chip.text = item.manga.source.title
+ chip.text = item.manga.source.getTitle(chip.context)
ImageRequest.Builder(context)
.data(item.manga.source.faviconUri())
.lifecycle(lifecycleOwner)
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AlternativesActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AlternativesActivity.kt
index 03d099a54..aefe5ce3b 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AlternativesActivity.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AlternativesActivity.kt
@@ -13,6 +13,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
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.parser.MangaIntent
import org.koitharu.kotatsu.core.ui.BaseActivity
@@ -95,9 +96,9 @@ class AlternativesActivity : BaseActivity(),
getString(
R.string.migrate_confirmation,
viewModel.manga.title,
- viewModel.manga.source.title,
+ viewModel.manga.source.getTitle(this),
target.title,
- target.source.title,
+ target.source.getTitle(this),
),
)
.setNegativeButton(android.R.string.cancel, null)
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AlternativesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AlternativesViewModel.kt
index 64eab5e0b..918227088 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AlternativesViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AlternativesViewModel.kt
@@ -15,11 +15,13 @@ import org.koitharu.kotatsu.core.model.chaptersCount
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.parser.MangaIntent
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.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.require
-import org.koitharu.kotatsu.list.domain.ListExtraProvider
+import org.koitharu.kotatsu.history.data.HistoryRepository
+import org.koitharu.kotatsu.list.domain.ReadingProgress
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
@@ -34,7 +36,8 @@ class AlternativesViewModel @Inject constructor(
private val mangaRepositoryFactory: MangaRepository.Factory,
private val alternativesUseCase: AlternativesUseCase,
private val migrateUseCase: MigrateUseCase,
- private val extraProvider: ListExtraProvider,
+ private val historyRepository: HistoryRepository,
+ private val settings: AppSettings,
) : BaseViewModel() {
val manga = savedStateHandle.require(MangaIntent.KEY_MANGA).manga
@@ -53,7 +56,7 @@ class AlternativesViewModel @Inject constructor(
.map {
MangaAlternativeModel(
manga = it,
- progress = extraProvider.getProgress(it.id),
+ progress = getProgress(it.id),
referenceChapters = refCount,
)
}.runningFold>(listOf(LoadingState)) { acc, item ->
@@ -86,13 +89,7 @@ class AlternativesViewModel @Inject constructor(
}
}
- private suspend fun mapList(list: List, refCount: Int): List {
- return list.map {
- MangaAlternativeModel(
- manga = it,
- progress = extraProvider.getProgress(it.id),
- referenceChapters = refCount,
- )
- }
+ private suspend fun getProgress(mangaId: Long): ReadingProgress? {
+ return historyRepository.getProgress(mangaId, settings.progressIndicatorMode)
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/MangaAlternativeModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/MangaAlternativeModel.kt
index 18571c17a..da9d9b5a6 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/MangaAlternativeModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/MangaAlternativeModel.kt
@@ -1,12 +1,13 @@
package org.koitharu.kotatsu.alternatives.ui
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.parsers.model.Manga
data class MangaAlternativeModel(
val manga: Manga,
- val progress: Float,
+ val progress: ReadingProgress?,
private val referenceChapters: Int,
) : ListModel {
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarkLargeAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarkLargeAD.kt
index e0aab168f..bdbd35ef0 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarkLargeAD.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarkLargeAD.kt
@@ -37,6 +37,6 @@ fun bookmarkLargeAD(
source(item.manga.source)
enqueueWith(coil)
}
- binding.progressView.percent = item.percent
+ binding.progressView.setProgress(item.percent, false)
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt
index 04f71e3bd..a3a5bb1f0 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt
@@ -12,13 +12,14 @@ import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import dagger.hilt.android.AndroidEntryPoint
+import okhttp3.internal.userAgent
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository
-import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
+import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.ui.BaseActivity
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.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -42,10 +43,9 @@ class BrowserActivity : BaseActivity(), BrowserCallback
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
}
- val userAgent = intent?.getSerializableExtraCompat(EXTRA_SOURCE)?.let { source ->
- val repository = mangaRepositoryFactory.create(source) as? RemoteMangaRepository
- repository?.headers?.get(CommonHeaders.USER_AGENT)
- }
+ val mangaSource = MangaSource(intent?.getStringExtra(EXTRA_SOURCE))
+ val repository = mangaRepositoryFactory.create(mangaSource) as? ParserMangaRepository
+ repository?.headers?.get(CommonHeaders.USER_AGENT)
viewBinding.webView.configureForParser(userAgent)
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
viewBinding.webView.webViewClient = BrowserClient(this)
@@ -147,7 +147,7 @@ class BrowserActivity : BaseActivity(), BrowserCallback
return Intent(context, BrowserActivity::class.java)
.setData(Uri.parse(url))
.putExtra(EXTRA_TITLE, title)
- .putExtra(EXTRA_SOURCE, source)
+ .putExtra(EXTRA_SOURCE, source?.name)
}
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CaptchaNotifier.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CaptchaNotifier.kt
index ea6d2db10..83318f8ea 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CaptchaNotifier.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CaptchaNotifier.kt
@@ -14,8 +14,9 @@ import coil.request.ErrorResult
import coil.request.ImageRequest
import org.koitharu.kotatsu.R
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.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
class CaptchaNotifier(
@@ -46,7 +47,7 @@ class CaptchaNotifier(
.setGroup(GROUP_CAPTCHA)
.setAutoCancel(true)
.setVisibility(
- if (exception.source?.contentType == ContentType.HENTAI) {
+ if (exception.source?.isNsfw() == true) {
NotificationCompat.VISIBILITY_SECRET
} else {
NotificationCompat.VISIBILITY_PUBLIC
@@ -55,7 +56,7 @@ class CaptchaNotifier(
.setContentText(
context.getString(
R.string.captcha_required_summary,
- exception.source?.title ?: context.getString(R.string.app_name),
+ exception.source?.getTitle(context) ?: context.getString(R.string.app_name),
),
)
.setContentIntent(PendingIntentCompat.getActivity(context, 0, intent, 0, false))
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareActivity.kt
index 68daa3c2d..28c2f47d6 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareActivity.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareActivity.kt
@@ -55,7 +55,11 @@ class CloudFlareActivity : BaseActivity(), CloudFlareCal
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
}
- val url = intent?.dataString.orEmpty()
+ val url = intent?.dataString
+ if (url.isNullOrEmpty()) {
+ finishAfterTransition()
+ return
+ }
cfClient = CloudFlareClient(cookieJar, this, url)
viewBinding.webView.configureForParser(intent?.getStringExtra(ARG_UA))
viewBinding.webView.webViewClient = cfClient
@@ -63,12 +67,7 @@ class CloudFlareActivity : BaseActivity(), CloudFlareCal
onBackPressedDispatcher.addCallback(it)
}
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
- if (savedInstanceState != null) {
- return
- }
- if (url.isEmpty()) {
- finishAfterTransition()
- } else {
+ if (savedInstanceState == null) {
onTitleChanged(getString(R.string.loading_), url)
viewBinding.webView.loadUrl(url)
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt
index 8c93fc4b6..f1abcfdb2 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt
@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.core
import android.app.Application
+import android.content.ContentResolver
import android.content.Context
import android.provider.SearchRecentSuggestions
import android.text.Html
@@ -110,6 +111,8 @@ interface AppModule {
.decoderDispatcher(Dispatchers.IO)
.transformationDispatcher(Dispatchers.Default)
.diskCache(diskCacheFactory)
+ .respectCacheHeaders(false)
+ .networkObserverEnabled(false)
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
.allowRgb565(context.isLowRamDevice())
.eventListener(CaptchaNotifier(context))
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration16To17.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration16To17.kt
index e49a9b1ee..7e58a5bc5 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration16To17.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration16To17.kt
@@ -4,7 +4,7 @@ import android.content.Context
import androidx.preference.PreferenceManager
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
-import org.koitharu.kotatsu.parsers.model.MangaSource
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
class Migration16To17(context: Context) : Migration(16, 17) {
@@ -15,11 +15,8 @@ class Migration16To17(context: Context) : Migration(16, 17) {
db.execSQL("CREATE INDEX `index_sources_sort_key` ON `sources` (`sort_key`)")
val hiddenSources = prefs.getStringSet("sources_hidden", null).orEmpty()
val order = prefs.getString("sources_order_2", null)?.split('|').orEmpty()
- val sources = MangaSource.entries
+ val sources = MangaParserSource.entries
for (source in sources) {
- if (source == MangaSource.LOCAL) {
- continue
- }
val name = source.name
val isHidden = name in hiddenSources
var sortKey = order.indexOf(name)
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt
index 8ca03088a..8a3674fa8 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt
@@ -11,7 +11,6 @@ import org.koitharu.kotatsu.details.ui.model.ChapterListItem
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.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.util.formatSimple
import org.koitharu.kotatsu.parsers.util.mapToSet
@@ -109,7 +108,7 @@ fun Manga.getPreferredBranch(history: MangaHistory?): String? {
}
val Manga.isLocal: Boolean
- get() = source == MangaSource.LOCAL
+ get() = source == LocalMangaSource
val Manga.appUrl: Uri
get() = Uri.parse("https://kotatsu.app/manga").buildUpon()
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/MangaSource.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/MangaSource.kt
index 1de70d7c2..5e847ad11 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/MangaSource.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/MangaSource.kt
@@ -7,24 +7,47 @@ import android.text.style.ForegroundColorSpan
import android.text.style.RelativeSizeSpan
import android.text.style.SuperscriptSpan
import androidx.annotation.StringRes
-import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
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.getThemeColor
import org.koitharu.kotatsu.core.util.ext.toLocale
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.util.splitTwoParts
import com.google.android.material.R as materialR
-fun MangaSource(name: String): MangaSource {
- MangaSource.entries.forEach {
+data object LocalMangaSource : MangaSource {
+ override val name = "LOCAL"
+}
+
+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
}
- return MangaSource.UNKNOWN
+ return UnknownMangaSource
}
-fun MangaSource.isNsfw() = contentType == ContentType.HENTAI
+fun MangaSource.isNsfw(): Boolean = when (this) {
+ is MangaSourceInfo -> mangaSource.isNsfw()
+ is MangaParserSource -> contentType == ContentType.HENTAI
+ else -> false
+}
@get:StringRes
val ContentType.titleResId
@@ -35,23 +58,28 @@ val ContentType.titleResId
ContentType.OTHER -> R.string.content_type_other
}
-fun MangaSource.getSummary(context: Context): String {
- val type = context.getString(contentType.titleResId)
- val locale = locale.toLocale().getDisplayName(context)
- return context.getString(R.string.source_summary_pattern, type, locale)
+fun MangaSource.getSummary(context: Context): String? = when (this) {
+ is MangaSourceInfo -> mangaSource.getSummary(context)
+ is MangaParserSource -> {
+ val type = context.getString(contentType.titleResId)
+ val locale = locale.toLocale().getDisplayName(context)
+ context.getString(R.string.source_summary_pattern, type, locale)
+ }
+
+ is ExternalMangaSource -> context.getString(R.string.external_source)
+
+ else -> null
}
-fun MangaSource.getTitle(context: Context): CharSequence = if (isNsfw()) {
- buildSpannedString {
- append(title)
- append(' ')
- appendNsfwLabel(context)
- }
-} else {
- title
+fun MangaSource.getTitle(context: Context): String = when (this) {
+ 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)
}
-private fun SpannableStringBuilder.appendNsfwLabel(context: Context) = inSpans(
+fun SpannableStringBuilder.appendNsfwLabel(context: Context) = inSpans(
ForegroundColorSpan(context.getThemeColor(materialR.attr.colorError, Color.RED)),
RelativeSizeSpan(0.74f),
SuperscriptSpan(),
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/MangaSourceInfo.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/MangaSourceInfo.kt
new file mode 100644
index 000000000..a14f5b185
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/MangaSourceInfo.kt
@@ -0,0 +1,9 @@
+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
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/MangaSourceParceler.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/MangaSourceParceler.kt
new file mode 100644
index 000000000..f18fca67d
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/MangaSourceParceler.kt
@@ -0,0 +1,15 @@
+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 {
+
+ override fun create(parcel: Parcel): MangaSource = MangaSource(parcel.readString())
+
+ override fun MangaSource.write(parcel: Parcel, flags: Int) {
+ parcel.writeString(name)
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableChapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableChapter.kt
index 8d47965db..148060125 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableChapter.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableChapter.kt
@@ -4,9 +4,8 @@ import android.os.Parcel
import android.os.Parcelable
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
-import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
+import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaChapter
-import org.koitharu.kotatsu.parsers.model.MangaSource
@Parcelize
data class ParcelableChapter(
@@ -25,8 +24,8 @@ data class ParcelableChapter(
scanlator = parcel.readString(),
uploadDate = parcel.readLong(),
branch = parcel.readString(),
- source = parcel.readSerializableCompat() ?: MangaSource.UNKNOWN,
- )
+ source = MangaSource(parcel.readString()),
+ ),
)
override fun ParcelableChapter.write(parcel: Parcel, flags: Int) = with(chapter) {
@@ -38,7 +37,7 @@ data class ParcelableChapter(
parcel.writeString(scanlator)
parcel.writeLong(uploadDate)
parcel.writeString(branch)
- parcel.writeSerializable(source)
+ parcel.writeString(source.name)
}
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableManga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableManga.kt
index c4d19a593..fb91de501 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableManga.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableManga.kt
@@ -5,6 +5,7 @@ import android.os.Parcelable
import androidx.core.os.ParcelCompat
import kotlinx.parcelize.Parceler
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.readSerializableCompat
import org.koitharu.kotatsu.parsers.model.Manga
@@ -30,7 +31,7 @@ data class ParcelableManga(
parcel.writeParcelable(ParcelableMangaTags(tags), flags)
parcel.writeSerializable(state)
parcel.writeString(author)
- parcel.writeSerializable(source)
+ parcel.writeString(source.name)
}
override fun create(parcel: Parcel) = ParcelableManga(
@@ -49,8 +50,8 @@ data class ParcelableManga(
state = parcel.readSerializableCompat(),
author = parcel.readString(),
chapters = null,
- source = requireNotNull(parcel.readSerializableCompat()),
- )
+ source = MangaSource(parcel.readString()),
+ ),
)
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaPage.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaPage.kt
index eee936e84..94b3ce5fc 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaPage.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaPage.kt
@@ -5,7 +5,7 @@ import android.os.Parcelable
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.TypeParceler
-import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
+import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaPage
object MangaPageParceler : Parceler {
@@ -13,14 +13,14 @@ object MangaPageParceler : Parceler {
id = parcel.readLong(),
url = requireNotNull(parcel.readString()),
preview = parcel.readString(),
- source = requireNotNull(parcel.readSerializableCompat()),
+ source = MangaSource(parcel.readString()),
)
override fun MangaPage.write(parcel: Parcel, flags: Int) {
parcel.writeLong(id)
parcel.writeString(url)
parcel.writeString(preview)
- parcel.writeSerializable(source)
+ parcel.writeString(source.name)
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaTags.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaTags.kt
index 75640156a..b8fa8208f 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaTags.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaTags.kt
@@ -5,20 +5,20 @@ import android.os.Parcelable
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.TypeParceler
-import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
+import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
object MangaTagParceler : Parceler {
override fun create(parcel: Parcel) = MangaTag(
title = requireNotNull(parcel.readString()),
key = requireNotNull(parcel.readString()),
- source = requireNotNull(parcel.readSerializableCompat()),
+ source = MangaSource(parcel.readString()),
)
override fun MangaTag.write(parcel: Parcel, flags: Int) {
parcel.writeString(title)
parcel.writeString(key)
- parcel.writeSerializable(source)
+ parcel.writeString(source.name)
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/CommonHeadersInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/CommonHeadersInterceptor.kt
index 26e4dc212..a1aa3bf86 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/CommonHeadersInterceptor.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/CommonHeadersInterceptor.kt
@@ -11,7 +11,7 @@ import okio.IOException
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
import org.koitharu.kotatsu.core.parser.MangaRepository
-import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
+import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mergeWith
@@ -30,7 +30,7 @@ class CommonHeadersInterceptor @Inject constructor(
val request = chain.request()
val source = request.tag(MangaSource::class.java)
val repository = if (source != null) {
- mangaRepositoryFactoryLazy.get().create(source) as? RemoteMangaRepository
+ mangaRepositoryFactoryLazy.get().create(source) as? ParserMangaRepository
} else {
if (BuildConfig.DEBUG) {
Log.w("Http", "Request without source tag: ${request.url}")
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt
index efdc9564c..04c67cd05 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt
@@ -13,8 +13,9 @@ import okhttp3.internal.canParseAsIpAddress
import okhttp3.internal.closeQuietly
import okhttp3.internal.publicsuffix.PublicSuffixDatabase
import org.koitharu.kotatsu.core.parser.MangaRepository
-import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
+import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import java.util.EnumMap
import javax.inject.Inject
@@ -26,8 +27,8 @@ class MirrorSwitchInterceptor @Inject constructor(
private val settings: AppSettings,
) : Interceptor {
- private val locks = EnumMap(MangaSource::class.java)
- private val blacklist = EnumMap>(MangaSource::class.java)
+ private val locks = EnumMap(MangaParserSource::class.java)
+ private val blacklist = EnumMap>(MangaParserSource::class.java)
val isEnabled: Boolean
get() = settings.isMirrorSwitchingAvailable
@@ -53,7 +54,7 @@ class MirrorSwitchInterceptor @Inject constructor(
}
}
- suspend fun trySwitchMirror(repository: RemoteMangaRepository): Boolean = runInterruptible(Dispatchers.Default) {
+ suspend fun trySwitchMirror(repository: ParserMangaRepository): Boolean = runInterruptible(Dispatchers.Default) {
if (!isEnabled) {
return@runInterruptible false
}
@@ -75,14 +76,14 @@ class MirrorSwitchInterceptor @Inject constructor(
}
}
- fun rollback(repository: RemoteMangaRepository, oldMirror: String) = synchronized(obtainLock(repository.source)) {
+ fun rollback(repository: ParserMangaRepository, oldMirror: String) = synchronized(obtainLock(repository.source)) {
blacklist[repository.source]?.remove(oldMirror)
repository.domain = oldMirror
}
private fun trySwitchMirror(request: Request, chain: Interceptor.Chain): Response? {
val source = request.tag(MangaSource::class.java) ?: return null
- val repository = mangaRepositoryFactoryLazy.get().create(source) as? RemoteMangaRepository ?: return null
+ val repository = mangaRepositoryFactoryLazy.get().create(source) as? ParserMangaRepository ?: return null
val mirrors = repository.getAvailableMirrors()
if (mirrors.isEmpty()) {
return null
@@ -93,7 +94,7 @@ class MirrorSwitchInterceptor @Inject constructor(
}
private fun tryMirrors(
- repository: RemoteMangaRepository,
+ repository: ParserMangaRepository,
mirrors: List,
chain: Interceptor.Chain,
request: Request,
@@ -145,15 +146,15 @@ class MirrorSwitchInterceptor @Inject constructor(
return source().readByteArray().toResponseBody(contentType())
}
- private fun obtainLock(source: MangaSource): Any = locks.getOrPut(source) {
+ private fun obtainLock(source: MangaParserSource): Any = locks.getOrPut(source) {
Any()
}
- private fun isBlacklisted(source: MangaSource, domain: String): Boolean {
+ private fun isBlacklisted(source: MangaParserSource, domain: String): Boolean {
return blacklist[source]?.contains(domain) == true
}
- private fun addToBlacklist(source: MangaSource, domain: String) {
+ private fun addToBlacklist(source: MangaParserSource, domain: String) {
blacklist.getOrPut(source) {
ArraySet(2)
}.add(domain)
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/os/AppShortcutManager.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/os/AppShortcutManager.kt
index 986855095..71aad8f3f 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/os/AppShortcutManager.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/os/AppShortcutManager.kt
@@ -21,6 +21,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
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.favicon.faviconUri
import org.koitharu.kotatsu.core.prefs.AppSettings
@@ -173,9 +174,10 @@ class AppShortcutManager @Inject constructor(
onSuccess = { IconCompat.createWithAdaptiveBitmap(it) },
onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) },
)
+ val title = source.getTitle(context)
ShortcutInfoCompat.Builder(context, source.name)
- .setShortLabel(source.title)
- .setLongLabel(source.title)
+ .setShortLabel(title)
+ .setLongLabel(title)
.setIcon(icon)
.setLongLived(true)
.setIntent(MangaListActivity.newIntent(context, source))
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/CachingMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/CachingMangaRepository.kt
new file mode 100644
index 000000000..261f78900
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/CachingMangaRepository.kt
@@ -0,0 +1,104 @@
+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()
+ private val relatedMangaMutex = MultiMutex()
+ private val pagesMutex = MultiMutex()
+
+ final override suspend fun getDetails(manga: Manga): Manga = getDetails(manga, CachePolicy.ENABLED)
+
+ final override suspend fun getPages(chapter: MangaChapter): List = 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 = 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
+
+ protected abstract suspend fun getPagesImpl(chapter: MangaChapter): List
+
+ private suspend fun asyncSafe(block: suspend CoroutineScope.() -> T): SafeDeferred {
+ var dispatcher = currentCoroutineContext()[CoroutineDispatcher.Key]
+ if (dispatcher == null || dispatcher is MainCoroutineDispatcher) {
+ dispatcher = Dispatchers.Default
+ }
+ return SafeDeferred(
+ processLifecycleScope.async(dispatcher) {
+ runCatchingCancellable { block() }
+ },
+ )
+ }
+
+ private fun List.distinctById(): List {
+ if (isEmpty()) {
+ return emptyList()
+ }
+ val result = ArrayList(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
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/DummyParser.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/DummyParser.kt
index 5d05b9c0a..8bad91af4 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/DummyParser.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/DummyParser.kt
@@ -8,7 +8,7 @@ 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.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
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
*/
-class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
+class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaParserSource.DUMMY) {
override val configKeyDomain: ConfigKey.Domain
get() = ConfigKey.Domain("localhost")
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/EmptyParser.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/EmptyMangaRepository.kt
similarity index 50%
rename from app/src/main/kotlin/org/koitharu/kotatsu/core/parser/EmptyParser.kt
rename to app/src/main/kotlin/org/koitharu/kotatsu/core/parser/EmptyMangaRepository.kt
index cfea2d7ff..833b87edd 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/EmptyParser.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/EmptyMangaRepository.kt
@@ -1,38 +1,49 @@
package org.koitharu.kotatsu.core.parser
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
-import org.koitharu.kotatsu.parsers.MangaLoaderContext
-import org.koitharu.kotatsu.parsers.MangaParser
-import org.koitharu.kotatsu.parsers.config.ConfigKey
+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
-/**
- * This parser is just for parser development, it should not be used in releases
- */
-class EmptyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
+class EmptyMangaRepository(override val source: MangaSource) : MangaRepository {
- override val configKeyDomain: ConfigKey.Domain
- get() = ConfigKey.Domain("localhost")
-
- override val availableSortOrders: Set
+ override val sortOrders: Set
get() = EnumSet.allOf(SortOrder::class.java)
-
- override suspend fun getDetails(manga: Manga): Manga = stub(manga)
+ override val states: Set
+ get() = emptySet()
+ override val contentRatings: Set
+ 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 = stub(null)
+ override suspend fun getDetails(manga: Manga): Manga = stub(manga)
+
override suspend fun getPages(chapter: MangaChapter): List = stub(null)
- override suspend fun getAvailableTags(): Set = stub(null)
+ override suspend fun getPageUrl(page: MangaPage): String = stub(null)
+
+ override suspend fun getTags(): Set = stub(null)
+
+ override suspend fun getLocales(): Set = stub(null)
- override suspend fun getRelatedManga(seed: Manga): List = stub(seed)
+ override suspend fun getRelated(seed: Manga): List = stub(seed)
private fun stub(manga: Manga?): Nothing {
throw UnsupportedSourceException("This manga source is not supported", manga)
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaDataRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaDataRepository.kt
index ba2b08223..ffec4f507 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaDataRepository.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaDataRepository.kt
@@ -12,6 +12,7 @@ import org.koitharu.kotatsu.core.db.entity.toEntities
import org.koitharu.kotatsu.core.db.entity.toEntity
import org.koitharu.kotatsu.core.db.entity.toManga
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.prefs.ReaderMode
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
@@ -101,7 +102,7 @@ class MangaDataRepository @Inject constructor(
suspend fun cleanupLocalManga() {
val dao = db.getMangaDao()
- val broken = dao.findAllBySource(MangaSource.LOCAL.name)
+ val broken = dao.findAllBySource(LocalMangaSource.name)
.filter { x -> x.manga.url.toUri().toFileOrNull()?.exists() == false }
if (broken.isNotEmpty()) {
dao.delete(broken.map { it.manga })
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLinkResolver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLinkResolver.kt
index f1474aaa1..065529cc2 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLinkResolver.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLinkResolver.kt
@@ -4,10 +4,11 @@ import android.net.Uri
import coil.request.CachePolicy
import dagger.Reusable
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.explore.data.MangaSourcesRepository
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.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -36,7 +37,7 @@ class MangaLinkResolver @Inject constructor(
require(uri.pathSegments.singleOrNull() == "manga") { "Invalid url" }
val sourceName = requireNotNull(uri.getQueryParameter("source")) { "Source is not specified" }
val source = MangaSource(sourceName)
- require(source != MangaSource.UNKNOWN) { "Manga source $sourceName is not supported" }
+ require(source != UnknownMangaSource) { "Manga source $sourceName is not supported" }
val repo = repositoryFactory.create(source)
return repo.findExact(
url = uri.getQueryParameter("url"),
@@ -51,7 +52,7 @@ class MangaLinkResolver @Inject constructor(
val host = uri.host ?: return null
val repo = sourcesRepository.allMangaSources.asSequence()
.map { source ->
- repositoryFactory.create(source) as RemoteMangaRepository
+ repositoryFactory.create(source) as ParserMangaRepository
}.find { repo ->
host in repo.domains
} ?: return null
@@ -85,7 +86,7 @@ class MangaLinkResolver @Inject constructor(
}
private suspend fun MangaRepository.getDetailsNoCache(manga: Manga): Manga {
- return if (this is RemoteMangaRepository) {
+ return if (this is ParserMangaRepository) {
getDetails(manga, CachePolicy.READ_ONLY)
} else {
getDetails(manga)
@@ -108,7 +109,7 @@ class MangaLinkResolver @Inject constructor(
url = url,
publicUrl = "",
rating = 0.0f,
- isNsfw = source.contentType == ContentType.HENTAI,
+ isNsfw = source.isNsfw(),
coverUrl = "",
tags = emptySet(),
state = null,
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaParser.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaParser.kt
index 09262377c..3d12f5ecd 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaParser.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaParser.kt
@@ -2,12 +2,11 @@ package org.koitharu.kotatsu.core.parser
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
-import org.koitharu.kotatsu.parsers.model.MangaSource
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
-fun MangaParser(source: MangaSource, loaderContext: MangaLoaderContext): MangaParser {
+fun MangaParser(source: MangaParserSource, loaderContext: MangaLoaderContext): MangaParser {
return when (source) {
- MangaSource.UNKNOWN -> EmptyParser(loaderContext)
- MangaSource.DUMMY -> DummyParser(loaderContext)
+ MangaParserSource.DUMMY -> DummyParser(loaderContext)
else -> loaderContext.newParserInstance(source)
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaRepository.kt
index 72210b1e5..ae2bef8a9 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaRepository.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaRepository.kt
@@ -1,8 +1,16 @@
package org.koitharu.kotatsu.core.parser
+import android.content.Context
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.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.parser.external.ExternalMangaRepository
+import org.koitharu.kotatsu.core.parser.external.ExternalMangaSource
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.model.ContentRating
@@ -10,12 +18,12 @@ 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.MangaParserSource
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.lang.ref.WeakReference
-import java.util.EnumMap
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
@@ -53,32 +61,60 @@ interface MangaRepository {
suspend fun getRelated(seed: Manga): List
+ suspend fun find(manga: Manga): Manga? {
+ val list = getList(0, MangaListFilter.Search(manga.title))
+ return list.find { x -> x.id == manga.id }
+ }
+
@Singleton
class Factory @Inject constructor(
+ @ApplicationContext private val context: Context,
private val localMangaRepository: LocalMangaRepository,
private val loaderContext: MangaLoaderContext,
private val contentCache: MemoryContentCache,
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
) {
- private val cache = EnumMap>(MangaSource::class.java)
+ private val cache = ArrayMap>()
@AnyThread
fun create(source: MangaSource): MangaRepository {
- if (source == MangaSource.LOCAL) {
- return localMangaRepository
+ when (source) {
+ is MangaSourceInfo -> return create(source.mangaSource)
+ LocalMangaSource -> return localMangaRepository
+ UnknownMangaSource -> return EmptyMangaRepository(source)
}
cache[source]?.get()?.let { return it }
return synchronized(cache) {
cache[source]?.get()?.let { return it }
- val repository = RemoteMangaRepository(
- parser = MangaParser(source, loaderContext),
+ val repository = createRepository(source)
+ if (repository != null) {
+ 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,
- mirrorSwitchInterceptor = mirrorSwitchInterceptor,
)
- cache[source] = WeakReference(repository)
- repository
+ } else {
+ EmptyMangaRepository(source)
}
+
+ else -> null
}
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/ParserMangaRepository.kt
similarity index 54%
rename from app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt
rename to app/src/main/kotlin/org/koitharu/kotatsu/core/parser/ParserMangaRepository.kt
index 7d7e78fa6..f2f3a7b42 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/ParserMangaRepository.kt
@@ -1,24 +1,11 @@
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.Interceptor
import okhttp3.Response
-import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.cache.MemoryContentCache
-import org.koitharu.kotatsu.core.cache.SafeDeferred
import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor
import org.koitharu.kotatsu.core.prefs.SourceSettings
-import org.koitharu.kotatsu.core.util.MultiMutex
-import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
import org.koitharu.kotatsu.parsers.config.ConfigKey
@@ -28,7 +15,7 @@ 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.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
@@ -36,17 +23,13 @@ import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.util.Locale
-class RemoteMangaRepository(
+class ParserMangaRepository(
private val parser: MangaParser,
- private val cache: MemoryContentCache,
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
-) : MangaRepository, Interceptor {
+ cache: MemoryContentCache,
+) : CachingMangaRepository(cache), Interceptor {
- private val detailsMutex = MultiMutex()
- private val relatedMangaMutex = MultiMutex()
- private val pagesMutex = MultiMutex()
-
- override val source: MangaSource
+ override val source: MangaParserSource
get() = parser.source
override val sortOrders: Set
@@ -99,18 +82,11 @@ class RemoteMangaRepository(
}
}
- override suspend fun getDetails(manga: Manga): Manga = getDetails(manga, CachePolicy.ENABLED)
-
- override suspend fun getPages(chapter: MangaChapter): List = pagesMutex.withLock(chapter.id) {
- 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 getPagesImpl(
+ chapter: MangaChapter
+ ): List = mirrorSwitchInterceptor.withMirrorSwitching {
+ parser.getPages(chapter)
+ }
override suspend fun getPageUrl(page: MangaPage): String = mirrorSwitchInterceptor.withMirrorSwitching {
parser.getPageUrl(page)
@@ -128,37 +104,10 @@ class RemoteMangaRepository(
parser.getFavicons()
}
- override suspend fun getRelated(seed: Manga): List = relatedMangaMutex.withLock(seed.id) {
- cache.getRelatedManga(source, seed.url)?.let { return it }
- val related = asyncSafe {
- parser.getRelatedManga(seed).filterNot { it.id == seed.id }
- }
- cache.putRelatedManga(source, seed.url, related)
- 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 {
- mirrorSwitchInterceptor.withMirrorSwitching {
- parser.getDetails(manga)
- }
- }
- if (cachePolicy.writeEnabled) {
- cache.putDetails(source, manga.url, details)
- }
- details
- }.await()
+ override suspend fun getRelatedMangaImpl(seed: Manga): List = parser.getRelatedManga(seed)
- 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 }
+ override suspend fun getDetailsImpl(manga: Manga): Manga = mirrorSwitchInterceptor.withMirrorSwitching {
+ parser.getDetails(manga)
}
fun getAuthProvider(): MangaParserAuthProvider? = parser as? MangaParserAuthProvider
@@ -175,40 +124,8 @@ class RemoteMangaRepository(
return getConfig().isSlowdownEnabled
}
- fun invalidateCache() {
- cache.clear(source)
- }
-
fun getConfig() = parser.config as SourceSettings
- private suspend fun asyncSafe(block: suspend CoroutineScope.() -> T): SafeDeferred {
- var dispatcher = currentCoroutineContext()[CoroutineDispatcher.Key]
- if (dispatcher == null || dispatcher is MainCoroutineDispatcher) {
- dispatcher = Dispatchers.Default
- }
- return SafeDeferred(
- processLifecycleScope.async(dispatcher) {
- runCatchingCancellable { block() }
- },
- )
- }
-
- private fun List.distinctById(): List {
- if (isEmpty()) {
- return emptyList()
- }
- val result = ArrayList(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 MirrorSwitchInterceptor.withMirrorSwitching(block: suspend () -> R): R {
if (!isEnabled) {
return block()
@@ -220,14 +137,14 @@ class RemoteMangaRepository(
if (result.isValidResult()) {
return result.getOrThrow()
}
- return if (trySwitchMirror(this@RemoteMangaRepository)) {
+ return if (trySwitchMirror(this@ParserMangaRepository)) {
val newResult = runCatchingCancellable {
block()
}
if (newResult.isValidResult()) {
return newResult.getOrThrow()
} else {
- rollback(this@RemoteMangaRepository, initialMirror)
+ rollback(this@ParserMangaRepository, initialMirror)
return result.getOrThrow()
}
} else {
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalMangaRepository.kt
new file mode 100644
index 000000000..a047d0ebc
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalMangaRepository.kt
@@ -0,0 +1,264 @@
+package org.koitharu.kotatsu.core.parser.external
+
+import android.content.ContentResolver
+import android.database.Cursor
+import androidx.collection.ArraySet
+import androidx.core.database.getStringOrNull
+import androidx.core.net.toUri
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+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.ifNullOrEmpty
+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 ExternalMangaRepository(
+ private val contentResolver: ContentResolver,
+ override val source: ExternalMangaSource,
+ cache: MemoryContentCache,
+) : CachingMangaRepository(cache) {
+
+ private val capabilities by lazy { queryCapabilities() }
+
+ override val sortOrders: Set
+ get() = capabilities?.availableSortOrders ?: EnumSet.of(SortOrder.ALPHABETICAL)
+ override val states: Set
+ get() = capabilities?.availableStates.orEmpty()
+ override val contentRatings: Set
+ 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 =
+ runInterruptible(Dispatchers.Default) {
+ 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)?.use { cursor ->
+ val result = ArrayList(cursor.count)
+ if (cursor.moveToFirst()) {
+ do {
+ result += cursor.getManga()
+ } while (cursor.moveToNext())
+ }
+ result
+ }.orEmpty()
+ }
+
+ override suspend fun getDetailsImpl(manga: Manga): Manga = coroutineScope {
+ val chapters = async { 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.await(),
+ source = source,
+ )
+ }
+
+ override suspend fun getPagesImpl(chapter: MangaChapter): List = runInterruptible(Dispatchers.Default) {
+ val uri = "content://${source.authority}/chapters".toUri()
+ .buildUpon()
+ .appendPath(chapter.url)
+ .build()
+ contentResolver.query(uri, null, null, null, null)?.use { cursor ->
+ val result = ArrayList(cursor.count)
+ if (cursor.moveToFirst()) {
+ do {
+ result += MangaPage(
+ id = cursor.getLong(0),
+ url = cursor.getString(1),
+ preview = cursor.getStringOrNull(2),
+ source = source,
+ )
+ } while (cursor.moveToNext())
+ }
+ result
+ }.orEmpty()
+ }
+
+ override suspend fun getPageUrl(page: MangaPage): String = page.url
+
+ override suspend fun getTags(): Set = runInterruptible(Dispatchers.Default) {
+ val uri = "content://${source.authority}/tags".toUri()
+ contentResolver.query(uri, null, null, null, null)?.use { cursor ->
+ val result = ArraySet(cursor.count)
+ if (cursor.moveToFirst()) {
+ do {
+ result += MangaTag(
+ key = cursor.getString(0),
+ title = cursor.getString(1),
+ source = source,
+ )
+ } while (cursor.moveToNext())
+ }
+ result
+ }.orEmpty()
+ }
+
+ override suspend fun getLocales(): Set = emptySet()
+
+ override suspend fun getRelatedMangaImpl(seed: Manga): List = emptyList() // TODO
+
+ private suspend fun queryDetails(url: String): Manga = runInterruptible(Dispatchers.Default) {
+ val uri = "content://${source.authority}/manga".toUri()
+ .buildUpon()
+ .appendPath(url)
+ .build()
+ checkNotNull(
+ contentResolver.query(uri, null, null, null, null)?.use { cursor ->
+ cursor.moveToFirst()
+ cursor.getManga()
+ },
+ )
+ }
+
+ private suspend fun queryChapters(url: String): List? = runInterruptible(Dispatchers.Default) {
+ val uri = "content://${source.authority}/manga/chapters".toUri()
+ .buildUpon()
+ .appendPath(url)
+ .build()
+ contentResolver.query(uri, null, null, null, null)?.use { cursor ->
+ val result = ArrayList(cursor.count)
+ if (cursor.moveToFirst()) {
+ do {
+ result += MangaChapter(
+ id = cursor.getLong(0),
+ name = cursor.getString(1),
+ number = cursor.getFloat(2),
+ volume = cursor.getInt(3),
+ url = cursor.getString(4),
+ scanlator = cursor.getStringOrNull(5),
+ uploadDate = cursor.getLong(6),
+ branch = cursor.getStringOrNull(7),
+ source = source,
+ )
+ } while (cursor.moveToNext())
+ }
+ result
+ }
+ }
+
+ private fun Cursor.getManga() = Manga(
+ id = getLong(0),
+ title = getString(1),
+ altTitle = getStringOrNull(2),
+ url = getString(3),
+ publicUrl = getString(4),
+ rating = getFloat(5),
+ isNsfw = getInt(6) > 1,
+ coverUrl = getString(7),
+ tags = getStringOrNull(8)?.split(':')?.mapNotNullToSet {
+ val parts = it.splitTwoParts('=') ?: return@mapNotNullToSet null
+ MangaTag(key = parts.first, title = parts.second, source = source)
+ }.orEmpty(),
+ state = getStringOrNull(9)?.let { MangaState.entries.find(it) },
+ author = optString(10),
+ largeCoverUrl = optString(11),
+ description = optString(12),
+ chapters = emptyList(),
+ source = source,
+ )
+
+ private fun Cursor.optString(columnIndex: Int): String? {
+ return if (isNull(columnIndex)) {
+ null
+ } else {
+ getString(columnIndex)
+ }
+ }
+
+ private fun queryCapabilities(): MangaSourceCapabilities? {
+ val uri = "content://${source.authority}/capabilities".toUri()
+ return contentResolver.query(uri, null, null, null, null)?.use { cursor ->
+ if (cursor.moveToFirst()) {
+ MangaSourceCapabilities(
+ availableSortOrders = cursor.getStringOrNull(0)
+ ?.split(',')
+ ?.mapNotNullTo(EnumSet.noneOf(SortOrder::class.java)) {
+ SortOrder.entries.find(it)
+ }.orEmpty(),
+ availableStates = cursor.getStringOrNull(1)
+ ?.split(',')
+ ?.mapNotNullTo(EnumSet.noneOf(MangaState::class.java)) {
+ MangaState.entries.find(it)
+ }.orEmpty(),
+ availableContentRating = cursor.getStringOrNull(2)
+ ?.split(',')
+ ?.mapNotNullTo(EnumSet.noneOf(ContentRating::class.java)) {
+ ContentRating.entries.find(it)
+ }.orEmpty(),
+ isMultipleTagsSupported = cursor.getInt(3) > 1,
+ isTagsExclusionSupported = cursor.getInt(4) > 1,
+ isSearchSupported = cursor.getInt(5) > 1,
+ contentType = ContentType.entries.find(cursor.getString(6)) ?: ContentType.OTHER,
+ defaultSortOrder = cursor.getStringOrNull(7)?.let {
+ SortOrder.entries.find(it)
+ } ?: SortOrder.ALPHABETICAL,
+ sourceLocale = cursor.getStringOrNull(8)?.let { Locale(it) } ?: Locale.ROOT,
+ )
+ } else {
+ null
+ }
+ }
+ }
+
+ private class MangaSourceCapabilities(
+ val availableSortOrders: Set,
+ val availableStates: Set,
+ val availableContentRating: Set,
+ val isMultipleTagsSupported: Boolean,
+ val isTagsExclusionSupported: Boolean,
+ val isSearchSupported: Boolean,
+ val contentType: ContentType,
+ val defaultSortOrder: SortOrder,
+ val sourceLocale: Locale,
+ )
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalMangaSource.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalMangaSource.kt
new file mode 100644
index 000000000..cb15c293c
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalMangaSource.kt
@@ -0,0 +1,30 @@
+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
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/favicon/FaviconFetcher.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/favicon/FaviconFetcher.kt
index d024d94d1..ef155b03e 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/favicon/FaviconFetcher.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/favicon/FaviconFetcher.kt
@@ -1,12 +1,19 @@
package org.koitharu.kotatsu.core.parser.favicon
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.os.Build
import android.webkit.MimeTypeMap
import coil.ImageLoader
import coil.decode.DataSource
import coil.decode.ImageSource
import coil.disk.DiskCache
+import coil.fetch.DrawableResult
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
@@ -14,7 +21,9 @@ import coil.network.HttpException
import coil.request.Options
import coil.size.Size
import coil.size.pxOrElse
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.runInterruptible
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
@@ -24,8 +33,10 @@ import okio.Closeable
import okio.buffer
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
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.RemoteMangaRepository
+import org.koitharu.kotatsu.core.parser.ParserMangaRepository
+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.local.data.CacheDir
@@ -46,14 +57,27 @@ class FaviconFetcher(
) : Fetcher {
private val diskCacheKey
- get() = options.diskCacheKey ?: "${mangaSource.name}[${mangaSource.ordinal}]x${options.size.toCacheKey()}"
+ get() = options.diskCacheKey ?: "${mangaSource.name}x${options.size.toCacheKey()}"
private val fileSystem
get() = checkNotNull(diskCache.value).fileSystem
override suspend fun fetch(): FetchResult {
getCached(options)?.let { return it }
- val repo = mangaRepositoryFactory.create(mangaSource) as RemoteMangaRepository
+ return when (val repo = mangaRepositoryFactory.create(mangaSource)) {
+ 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(
options.size.width.pxOrElse { FALLBACK_SIZE },
options.size.height.pxOrElse { FALLBACK_SIZE },
@@ -100,6 +124,20 @@ class FaviconFetcher(
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? {
if (!options.diskCachePolicy.readEnabled) {
return null
@@ -165,6 +203,13 @@ 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(
context: Context,
okHttpClientLazy: Lazy,
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt
index fdbd623d8..23df27186 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt
@@ -192,8 +192,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getBoolean(KEY_FEED_HEADER, true)
set(value) = prefs.edit { putBoolean(KEY_FEED_HEADER, value) }
- val isReadingIndicatorsEnabled: Boolean
- get() = prefs.getBoolean(KEY_READING_INDICATORS, true)
+ val progressIndicatorMode: ProgressIndicatorMode
+ get() = prefs.getEnumValue(KEY_PROGRESS_INDICATORS, ProgressIndicatorMode.PERCENT_READ)
val isHistoryExcludeNsfw: Boolean
get() = prefs.getBoolean(KEY_HISTORY_EXCLUDE_NSFW, false)
@@ -619,7 +619,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_BACKUP_PERIODICAL_LAST = "backup_periodic_last"
const val KEY_HISTORY_GROUPING = "history_grouping"
const val KEY_UPDATED_GROUPING = "updated_grouping"
- const val KEY_READING_INDICATORS = "reading_indicators"
+ const val KEY_PROGRESS_INDICATORS = "progress_indicators"
const val KEY_REVERSE_CHAPTERS = "reverse_chapters"
const val KEY_GRID_VIEW_CHAPTERS = "grid_view_chapters"
const val KEY_HISTORY_EXCLUDE_NSFW = "history_exclude_nsfw"
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/ProgressIndicatorMode.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/ProgressIndicatorMode.kt
new file mode 100644
index 000000000..6bf1da864
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/ProgressIndicatorMode.kt
@@ -0,0 +1,6 @@
+package org.koitharu.kotatsu.core.prefs
+
+enum class ProgressIndicatorMode {
+
+ NONE, PERCENT_READ, PERCENT_LEFT, CHAPTERS_READ, CHAPTERS_LEFT;
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/AnimatedFaviconDrawable.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/AnimatedFaviconDrawable.kt
new file mode 100644
index 000000000..24e15150c
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/AnimatedFaviconDrawable.kt
@@ -0,0 +1,72 @@
+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)
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/FaviconDrawable.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/FaviconDrawable.kt
index b4931d916..5beacf886 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/FaviconDrawable.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/FaviconDrawable.kt
@@ -17,18 +17,18 @@ import com.google.android.material.color.MaterialColors
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.KotatsuColors
-class FaviconDrawable(
+open class FaviconDrawable(
context: Context,
@StyleRes styleResId: Int,
name: String,
) : Drawable() {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
- private var colorBackground = Color.WHITE
+ protected var colorBackground = Color.WHITE
+ protected var colorForeground = Color.DKGRAY
private var colorStroke = Color.LTGRAY
private val letter = name.take(1).uppercase()
private var cornerSize = 0f
- private var colorForeground = Color.DKGRAY
private val textBounds = Rect()
private val tempRect = Rect()
private val boundsF = RectF()
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/ListSelectionController.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/ListSelectionController.kt
index f2f5d7e4b..db0cdef05 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/ListSelectionController.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/ListSelectionController.kt
@@ -1,11 +1,11 @@
package org.koitharu.kotatsu.core.ui.list
-import android.app.Notification.Action
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.view.ActionMode
+import androidx.collection.LongSet
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
@@ -14,6 +14,8 @@ import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryOwner
import kotlinx.coroutines.Dispatchers
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
private const val KEY_SELECTION = "selection"
@@ -35,11 +37,9 @@ class ListSelectionController(
registryOwner.lifecycle.addObserver(StateEventObserver())
}
- fun snapshot(): Set {
- return peekCheckedIds().toSet()
- }
+ fun snapshot(): Set = peekCheckedIds().toSet()
- fun peekCheckedIds(): Set {
+ fun peekCheckedIds(): LongSet {
return decoration.checkedItemsIds
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/decor/AbstractSelectionItemDecoration.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/decor/AbstractSelectionItemDecoration.kt
index 8216f4aab..1c3be909c 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/decor/AbstractSelectionItemDecoration.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/decor/AbstractSelectionItemDecoration.kt
@@ -4,6 +4,8 @@ import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.RectF
import android.view.View
+import androidx.collection.LongSet
+import androidx.collection.MutableLongSet
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.NO_ID
@@ -12,7 +14,7 @@ abstract class AbstractSelectionItemDecoration : RecyclerView.ItemDecoration() {
private val bounds = Rect()
private val boundsF = RectF()
- protected val selection = HashSet()
+ protected val selection = MutableLongSet()
protected var hasBackground: Boolean = true
protected var hasForeground: Boolean = false
@@ -21,7 +23,7 @@ abstract class AbstractSelectionItemDecoration : RecyclerView.ItemDecoration() {
val checkedItemsCount: Int
get() = selection.size
- val checkedItemsIds: Set
+ val checkedItemsIds: LongSet
get() = selection
fun toggleItemChecked(id: Long) {
@@ -39,7 +41,9 @@ abstract class AbstractSelectionItemDecoration : RecyclerView.ItemDecoration() {
}
fun checkAll(ids: Collection) {
- selection.addAll(ids)
+ for (id in ids) {
+ selection.add(id)
+ }
}
fun clearSelection() {
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Collections.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Collections.kt
index 11400295c..edcb7845a 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Collections.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Collections.kt
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.util.ext
import androidx.collection.ArrayMap
import androidx.collection.ArraySet
+import androidx.collection.LongSet
import org.koitharu.kotatsu.BuildConfig
import java.util.Collections
import java.util.EnumSet
@@ -77,3 +78,16 @@ inline fun Collection.mapToArray(transform: (T) -> R): Array result[index] = transform(t) }
return result as Array
}
+
+fun LongSet.toLongArray(): LongArray {
+ val result = LongArray(size)
+ var i = 0
+ forEach { result[i++] = it }
+ return result
+}
+
+fun LongSet.toSet(): Set = toCollection(ArraySet(size))
+
+fun > LongSet.toCollection(out: R): R = out.also { result ->
+ forEach(result::add)
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsLoadUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsLoadUseCase.kt
index 837679389..32142b7f7 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsLoadUseCase.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsLoadUseCase.kt
@@ -55,11 +55,11 @@ class DetailsLoadUseCase @Inject constructor(
try {
val details = getDetails(manga)
launch { updateTracker(details) }
- send(MangaDetails(details, local?.peek(), details.description?.parseAsHtml(withImages = false), false))
- send(MangaDetails(details, local?.await(), details.description?.parseAsHtml(withImages = true), true))
+ send(MangaDetails(details, local?.peek(), details.description?.parseAsHtml(withImages = false)?.trim(), false))
+ send(MangaDetails(details, local?.await(), details.description?.parseAsHtml(withImages = true)?.trim(), true))
} catch (e: IOException) {
local?.await()?.manga?.also { localManga ->
- send(MangaDetails(localManga, null, localManga.description?.parseAsHtml(withImages = false), true))
+ send(MangaDetails(localManga, null, localManga.description?.parseAsHtml(withImages = false)?.trim(), true))
} ?: close(e)
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt
index 471f4aa32..1caca8498 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt
@@ -5,7 +5,9 @@ import android.content.Intent
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.EntryPointAccessors
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.isLocal
import org.koitharu.kotatsu.core.model.parcelable.ParcelableChapter
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.parser.MangaRepository
@@ -62,7 +64,7 @@ class MangaPrefetchService : CoroutineIntentService() {
private suspend fun prefetchLast() {
val last = historyRepository.getLastOrNull() ?: return
- if (last.source == MangaSource.LOCAL) return
+ if (last.isLocal) return
val repo = mangaRepositoryFactory.create(last.source)
val details = runCatchingCancellable { repo.getDetails(last) }.getOrNull() ?: return
val chapters = details.chapters
@@ -110,7 +112,7 @@ class MangaPrefetchService : CoroutineIntentService() {
}
private fun isPrefetchAvailable(context: Context, source: MangaSource?): Boolean {
- if (source == MangaSource.LOCAL || context.isPowerSaveMode()) {
+ if (source == LocalMangaSource || context.isPowerSaveMode()) {
return false
}
val entryPoint = EntryPointAccessors.fromApplication(
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
index c3e4b772d..77b087326 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
@@ -43,6 +43,9 @@ import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.model.FavouriteCategory
+import org.koitharu.kotatsu.core.model.LocalMangaSource
+import org.koitharu.kotatsu.core.model.UnknownMangaSource
+import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.model.iconResId
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.model.titleResId
@@ -86,15 +89,14 @@ import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
import org.koitharu.kotatsu.image.ui.ImageActivity
-import org.koitharu.kotatsu.list.domain.ListExtraProvider
+import org.koitharu.kotatsu.list.domain.MangaListMapper
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD
import org.koitharu.kotatsu.list.ui.model.ListModel
-import org.koitharu.kotatsu.list.ui.model.MangaItemModel
+import org.koitharu.kotatsu.list.ui.model.MangaListModel
import org.koitharu.kotatsu.list.ui.size.StaticItemSizeResolver
import org.koitharu.kotatsu.local.ui.info.LocalInfoDialog
import org.koitharu.kotatsu.parsers.model.Manga
-import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.ellipsize
import org.koitharu.kotatsu.reader.ui.ReaderActivity
@@ -120,7 +122,7 @@ class DetailsActivity :
lateinit var coil: ImageLoader
@Inject
- lateinit var tagHighlighter: ListExtraProvider
+ lateinit var listMapper: MangaListMapper
private val viewModel: DetailsViewModel by viewModels()
private lateinit var menuProvider: DetailsMenuProvider
@@ -389,7 +391,7 @@ class DetailsActivity :
}
}
- private fun onRelatedMangaChanged(related: List) {
+ private fun onRelatedMangaChanged(related: List) {
if (related.isEmpty()) {
viewBinding.groupRelated.isVisible = false
return
@@ -463,10 +465,10 @@ class DetailsActivity :
imageViewState.isVisible = false
}
- if (manga.source == MangaSource.LOCAL || manga.source == MangaSource.UNKNOWN) {
+ if (manga.source == LocalMangaSource || manga.source == UnknownMangaSource) {
infoLayout.chipSource.isVisible = false
} else {
- infoLayout.chipSource.text = manga.source.title
+ infoLayout.chipSource.text = manga.source.getTitle(this@DetailsActivity)
infoLayout.chipSource.isVisible = true
}
@@ -611,15 +613,7 @@ class DetailsActivity :
private fun bindTags(manga: Manga) {
viewBinding.chipsTags.isVisible = manga.tags.isNotEmpty()
- viewBinding.chipsTags.setChips(
- manga.tags.map { tag ->
- ChipsView.ChipModel(
- title = tag.title,
- tint = tagHighlighter.getTagTint(tag),
- data = tag,
- )
- },
- )
+ viewBinding.chipsTags.setChips(listMapper.mapTags(manga.tags))
}
private fun loadCover(manga: Manga) {
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt
index 87ef10aa0..c37f5813e 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt
@@ -16,11 +16,12 @@ import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity
import org.koitharu.kotatsu.browser.BrowserActivity
+import org.koitharu.kotatsu.core.model.LocalMangaSource
+import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ShareHelper
import org.koitharu.kotatsu.download.ui.dialog.DownloadOption
-import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet
@@ -38,10 +39,10 @@ class DetailsMenuProvider(
override fun onPrepareMenu(menu: Menu) {
val manga = viewModel.manga.value
- menu.findItem(R.id.action_save).isVisible = manga?.source != null && manga.source != MangaSource.LOCAL
- menu.findItem(R.id.action_delete).isVisible = manga?.source == MangaSource.LOCAL
- menu.findItem(R.id.action_browser).isVisible = manga?.source != MangaSource.LOCAL
- menu.findItem(R.id.action_alternatives).isVisible = manga?.source != MangaSource.LOCAL
+ menu.findItem(R.id.action_save).isVisible = manga?.source != null && manga.source != LocalMangaSource
+ menu.findItem(R.id.action_delete).isVisible = manga?.source == LocalMangaSource
+ menu.findItem(R.id.action_browser).isVisible = manga?.source != LocalMangaSource
+ menu.findItem(R.id.action_alternatives).isVisible = manga?.source != LocalMangaSource
menu.findItem(R.id.action_shortcut).isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(activity)
menu.findItem(R.id.action_scrobbling).isVisible = viewModel.isScrobblingAvailable
menu.findItem(R.id.action_online).isVisible = viewModel.remoteManga.value != null
@@ -53,7 +54,7 @@ class DetailsMenuProvider(
R.id.action_share -> {
viewModel.manga.value?.let {
val shareHelper = ShareHelper(activity)
- if (it.source == MangaSource.LOCAL) {
+ if (it.isLocal) {
shareHelper.shareCbz(listOf(it.url.toUri().toFile()))
} else {
shareHelper.shareMangaLink(it)
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
index 5d1d6c798..bb1336682 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
@@ -50,9 +50,8 @@ import org.koitharu.kotatsu.details.ui.model.HistoryInfo
import org.koitharu.kotatsu.details.ui.model.MangaBranch
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.history.data.HistoryRepository
-import org.koitharu.kotatsu.list.domain.ListExtraProvider
-import org.koitharu.kotatsu.list.ui.model.MangaItemModel
-import org.koitharu.kotatsu.list.ui.model.toUi
+import org.koitharu.kotatsu.list.domain.MangaListMapper
+import org.koitharu.kotatsu.list.ui.model.MangaListModel
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
import org.koitharu.kotatsu.local.domain.model.LocalManga
@@ -76,7 +75,7 @@ class DetailsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
private val relatedMangaUseCase: RelatedMangaUseCase,
- private val extraProvider: ListExtraProvider,
+ private val mangaListMapper: MangaListMapper,
private val detailsLoadUseCase: DetailsLoadUseCase,
private val progressUpdateUseCase: ProgressUpdateUseCase,
private val readingTimeUseCase: ReadingTimeUseCase,
@@ -171,9 +170,12 @@ class DetailsViewModel @Inject constructor(
val scrobblingInfo: StateFlow> = interactor.observeScrobblingInfo(mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
- val relatedManga: StateFlow> = manga.mapLatest {
+ val relatedManga: StateFlow> = manga.mapLatest {
if (it != null && settings.isRelatedMangaEnabled) {
- relatedMangaUseCase.invoke(it)?.toUi(ListMode.GRID, extraProvider).orEmpty()
+ mangaListMapper.toListModelList(
+ manga = relatedMangaUseCase(it).orEmpty(),
+ mode = ListMode.GRID,
+ )
} else {
emptyList()
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/chapters/ChaptersFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/chapters/ChaptersFragment.kt
index 764f87738..ed4e5c805 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/chapters/ChaptersFragment.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/chapters/ChaptersFragment.kt
@@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.LocalMangaSource
import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
@@ -31,6 +32,8 @@ import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
import org.koitharu.kotatsu.core.util.ext.findParentCallback
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
+import org.koitharu.kotatsu.core.util.ext.toCollection
+import org.koitharu.kotatsu.core.util.ext.toSet
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding
import org.koitharu.kotatsu.details.ui.DetailsViewModel
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
@@ -40,7 +43,6 @@ import org.koitharu.kotatsu.details.ui.withVolumeHeaders
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService
-import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
import org.koitharu.kotatsu.reader.ui.ReaderNavigationCallback
import org.koitharu.kotatsu.reader.ui.ReaderState
@@ -137,10 +139,10 @@ class ChaptersFragment :
val ids = selectionController?.peekCheckedIds()
val manga = viewModel.manga.value
when {
- ids.isNullOrEmpty() || manga == null -> Unit
+ ids == null || ids.isEmpty() || manga == null -> Unit
ids.size == manga.chapters?.size -> viewModel.deleteLocal()
else -> {
- LocalChaptersRemoveService.start(requireContext(), manga, ids)
+ LocalChaptersRemoveService.start(requireContext(), manga, ids.toSet())
Snackbar.make(
requireViewBinding().recyclerViewChapters,
R.string.chapters_will_removed_background,
@@ -154,7 +156,7 @@ class ChaptersFragment :
R.id.action_select_range -> {
val items = chaptersAdapter?.items ?: return false
- val ids = HashSet(controller.peekCheckedIds())
+ val ids = controller.peekCheckedIds().toCollection(HashSet())
val buffer = HashSet()
var isAdding = false
for (x in items) {
@@ -188,8 +190,12 @@ class ChaptersFragment :
}
R.id.action_mark_current -> {
- val id = controller.peekCheckedIds().singleOrNull() ?: return false
- viewModel.markChapterAsCurrent(id)
+ val ids = controller.peekCheckedIds()
+ if (ids.size == 1) {
+ viewModel.markChapterAsCurrent(ids.first())
+ } else {
+ return false
+ }
mode.finish()
true
}
@@ -218,7 +224,7 @@ class ChaptersFragment :
var canSave = true
var canDelete = true
items.forEach { (_, x) ->
- val isLocal = x.isDownloaded || x.chapter.source == MangaSource.LOCAL
+ val isLocal = x.isDownloaded || x.chapter.source == LocalMangaSource
if (isLocal) canSave = false else canDelete = false
}
menu.findItem(R.id.action_save).isVisible = canSave
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/related/RelatedListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/related/RelatedListViewModel.kt
index 8cc83be4b..679f36693 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/related/RelatedListViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/related/RelatedListViewModel.kt
@@ -20,12 +20,11 @@ import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
-import org.koitharu.kotatsu.list.domain.ListExtraProvider
+import org.koitharu.kotatsu.list.domain.MangaListMapper
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorState
-import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.Manga
import javax.inject.Inject
@@ -34,7 +33,7 @@ class RelatedListViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
mangaRepositoryFactory: MangaRepository.Factory,
settings: AppSettings,
- private val extraProvider: ListExtraProvider,
+ private val mangaListMapper: MangaListMapper,
downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler) {
@@ -46,14 +45,14 @@ class RelatedListViewModel @Inject constructor(
override val content = combine(
mangaList,
- listMode,
+ observeListModeWithTriggers(),
listError,
) { list, mode, error ->
when {
list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true))
list == null -> listOf(LoadingState)
list.isEmpty() -> listOf(createEmptyState())
- else -> list.toUi(mode, extraProvider)
+ else -> mangaListMapper.toListModelList(list, mode)
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt
index 828f0cef2..09c42f904 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt
@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.download.ui.list
import androidx.collection.ArrayMap
+import androidx.collection.LongSet
import androidx.collection.LongSparseArray
import androidx.collection.getOrElse
import androidx.collection.set
@@ -24,7 +25,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.formatNumber
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
-import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
+import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
@@ -182,7 +183,7 @@ class DownloadsViewModel @Inject constructor(
}
}
- fun snapshot(ids: Set): Collection {
+ fun snapshot(ids: LongSet): Collection {
return works.value?.filterTo(ArrayList(ids.size)) { x -> x.id.mostSignificantBits in ids }.orEmpty()
}
@@ -325,6 +326,6 @@ class DownloadsViewModel @Inject constructor(
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
private suspend fun tryLoad(manga: Manga) = runCatchingCancellable {
- (mangaRepositoryFactory.create(manga.source) as RemoteMangaRepository).getDetails(manga)
+ (mangaRepositoryFactory.create(manga.source) as ParserMangaRepository).getDetails(manga)
}.getOrNull()
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt
index aa23dc794..cd6c5b72d 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt
@@ -21,13 +21,13 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.LocalMangaSource
import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.download.ui.list.DownloadsActivity
import org.koitharu.kotatsu.parsers.model.Manga
-import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.search.ui.MangaListActivity
@@ -231,7 +231,7 @@ class DownloadNotificationFactory @AssistedInject constructor(
if (manga != null) {
DetailsActivity.newIntent(context, manga)
} else {
- MangaListActivity.newIntent(context, MangaSource.LOCAL)
+ MangaListActivity.newIntent(context, LocalMangaSource)
},
PendingIntent.FLAG_CANCEL_CURRENT,
false,
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadSlowdownDispatcher.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadSlowdownDispatcher.kt
index e6a512f95..9f2f2e3b2 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadSlowdownDispatcher.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadSlowdownDispatcher.kt
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.download.ui.worker
import androidx.collection.MutableObjectLongMap
import kotlinx.coroutines.delay
import org.koitharu.kotatsu.core.parser.MangaRepository
-import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
+import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.parsers.model.MangaSource
class DownloadSlowdownDispatcher(
@@ -13,7 +13,7 @@ class DownloadSlowdownDispatcher(
private val timeMap = MutableObjectLongMap()
suspend fun delay(source: MangaSource) {
- val repo = mangaRepositoryFactory.create(source) as? RemoteMangaRepository ?: return
+ val repo = mangaRepositoryFactory.create(source) as? ParserMangaRepository ?: return
if (!repo.isSlowdownEnabled()) {
return
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt
index 2c1ae89ac..4a30fa7b8 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt
@@ -43,6 +43,7 @@ import okio.use
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.TooManyRequestExceptions
import org.koitharu.kotatsu.core.model.ids
+import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
import org.koitharu.kotatsu.core.parser.MangaDataRepository
@@ -179,7 +180,7 @@ class DownloadWorker @AssistedInject constructor(
checkNotNull(destination) { applicationContext.getString(R.string.cannot_find_available_storage) }
var output: LocalMangaOutput? = null
try {
- if (manga.source == MangaSource.LOCAL) {
+ if (manga.isLocal) {
manga = localMangaRepository.getRemoteManga(manga)
?: error("Cannot obtain remote manga instance")
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt
index 1021e6d70..e2bc04cc2 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt
@@ -1,8 +1,16 @@
package org.koitharu.kotatsu.explore.data
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import androidx.core.content.ContextCompat
import androidx.room.withTransaction
-import dagger.Reusable
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
@@ -12,20 +20,27 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
+import org.koitharu.kotatsu.core.model.MangaSourceInfo
+import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.model.isNsfw
+import org.koitharu.kotatsu.core.parser.external.ExternalMangaSource
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
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.util.mapNotNullToSet
import org.koitharu.kotatsu.parsers.util.mapToSet
import java.util.Collections
import java.util.EnumSet
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
+import javax.inject.Singleton
-@Reusable
+@Singleton
class MangaSourcesRepository @Inject constructor(
+ @ApplicationContext private val context: Context,
private val db: MangaDatabase,
private val settings: AppSettings,
) {
@@ -34,15 +49,13 @@ class MangaSourcesRepository @Inject constructor(
private val dao: MangaSourcesDao
get() = db.getSourcesDao()
- private val remoteSources = EnumSet.allOf(MangaSource::class.java).apply {
- remove(MangaSource.LOCAL)
- remove(MangaSource.UNKNOWN)
+ private val remoteSources = EnumSet.allOf(MangaParserSource::class.java).apply {
if (!BuildConfig.DEBUG) {
- remove(MangaSource.DUMMY)
+ remove(MangaParserSource.DUMMY)
}
}
- val allMangaSources: Set
+ val allMangaSources: Set
get() = Collections.unmodifiableSet(remoteSources)
suspend fun getEnabledSources(): List {
@@ -54,7 +67,7 @@ class MangaSourcesRepository @Inject constructor(
suspend fun getPinnedSources(): Set {
assimilateNewSources()
val skipNsfw = settings.isNsfwContentDisabled
- return dao.findAllPinned().mapNotNullTo(EnumSet.noneOf(MangaSource::class.java)) {
+ return dao.findAllPinned().mapNotNullToSet {
it.source.toMangaSourceOrNull()?.takeUnless { x -> skipNsfw && x.isNsfw() }
}
}
@@ -75,7 +88,7 @@ class MangaSourcesRepository @Inject constructor(
return result
}
- suspend fun getAvailableSources(
+ suspend fun queryParserSources(
isDisabledOnly: Boolean,
isNewOnly: Boolean,
excludeBroken: Boolean,
@@ -83,7 +96,7 @@ class MangaSourcesRepository @Inject constructor(
query: String?,
locale: String?,
sortOrder: SourcesSortOrder?,
- ): List {
+ ): List {
assimilateNewSources()
val entities = dao.findAll().toMutableList()
if (isDisabledOnly) {
@@ -95,7 +108,9 @@ class MangaSourcesRepository @Inject constructor(
val sources = entities.toSources(
skipNsfwSources = settings.isNsfwContentDisabled,
sortOrder = sortOrder,
- )
+ ).run {
+ mapNotNullTo(ArrayList(size)) { it.mangaSource as? MangaParserSource }
+ }
if (locale != null) {
sources.retainAll { it.locale == locale }
}
@@ -107,7 +122,7 @@ class MangaSourcesRepository @Inject constructor(
}
if (!query.isNullOrEmpty()) {
sources.retainAll {
- it.title.contains(query, ignoreCase = true) || it.name.contains(query, ignoreCase = true)
+ it.getTitle(context).contains(query, ignoreCase = true) || it.name.contains(query, ignoreCase = true)
}
}
return sources
@@ -140,14 +155,21 @@ class MangaSourcesRepository @Inject constructor(
}.distinctUntilChanged().onStart { assimilateNewSources() }
}
- fun observeEnabledSources(): Flow> = combine(
+ fun observeEnabledSources(): Flow> = combine(
observeIsNsfwDisabled(),
observeSortOrder(),
) { skipNsfw, order ->
dao.observeEnabled(order).map {
it.toSources(skipNsfw, order)
}
- }.flatMapLatest { it }.onStart { assimilateNewSources() }
+ }.flatMapLatest { it }
+ .onStart { assimilateNewSources() }
+ .combine(observeExternalSources()) { enabled, external ->
+ val list = ArrayList(enabled.size + external.size)
+ external.mapTo(list) { MangaSourceInfo(it, isEnabled = true, isPinned = true) }
+ list.addAll(enabled)
+ list
+ }
fun observeAll(): Flow>> = dao.observeAll().map { entities ->
val result = ArrayList>(entities.size)
@@ -264,6 +286,15 @@ class MangaSourcesRepository @Inject constructor(
}
}
+ private suspend fun getNewSources(): MutableSet {
+ val entities = dao.findAll()
+ val result = EnumSet.copyOf(remoteSources)
+ for (e in entities) {
+ result.remove(e.source.toMangaSourceOrNull() ?: continue)
+ }
+ return result
+ }
+
private suspend fun setSourcesPinnedImpl(sources: Collection, isPinned: Boolean) {
if (sources.size == 1) { // fast path
dao.setPinned(sources.first().name, isPinned)
@@ -276,35 +307,63 @@ class MangaSourcesRepository @Inject constructor(
}
}
- private suspend fun getNewSources(): MutableSet {
- val entities = dao.findAll()
- val result = EnumSet.copyOf(remoteSources)
- for (e in entities) {
- result.remove(e.source.toMangaSourceOrNull() ?: continue)
- }
- return result
+ private fun observeExternalSources(): Flow> {
+ val intent = Intent("app.kotatsu.parser.PROVIDE_MANGA")
+ val pm = context.packageManager
+ return callbackFlow {
+ val receiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ trySendBlocking(intent)
+ }
+ }
+ ContextCompat.registerReceiver(
+ context,
+ receiver,
+ IntentFilter().apply {
+ addAction(Intent.ACTION_PACKAGE_ADDED)
+ addAction(Intent.ACTION_PACKAGE_VERIFIED)
+ addAction(Intent.ACTION_PACKAGE_REPLACED)
+ addAction(Intent.ACTION_PACKAGE_REMOVED)
+ addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED)
+ addDataScheme("package")
+ },
+ ContextCompat.RECEIVER_EXPORTED,
+ )
+ awaitClose { context.unregisterReceiver(receiver) }
+ }.onStart {
+ emit(null)
+ }.map {
+ pm.queryIntentContentProviders(intent, 0).map { resolveInfo ->
+ ExternalMangaSource(
+ packageName = resolveInfo.providerInfo.packageName,
+ authority = resolveInfo.providerInfo.authority,
+ )
+ }
+ }.distinctUntilChanged()
}
private fun List.toSources(
skipNsfwSources: Boolean,
sortOrder: SourcesSortOrder?,
- ): MutableList {
- val result = ArrayList(size)
- val pinned = EnumSet.noneOf(MangaSource::class.java)
+ ): MutableList {
+ val result = ArrayList(size)
for (entity in this) {
val source = entity.source.toMangaSourceOrNull() ?: continue
if (skipNsfwSources && source.isNsfw()) {
continue
}
if (source in remoteSources) {
- result.add(source)
- if (entity.isPinned) {
- pinned.add(source)
- }
+ result.add(
+ MangaSourceInfo(
+ mangaSource = source,
+ isEnabled = entity.isEnabled,
+ isPinned = entity.isPinned,
+ ),
+ )
}
}
if (sortOrder == SourcesSortOrder.ALPHABETIC) {
- result.sortWith(compareBy { it in pinned }.thenBy { it.title })
+ result.sortWith(compareBy { !it.isPinned }.thenBy { it.getTitle(context) })
}
return result
}
@@ -317,5 +376,5 @@ class MangaSourcesRepository @Inject constructor(
sourcesSortOrder
}
- private fun String.toMangaSourceOrNull(): MangaSource? = MangaSource.entries.find { it.name == this }
+ private fun String.toMangaSourceOrNull(): MangaParserSource? = MangaParserSource.entries.find { it.name == this }
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/domain/ExploreRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/domain/ExploreRepository.kt
index 9a473aac6..19446b1ee 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/domain/ExploreRepository.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/domain/ExploreRepository.kt
@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.explore.domain
+import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.almostEquals
@@ -7,7 +8,6 @@ import org.koitharu.kotatsu.core.util.ext.asArrayList
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.history.data.HistoryRepository
-import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -45,7 +45,7 @@ class ExploreRepository @Inject constructor(
suspend fun findRandomManga(source: MangaSource, tagsLimit: Int): Manga {
val tagsBlacklist = TagsBlacklist(settings.suggestionsTagsBlacklist, 0.4f)
- val skipNsfw = settings.isSuggestionsExcludeNsfw && source.contentType != ContentType.HENTAI
+ val skipNsfw = settings.isSuggestionsExcludeNsfw && !source.isNsfw()
val tags = historyRepository.getPopularTags(tagsLimit).mapNotNull {
if (it in tagsBlacklist) null else it.title
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt
index fc334dde9..519735223 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt
@@ -2,6 +2,8 @@ package org.koitharu.kotatsu.explore.ui
import android.content.DialogInterface
import android.content.Intent
+import android.net.Uri
+import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
@@ -20,6 +22,8 @@ import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.ui.AllBookmarksActivity
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
+import org.koitharu.kotatsu.core.model.LocalMangaSource
+import org.koitharu.kotatsu.core.parser.external.ExternalMangaSource
import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.dialog.TwoButtonsAlertDialog
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
@@ -40,8 +44,7 @@ import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.parsers.model.Manga
-import org.koitharu.kotatsu.parsers.model.MangaSource
-import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.settings.sources.catalog.SourcesCatalogActivity
@@ -123,7 +126,7 @@ class ExploreFragment :
override fun onClick(v: View) {
val intent = when (v.id) {
- R.id.button_local -> MangaListActivity.newIntent(v.context, MangaSource.LOCAL)
+ R.id.button_local -> MangaListActivity.newIntent(v.context, LocalMangaSource)
R.id.button_bookmarks -> AllBookmarksActivity.newIntent(v.context)
R.id.button_more -> SuggestionsActivity.newIntent(v.context)
R.id.button_downloads -> DownloadsActivity.newIntent(v.context)
@@ -165,16 +168,19 @@ class ExploreFragment :
}
override fun onPrepareActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
- val isSingleSelection = controller.count == 1
+ val selectedSources = viewModel.sourcesSnapshot(controller.peekCheckedIds())
+ val isSingleSelection = selectedSources.size == 1
menu.findItem(R.id.action_settings).isVisible = isSingleSelection
menu.findItem(R.id.action_shortcut).isVisible = isSingleSelection
+ menu.findItem(R.id.action_pin).isVisible = selectedSources.all { !it.isPinned }
+ menu.findItem(R.id.action_unpin).isVisible = selectedSources.all { it.isPinned }
+ menu.findItem(R.id.action_disable)?.isVisible = selectedSources.all { it.mangaSource is MangaParserSource }
+ menu.findItem(R.id.action_delete)?.isVisible = selectedSources.all { it.mangaSource is ExternalMangaSource }
return super.onPrepareActionMode(controller, mode, menu)
}
override fun onActionItemClicked(controller: ListSelectionController, mode: ActionMode, item: MenuItem): Boolean {
- val selectedSources = controller.peekCheckedIds().mapNotNullToSet { id ->
- MangaSource.entries.getOrNull(id.toInt())
- }
+ val selectedSources = viewModel.sourcesSnapshot(controller.peekCheckedIds())
if (selectedSources.isEmpty()) {
return false
}
@@ -190,6 +196,13 @@ class ExploreFragment :
mode.finish()
}
+ R.id.action_delete -> {
+ selectedSources.forEach {
+ (it.mangaSource as? ExternalMangaSource)?.let { uninstallExternalSource(it) }
+ }
+ mode.finish()
+ }
+
R.id.action_shortcut -> {
val source = selectedSources.singleOrNull() ?: return false
viewModel.requestPinShortcut(source)
@@ -238,4 +251,14 @@ class ExploreFragment :
.create()
.show()
}
+
+ private fun uninstallExternalSource(source: ExternalMangaSource) {
+ val uri = Uri.fromParts("package", source.packageName, null)
+ val action = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ Intent.ACTION_DELETE
+ } else {
+ Intent.ACTION_UNINSTALL_PACKAGE
+ }
+ context?.startActivity(Intent(action, uri))
+ }
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt
index fad516b34..717d6c321 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt
@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.explore.ui
+import androidx.collection.LongSet
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
@@ -13,6 +14,7 @@ import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.MangaSourceInfo
import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
@@ -26,12 +28,11 @@ import org.koitharu.kotatsu.explore.domain.ExploreRepository
import org.koitharu.kotatsu.explore.ui.model.ExploreButtons
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
import org.koitharu.kotatsu.explore.ui.model.RecommendationsItem
-import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.list.ui.model.EmptyHint
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState
-import org.koitharu.kotatsu.list.ui.model.MangaListModel
+import org.koitharu.kotatsu.list.ui.model.MangaCompactListModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
@@ -108,7 +109,7 @@ class ExploreViewModel @Inject constructor(
}
}
- fun setSourcesPinned(sources: Set, isPinned: Boolean) {
+ fun setSourcesPinned(sources: Collection, isPinned: Boolean) {
launchJob(Dispatchers.Default) {
sourcesRepository.setIsPinned(sources, isPinned)
val message = if (sources.size == 1) {
@@ -125,6 +126,12 @@ class ExploreViewModel @Inject constructor(
settings.closeTip(TIP_SUGGESTIONS)
}
+ fun sourcesSnapshot(ids: LongSet): List {
+ return content.value.mapNotNull {
+ (it as? MangaSourceItem)?.takeIf { x -> x.id in ids }?.source
+ }
+ }
+
private fun createContentFlow() = combine(
sourcesRepository.observeEnabledSources(),
getSuggestionFlow(),
@@ -136,7 +143,7 @@ class ExploreViewModel @Inject constructor(
}.withErrorHandling()
private fun buildList(
- sources: List,
+ sources: List,
recommendation: List,
isGrid: Boolean,
randomLoading: Boolean,
@@ -182,14 +189,15 @@ class ExploreViewModel @Inject constructor(
}
private fun List.toRecommendationList() = map { manga ->
- MangaListModel(
+ MangaCompactListModel(
id = manga.id,
title = manga.title,
subtitle = manga.tags.joinToString { it.title },
coverUrl = manga.coverUrl,
manga = manga,
counter = 0,
- progress = PROGRESS_NONE,
+ progress = null,
+ isFavorite = false,
)
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapterDelegates.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapterDelegates.kt
index ecbb50d91..28b93d6e5 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapterDelegates.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapterDelegates.kt
@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.explore.ui.adapter
import android.view.View
+import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
@@ -9,11 +10,13 @@ import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.BaseListAdapter
+import org.koitharu.kotatsu.core.ui.image.AnimatedFaviconDrawable
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
+import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.recyclerView
@@ -31,7 +34,7 @@ import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
import org.koitharu.kotatsu.explore.ui.model.RecommendationsItem
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.model.ListModel
-import org.koitharu.kotatsu.list.ui.model.MangaListModel
+import org.koitharu.kotatsu.list.ui.model.MangaCompactListModel
import org.koitharu.kotatsu.parsers.model.Manga
fun exploreButtonsAD(
@@ -63,7 +66,7 @@ fun exploreRecommendationItemAD(
{ layoutInflater, parent -> ItemRecommendationBinding.inflate(layoutInflater, parent, false) },
) {
- val adapter = BaseListAdapter()
+ val adapter = BaseListAdapter()
.addDelegate(ListItemType.MANGA_LIST, recommendationMangaItemAD(coil, itemClickListener, lifecycleOwner))
binding.pager.adapter = adapter
binding.pager.recyclerView?.isNestedScrollingEnabled = false
@@ -78,7 +81,7 @@ fun recommendationMangaItemAD(
coil: ImageLoader,
itemClickListener: OnListItemClickListener,
lifecycleOwner: LifecycleOwner,
-) = adapterDelegateViewBinding(
+) = adapterDelegateViewBinding(
{ layoutInflater, parent -> ItemRecommendationMangaBinding.inflate(layoutInflater, parent, false) },
) {
@@ -115,6 +118,7 @@ fun exploreSourceListItemAD(
) {
val eventListener = AdapterDelegateClickListenerAdapter(this, listener)
+ val iconPinned = ContextCompat.getDrawable(context, R.drawable.ic_pin_small)
binding.root.setOnClickListener(eventListener)
binding.root.setOnLongClickListener(eventListener)
@@ -122,11 +126,12 @@ fun exploreSourceListItemAD(
bind {
binding.textViewTitle.text = item.source.getTitle(context)
+ binding.textViewTitle.drawableStart = if (item.source.isPinned) iconPinned else null
binding.textViewSubtitle.text = item.source.getSummary(context)
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
fallback(fallbackIcon)
- placeholder(fallbackIcon)
+ placeholder(AnimatedFaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name))
error(fallbackIcon)
source(item.source)
enqueueWith(coil)
@@ -150,6 +155,7 @@ fun exploreSourceGridItemAD(
) {
val eventListener = AdapterDelegateClickListenerAdapter(this, listener)
+ val iconPinned = ContextCompat.getDrawable(context, R.drawable.ic_pin_small)
binding.root.setOnClickListener(eventListener)
binding.root.setOnLongClickListener(eventListener)
@@ -157,10 +163,11 @@ fun exploreSourceGridItemAD(
bind {
binding.textViewTitle.text = item.source.getTitle(context)
+ binding.textViewTitle.drawableStart = if (item.source.isPinned) iconPinned else null
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Large, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
fallback(fallbackIcon)
- placeholder(fallbackIcon)
+ placeholder(AnimatedFaviconDrawable(context, R.style.FaviconDrawable_Large, item.source.name))
error(fallbackIcon)
source(item.source)
enqueueWith(coil)
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/model/MangaSourceItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/model/MangaSourceItem.kt
index 4866dc2d1..9c084c80a 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/model/MangaSourceItem.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/model/MangaSourceItem.kt
@@ -1,15 +1,15 @@
package org.koitharu.kotatsu.explore.ui.model
+import org.koitharu.kotatsu.core.model.MangaSourceInfo
+import org.koitharu.kotatsu.core.util.ext.longHashCode
import org.koitharu.kotatsu.list.ui.model.ListModel
-import org.koitharu.kotatsu.parsers.model.MangaSource
data class MangaSourceItem(
- val source: MangaSource,
+ val source: MangaSourceInfo,
val isGrid: Boolean,
) : ListModel {
- val id: Long
- get() = source.ordinal.toLong()
+ val id: Long = source.name.longHashCode()
override fun areItemsTheSame(other: ListModel): Boolean {
return other is MangaSourceItem && other.source == source
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/model/RecommendationsItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/model/RecommendationsItem.kt
index 347ea9bba..078a00b61 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/model/RecommendationsItem.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/model/RecommendationsItem.kt
@@ -1,10 +1,10 @@
package org.koitharu.kotatsu.explore.ui.model
import org.koitharu.kotatsu.list.ui.model.ListModel
-import org.koitharu.kotatsu.list.ui.model.MangaListModel
+import org.koitharu.kotatsu.list.ui.model.MangaCompactListModel
data class RecommendationsItem(
- val manga: List
+ val manga: List
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt
index 13f54b9d6..982540f1d 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt
@@ -115,6 +115,9 @@ abstract class FavouritesDao {
@Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id IN (:mangaIds) AND deleted_at = 0 ORDER BY favourites.created_at ASC")
abstract suspend fun findCategoriesIds(mangaIds: Collection): List
+ @Query("SELECT COUNT(category_id) FROM favourites WHERE manga_id = :mangaId AND deleted_at = 0")
+ abstract suspend fun findCategoriesCount(mangaId: Long): Int
+
/** INSERT **/
@Insert(onConflict = OnConflictStrategy.REPLACE)
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt
index 84387e157..eecac0d30 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt
@@ -115,6 +115,10 @@ class FavouritesRepository @Inject constructor(
return db.getFavouriteCategoriesDao().find(id.toInt()).toFavouriteCategory()
}
+ suspend fun isFavorite(mangaId: Long): Boolean {
+ return db.getFavouritesDao().findCategoriesCount(mangaId) != 0
+ }
+
suspend fun getCategoriesIds(mangaIds: Collection): Set {
return db.getFavouritesDao().findCategoriesIds(mangaIds).toSet()
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/model/Cover.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/model/Cover.kt
index e76c18724..32c0d4c0f 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/model/Cover.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/model/Cover.kt
@@ -1,12 +1,10 @@
package org.koitharu.kotatsu.favourites.domain.model
-import org.koitharu.kotatsu.parsers.model.MangaSource
-import org.koitharu.kotatsu.parsers.util.find
+import org.koitharu.kotatsu.core.model.MangaSource
data class Cover(
val url: String,
val source: String,
) {
- val mangaSource: MangaSource?
- get() = if (source.isEmpty()) null else MangaSource.entries.find(source)
+ val mangaSource by lazy { MangaSource(source) }
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt
index 3df13093a..b4c653264 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt
@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.favourites.ui.categories
+import androidx.collection.LongSet
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
@@ -76,7 +77,7 @@ class FavouritesCategoriesViewModel @Inject constructor(
}
}
- fun getCategories(ids: Set): ArrayList {
+ fun getCategories(ids: LongSet): ArrayList {
val items = content.requireValue()
return items.mapNotNullTo(ArrayList(ids.size)) { item ->
(item as? CategoryListModel)?.category?.takeIf { it.id in ids }
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt
index a49bdf217..0d2f78118 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt
@@ -10,13 +10,13 @@ import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.util.ext.sortedByOrdinal
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.list.ui.MangaListFragment
-import org.koitharu.kotatsu.parsers.model.MangaSource
@AndroidEntryPoint
class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener {
@@ -57,9 +57,7 @@ class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickLis
}
override fun onPrepareActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
- menu.findItem(R.id.action_save)?.isVisible = selectedItems.none {
- it.source == MangaSource.LOCAL
- }
+ menu.findItem(R.id.action_save)?.isVisible = selectedItems.none { it.isLocal }
return super.onPrepareActionMode(controller, mode, menu)
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt
index 4bd578710..249b70c47 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt
@@ -24,13 +24,12 @@ import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.ARG_CATEGORY_ID
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
import org.koitharu.kotatsu.history.domain.MarkAsReadUseCase
-import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.domain.ListSortOrder
+import org.koitharu.kotatsu.list.domain.MangaListMapper
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorState
-import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.Manga
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
@@ -41,7 +40,7 @@ private const val PAGE_SIZE = 20
class FavouritesListViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val repository: FavouritesRepository,
- private val listExtraProvider: ListExtraProvider,
+ private val mangaListMapper: MangaListMapper,
private val markAsReadUseCase: MarkAsReadUseCase,
settings: AppSettings,
downloadScheduler: DownloadWorker.Scheduler,
@@ -67,7 +66,7 @@ class FavouritesListViewModel @Inject constructor(
override val content = combine(
observeFavorites(),
- listMode,
+ observeListModeWithTriggers(),
refreshTrigger,
) { list, mode, _ ->
when {
@@ -86,7 +85,7 @@ class FavouritesListViewModel @Inject constructor(
else -> {
isReady.set(true)
- list.toUi(mode, listExtraProvider)
+ mangaListMapper.toListModelList(list, mode)
}
}
}.catch {
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt
index e664b7785..304a0be28 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt
@@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
@@ -31,7 +32,6 @@ import org.koitharu.kotatsu.core.util.LocaleComparator
import org.koitharu.kotatsu.core.util.ext.asArrayList
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
-import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
import org.koitharu.kotatsu.filter.ui.model.TagCatalogItem
@@ -43,6 +43,7 @@ import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorFooter
import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.model.MangaListFilter
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
@@ -66,7 +67,7 @@ class FilterCoordinator @Inject constructor(
) : MangaFilter {
private val coroutineScope = lifecycle.lifecycleScope
- private val repository = mangaRepositoryFactory.create(savedStateHandle.require(RemoteListFragment.ARG_SOURCE))
+ private val repository = mangaRepositoryFactory.create(MangaSource(savedStateHandle[RemoteListFragment.ARG_SOURCE]))
private val currentState = MutableStateFlow(
MangaListFilter.Advanced(
sortOrder = repository.defaultSortOrder,
@@ -451,7 +452,7 @@ class FilterCoordinator @Inject constructor(
}
private fun mergeTags(primary: Set, secondary: Set): Set {
- val result = TreeSet(TagTitleComparator(repository.source.locale))
+ val result = TreeSet(TagTitleComparator((repository.source as? MangaParserSource)?.locale))
result.addAll(secondary)
result.addAll(primary)
return result
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt
index 8add1dc3f..93b3b8db2 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt
@@ -88,7 +88,7 @@ abstract class HistoryDao {
abstract suspend fun insert(entity: HistoryEntity): Long
@Query(
- "UPDATE history SET page = :page, chapter_id = :chapterId, scroll = :scroll, percent = :percent, updated_at = :updatedAt, deleted_at = 0 WHERE manga_id = :mangaId",
+ "UPDATE history SET page = :page, chapter_id = :chapterId, scroll = :scroll, percent = :percent, updated_at = :updatedAt, chapters = :chapters, deleted_at = 0 WHERE manga_id = :mangaId",
)
abstract suspend fun update(
mangaId: Long,
@@ -96,6 +96,7 @@ abstract class HistoryDao {
chapterId: Long,
scroll: Float,
percent: Float,
+ chapters: Int,
updatedAt: Long,
): Int
@@ -116,6 +117,7 @@ abstract class HistoryDao {
chapterId = entity.chapterId,
scroll = entity.scroll,
percent = entity.percent,
+ chapters = entity.chaptersCount,
updatedAt = entity.updatedAt,
)
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt
index c5736a1f3..f9a85f9df 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt
@@ -19,10 +19,12 @@ import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
import org.koitharu.kotatsu.core.util.ext.mapItems
import org.koitharu.kotatsu.history.domain.model.MangaWithHistory
import org.koitharu.kotatsu.list.domain.ListSortOrder
+import org.koitharu.kotatsu.list.domain.ReadingProgress
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
@@ -102,6 +104,7 @@ class HistoryRepository @Inject constructor(
assert(manga.chapters != null)
db.withTransaction {
mangaRepository.storeManga(manga)
+ val branch = manga.chapters?.findById(chapterId)?.branch
db.getHistoryDao().upsert(
HistoryEntity(
mangaId = manga.id,
@@ -111,7 +114,7 @@ class HistoryRepository @Inject constructor(
page = page,
scroll = scroll.toFloat(), // we migrate to int, but decide to not update database
percent = percent,
- chaptersCount = manga.chapters?.size ?: -1,
+ chaptersCount = manga.chapters?.count { it.branch == branch } ?: 0,
deletedAt = 0L,
),
)
@@ -124,8 +127,13 @@ class HistoryRepository @Inject constructor(
return db.getHistoryDao().find(manga.id)?.recoverIfNeeded(manga)?.toMangaHistory()
}
- suspend fun getProgress(mangaId: Long): Float {
- return db.getHistoryDao().findProgress(mangaId) ?: PROGRESS_NONE
+ suspend fun getProgress(mangaId: Long, mode: ProgressIndicatorMode): ReadingProgress? {
+ val entity = db.getHistoryDao().find(mangaId) ?: return null
+ return ReadingProgress(
+ percent = entity.percent,
+ totalChapters = entity.chaptersCount,
+ mode = mode,
+ ).takeIf { it.isValid() }
}
suspend fun clear() {
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt
index 4d5a1cac3..03c2ee975 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt
@@ -8,6 +8,7 @@ import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.os.NetworkManageIntent
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.RecyclerScrollKeeper
@@ -17,7 +18,6 @@ import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.list.ui.size.DynamicItemSizeResolver
-import org.koitharu.kotatsu.parsers.model.MangaSource
@AndroidEntryPoint
class HistoryListFragment : MangaListFragment() {
@@ -44,9 +44,7 @@ class HistoryListFragment : MangaListFragment() {
}
override fun onPrepareActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
- menu.findItem(R.id.action_save)?.isVisible = selectedItems.none {
- it.source == MangaSource.LOCAL
- }
+ menu.findItem(R.id.action_save)?.isVisible = selectedItems.none { it.isLocal }
return super.onPrepareActionMode(controller, mode, menu)
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt
index e10d0c626..6485c98f3 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt
@@ -28,8 +28,8 @@ import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.history.domain.MarkAsReadUseCase
import org.koitharu.kotatsu.history.domain.model.MangaWithHistory
-import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.domain.ListSortOrder
+import org.koitharu.kotatsu.list.domain.MangaListMapper
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyHint
import org.koitharu.kotatsu.list.ui.model.EmptyState
@@ -38,9 +38,6 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.TipModel
import org.koitharu.kotatsu.list.ui.model.toErrorState
-import org.koitharu.kotatsu.list.ui.model.toGridModel
-import org.koitharu.kotatsu.list.ui.model.toListDetailedModel
-import org.koitharu.kotatsu.list.ui.model.toListModel
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import java.time.Instant
@@ -53,7 +50,7 @@ private const val PAGE_SIZE = 20
class HistoryListViewModel @Inject constructor(
private val repository: HistoryRepository,
settings: AppSettings,
- private val extraProvider: ListExtraProvider,
+ private val mangaListMapper: MangaListMapper,
private val localMangaRepository: LocalMangaRepository,
private val markAsReadUseCase: MarkAsReadUseCase,
networkState: NetworkState,
@@ -91,7 +88,7 @@ class HistoryListViewModel @Inject constructor(
override val content = combine(
observeHistory(),
isGroupingEnabled,
- listMode,
+ observeListModeWithTriggers(),
networkState,
settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled },
) { list, grouped, mode, online, incognito ->
@@ -203,11 +200,7 @@ class HistoryListViewModel @Inject constructor(
prevHeader = header
}
}
- result += when (mode) {
- ListMode.LIST -> manga.toListModel(extraProvider)
- ListMode.DETAILED_LIST -> manga.toListDetailedModel(extraProvider)
- ListMode.GRID -> manga.toGridModel(extraProvider)
- }
+ result += mangaListMapper.toListModel(manga, mode)
}
return result
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt
index 618b8d788..13e5f0da6 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt
@@ -26,7 +26,6 @@ class ReadingProgressDrawable(
private val outlineColor: Int
private val backgroundColor: Int
private val textColor: Int
- private val textPattern = context.getString(R.string.percent_string_pattern)
private val textBounds = Rect()
private val tempRect = Rect()
private val hasBackground: Boolean
@@ -36,14 +35,18 @@ class ReadingProgressDrawable(
private val desiredWidth: Int
private val autoFitTextSize: Boolean
- var progress: Float = PROGRESS_NONE
+ var percent: Float = PROGRESS_NONE
+ set(value) {
+ field = value
+ invalidateSelf()
+ }
+
+ var text = ""
set(value) {
field = value
- text = textPattern.format((value * 100f).toInt().toString())
paint.getTextBounds(text, 0, text.length, textBounds)
invalidateSelf()
}
- private var text = ""
init {
val ta = context.obtainStyledAttributes(styleResId, R.styleable.ProgressDrawable)
@@ -79,7 +82,7 @@ class ReadingProgressDrawable(
}
override fun draw(canvas: Canvas) {
- if (progress < 0f) {
+ if (percent < 0f) {
return
}
val cx = bounds.exactCenterX()
@@ -103,12 +106,12 @@ class ReadingProgressDrawable(
cx + innerRadius,
cy + innerRadius,
-90f,
- 360f * progress,
+ 360f * percent,
false,
paint,
)
if (hasText) {
- if (checkDrawable != null && progress >= 1f - Math.ulp(progress)) {
+ if (checkDrawable != null && percent >= 1f - Math.ulp(percent)) {
tempRect.set(bounds)
tempRect.scale(0.6)
checkDrawable.bounds = tempRect
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/util/ReadingProgressView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/util/ReadingProgressView.kt
index 744201215..1e19c9b93 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/util/ReadingProgressView.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/util/ReadingProgressView.kt
@@ -11,8 +11,14 @@ import android.view.animation.AccelerateDecelerateInterpolator
import androidx.annotation.AttrRes
import androidx.annotation.StyleRes
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode.CHAPTERS_LEFT
+import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode.CHAPTERS_READ
+import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode.NONE
+import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode.PERCENT_LEFT
+import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode.PERCENT_READ
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
+import org.koitharu.kotatsu.list.domain.ReadingProgress
class ReadingProgressView @JvmOverloads constructor(
context: Context,
@@ -20,17 +26,30 @@ class ReadingProgressView @JvmOverloads constructor(
@AttrRes defStyleAttr: Int = 0,
) : View(context, attrs, defStyleAttr), ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener {
+ private val percentPattern = context.getString(R.string.percent_string_pattern)
private var percentAnimator: ValueAnimator? = null
private val animationDuration = context.getAnimationDuration(android.R.integer.config_shortAnimTime)
@StyleRes
private val drawableStyle: Int
- var percent: Float
- get() = peekProgressDrawable()?.progress ?: PROGRESS_NONE
+ var progress: ReadingProgress? = null
set(value) {
+ field = value
cancelAnimation()
- getProgressDrawable().progress = value
+ getProgressDrawable().also {
+ it.percent = value?.percent ?: PROGRESS_NONE
+ it.text = when (value?.mode) {
+ null,
+ NONE -> ""
+
+ PERCENT_READ -> percentPattern.format((value.percent * 100f).toInt().toString())
+ PERCENT_LEFT -> "-" + percentPattern.format((value.percentLeft * 100f).toInt().toString())
+
+ CHAPTERS_READ -> value.chapters.toString()
+ CHAPTERS_LEFT -> "-" + value.chaptersLeft.toString()
+ }
+ }
}
init {
@@ -39,7 +58,11 @@ class ReadingProgressView @JvmOverloads constructor(
ta.recycle()
outlineProvider = OutlineProvider()
if (isInEditMode) {
- percent = 0.27f
+ progress = ReadingProgress(
+ percent = 0.27f,
+ totalChapters = 20,
+ mode = PERCENT_READ,
+ )
}
}
@@ -53,7 +76,7 @@ class ReadingProgressView @JvmOverloads constructor(
override fun onAnimationUpdate(animation: ValueAnimator) {
val p = animation.animatedValue as Float
- getProgressDrawable().progress = p
+ getProgressDrawable().percent = p
}
override fun onAnimationStart(animation: Animator) = Unit
@@ -68,16 +91,25 @@ class ReadingProgressView @JvmOverloads constructor(
override fun onAnimationRepeat(animation: Animator) = Unit
- fun setPercent(value: Float, animate: Boolean) {
+ fun setProgress(percent: Float, animate: Boolean) {
+ setProgress(
+ value = ReadingProgress(percent, 1, PERCENT_READ),
+ animate = animate,
+ )
+ }
+
+ fun setProgress(value: ReadingProgress?, animate: Boolean) {
val currentDrawable = peekProgressDrawable()
- if (!animate || currentDrawable == null || value == PROGRESS_NONE) {
- percent = value
+ if (!animate || currentDrawable == null || value == null) {
+ progress = value
return
}
percentAnimator?.cancel()
+ val currentPercent = currentDrawable.percent.coerceAtLeast(0f)
+ progress = value.copy(percent = currentPercent)
percentAnimator = ValueAnimator.ofFloat(
- currentDrawable.progress.coerceAtLeast(0f),
- value,
+ currentDrawable.percent.coerceAtLeast(0f),
+ value.percent,
).apply {
duration = animationDuration
interpolator = AccelerateDecelerateInterpolator()
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/image/ui/ImageActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/image/ui/ImageActivity.kt
index d3ea527bf..a8e7440e1 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/image/ui/ImageActivity.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/image/ui/ImageActivity.kt
@@ -25,13 +25,13 @@ import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
+import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.util.PopupMenuMediator
import org.koitharu.kotatsu.core.util.ShareHelper
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getDisplayIcon
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
-import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
@@ -120,7 +120,7 @@ class ImageActivity : BaseActivity(), ImageRequest.Listene
.memoryCachePolicy(CachePolicy.DISABLED)
.lifecycle(this)
.listener(this)
- .source(intent.getSerializableExtraCompat(EXTRA_SOURCE))
+ .source(MangaSource(intent.getStringExtra(EXTRA_SOURCE)))
.target(SsivTarget(viewBinding.ssiv))
.enqueueWith(coil)
}
@@ -180,7 +180,7 @@ class ImageActivity : BaseActivity(), ImageRequest.Listene
fun newIntent(context: Context, url: String, source: MangaSource?): Intent {
return Intent(context, ImageActivity::class.java)
.setData(Uri.parse(url))
- .putExtra(EXTRA_SOURCE, source)
+ .putExtra(EXTRA_SOURCE, source?.name)
}
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListExtraProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListExtraProvider.kt
deleted file mode 100644
index 60cc18885..000000000
--- a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListExtraProvider.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-package org.koitharu.kotatsu.list.domain
-
-import android.content.Context
-import androidx.annotation.ColorRes
-import dagger.Reusable
-import dagger.hilt.android.qualifiers.ApplicationContext
-import org.koitharu.kotatsu.R
-import org.koitharu.kotatsu.core.prefs.AppSettings
-import org.koitharu.kotatsu.history.data.HistoryRepository
-import org.koitharu.kotatsu.history.data.PROGRESS_NONE
-import org.koitharu.kotatsu.parsers.model.MangaTag
-import org.koitharu.kotatsu.tracker.domain.TrackingRepository
-import javax.inject.Inject
-
-@Reusable
-class ListExtraProvider @Inject constructor(
- @ApplicationContext context: Context,
- private val settings: AppSettings,
- private val trackingRepository: TrackingRepository,
- private val historyRepository: HistoryRepository,
-) {
-
- private val dict by lazy {
- context.resources.openRawResource(R.raw.tags_redlist).use {
- val set = HashSet()
- it.bufferedReader().forEachLine { x ->
- val line = x.trim()
- if (line.isNotEmpty()) {
- set.add(line)
- }
- }
- set
- }
- }
-
- suspend fun getCounter(mangaId: Long): Int {
- return if (settings.isTrackerEnabled) {
- trackingRepository.getNewChaptersCount(mangaId)
- } else {
- 0
- }
- }
-
- suspend fun getProgress(mangaId: Long): Float {
- return if (settings.isReadingIndicatorsEnabled) {
- historyRepository.getProgress(mangaId)
- } else {
- PROGRESS_NONE
- }
- }
-
- @ColorRes
- fun getTagTint(tag: MangaTag): Int {
- return if (tag.title.lowercase() in dict) {
- R.color.warning
- } else {
- 0
- }
- }
-}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListMapper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListMapper.kt
new file mode 100644
index 000000000..802833859
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListMapper.kt
@@ -0,0 +1,131 @@
+package org.koitharu.kotatsu.list.domain
+
+import android.content.Context
+import androidx.annotation.ColorRes
+import androidx.collection.MutableScatterSet
+import androidx.collection.ScatterSet
+import dagger.hilt.android.qualifiers.ApplicationContext
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.core.prefs.ListMode
+import org.koitharu.kotatsu.core.ui.widgets.ChipsView
+import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
+import org.koitharu.kotatsu.history.data.HistoryRepository
+import org.koitharu.kotatsu.list.ui.model.MangaCompactListModel
+import org.koitharu.kotatsu.list.ui.model.MangaDetailedListModel
+import org.koitharu.kotatsu.list.ui.model.MangaGridModel
+import org.koitharu.kotatsu.list.ui.model.MangaListModel
+import org.koitharu.kotatsu.parsers.model.Manga
+import org.koitharu.kotatsu.parsers.model.MangaTag
+import org.koitharu.kotatsu.tracker.domain.TrackingRepository
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class MangaListMapper @Inject constructor(
+ @ApplicationContext context: Context,
+ private val settings: AppSettings,
+ private val trackingRepository: TrackingRepository,
+ private val historyRepository: HistoryRepository,
+ private val favouritesRepository: FavouritesRepository,
+) {
+
+ private val dict by lazy { readTagsDict(context) }
+
+ suspend fun toListModelList(manga: Collection, mode: ListMode): List = manga.map {
+ toListModel(it, mode)
+ }
+
+ suspend fun toListModelList(
+ destination: MutableCollection,
+ manga: Collection,
+ mode: ListMode
+ ) = manga.mapTo(destination) {
+ toListModel(it, mode)
+ }
+
+ suspend fun toListModel(manga: Manga, mode: ListMode): MangaListModel = when (mode) {
+ ListMode.LIST -> toCompactListModel(manga)
+ ListMode.DETAILED_LIST -> toDetailedListModel(manga)
+ ListMode.GRID -> toGridModel(manga)
+ }
+
+ suspend fun toCompactListModel(manga: Manga) = MangaCompactListModel(
+ id = manga.id,
+ title = manga.title,
+ subtitle = manga.tags.joinToString(", ") { it.title },
+ coverUrl = manga.coverUrl,
+ manga = manga,
+ counter = getCounter(manga.id),
+ progress = getProgress(manga.id),
+ isFavorite = isFavorite(manga.id),
+ )
+
+ suspend fun toDetailedListModel(manga: Manga) = MangaDetailedListModel(
+ id = manga.id,
+ title = manga.title,
+ subtitle = manga.altTitle,
+ coverUrl = manga.coverUrl,
+ manga = manga,
+ counter = getCounter(manga.id),
+ progress = getProgress(manga.id),
+ isFavorite = isFavorite(manga.id),
+ tags = mapTags(manga.tags),
+ )
+
+ suspend fun toGridModel(manga: Manga) = MangaGridModel(
+ id = manga.id,
+ title = manga.title,
+ coverUrl = manga.coverUrl,
+ manga = manga,
+ counter = getCounter(manga.id),
+ progress = getProgress(manga.id),
+ isFavorite = isFavorite(manga.id),
+ )
+
+ fun mapTags(tags: Collection) = tags.map {
+ ChipsView.ChipModel(
+ tint = getTagTint(it),
+ title = it.title,
+ data = it,
+ )
+ }
+
+ private suspend fun getCounter(mangaId: Long): Int {
+ return if (settings.isTrackerEnabled) {
+ trackingRepository.getNewChaptersCount(mangaId)
+ } else {
+ 0
+ }
+ }
+
+ private suspend fun getProgress(mangaId: Long): ReadingProgress? {
+ return historyRepository.getProgress(mangaId, settings.progressIndicatorMode)
+ }
+
+ private fun isFavorite(mangaId: Long): Boolean {
+ return false // TODO favouritesRepository.isFavorite(mangaId)
+ }
+
+ @ColorRes
+ private fun getTagTint(tag: MangaTag): Int {
+ return if (tag.title.lowercase() in dict) {
+ R.color.warning
+ } else {
+ 0
+ }
+ }
+
+ private fun readTagsDict(context: Context): ScatterSet =
+ context.resources.openRawResource(R.raw.tags_redlist).use {
+ val set = MutableScatterSet()
+ it.bufferedReader().forEachLine { x ->
+ val line = x.trim()
+ if (line.isNotEmpty()) {
+ set.add(line)
+ }
+ }
+ set.trim()
+ set
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ReadingProgress.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ReadingProgress.kt
new file mode 100644
index 000000000..e0120adcb
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ReadingProgress.kt
@@ -0,0 +1,35 @@
+package org.koitharu.kotatsu.list.domain
+
+import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode
+import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode.CHAPTERS_LEFT
+import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode.CHAPTERS_READ
+import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode.NONE
+import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode.PERCENT_LEFT
+import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode.PERCENT_READ
+
+data class ReadingProgress(
+ val percent: Float,
+ val totalChapters: Int,
+ val mode: ProgressIndicatorMode,
+) {
+
+ val percentLeft: Float
+ get() = 1f - percent
+
+ val chapters: Int
+ get() = (totalChapters * percent).toInt()
+
+ val chaptersLeft: Int
+ get() = (totalChapters * percentLeft).toInt()
+
+ fun isValid() = when (mode) {
+ NONE -> false
+ PERCENT_READ,
+ PERCENT_LEFT -> percent in 0f..1f
+
+ CHAPTERS_READ,
+ CHAPTERS_LEFT -> totalChapters > 0 && percent in 0f..1f
+ }
+
+ fun isReversed() = mode == PERCENT_LEFT || mode == CHAPTERS_LEFT
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt
index 61f3e49a3..03d46fe2c 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt
@@ -20,7 +20,6 @@ import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
-import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.prefs.AppSettings
@@ -51,7 +50,7 @@ import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
-import org.koitharu.kotatsu.list.ui.model.MangaItemModel
+import org.koitharu.kotatsu.list.ui.model.MangaListModel
import org.koitharu.kotatsu.list.ui.size.DynamicItemSizeResolver
import org.koitharu.kotatsu.main.ui.MainActivity
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
@@ -281,7 +280,7 @@ abstract class MangaListFragment :
return when (item.itemId) {
R.id.action_select_all -> {
val ids = listAdapter?.items?.mapNotNull {
- (it as? MangaItemModel)?.id
+ (it as? MangaListModel)?.id
} ?: return false
selectionController?.addAll(ids)
true
@@ -327,7 +326,7 @@ abstract class MangaListFragment :
val items = listAdapter?.items ?: return emptySet()
val result = ArraySet(checkedIds.size)
for (item in items) {
- if (item is MangaItemModel && item.id in checkedIds) {
+ if (item is MangaListModel && item.id in checkedIds) {
result.add(item.manga)
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt
index e8af462a8..c0aa69200 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt
@@ -2,11 +2,16 @@ package org.koitharu.kotatsu.list.ui
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel
@@ -55,4 +60,13 @@ abstract class MangaListViewModel(
} else {
this
}
+
+ protected fun observeListModeWithTriggers(): Flow = combine(
+ listMode,
+ settings.observe().filter { key ->
+ key == AppSettings.KEY_PROGRESS_INDICATORS || key == AppSettings.KEY_TRACKER_ENABLED
+ }.onStart { emit("") }
+ ) { mode, _ ->
+ mode
+ }
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaSelectionDecoration.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaSelectionDecoration.kt
index a2824ed63..81aec7a3a 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaSelectionDecoration.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaSelectionDecoration.kt
@@ -14,7 +14,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.decor.AbstractSelectionItemDecoration
import org.koitharu.kotatsu.core.util.ext.getItem
import org.koitharu.kotatsu.core.util.ext.getThemeColor
-import org.koitharu.kotatsu.list.ui.model.MangaItemModel
+import org.koitharu.kotatsu.list.ui.model.MangaListModel
import com.google.android.material.R as materialR
open class MangaSelectionDecoration(context: Context) : AbstractSelectionItemDecoration() {
@@ -37,7 +37,7 @@ open class MangaSelectionDecoration(context: Context) : AbstractSelectionItemDec
override fun getItemId(parent: RecyclerView, child: View): Long {
val holder = parent.getChildViewHolder(child) ?: return NO_ID
- val item = holder.getItem(MangaItemModel::class.java) ?: return NO_ID
+ val item = holder.getItem(MangaListModel::class.java) ?: return NO_ID
return item.id
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt
index 0fc374be2..ade54bbdd 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt
@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.list.ui.adapter
import android.view.View
+import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.google.android.material.badge.BadgeDrawable
@@ -14,7 +15,7 @@ import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemMangaGridBinding
-import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
+import org.koitharu.kotatsu.list.ui.ListModelDiffCallback.Companion.PAYLOAD_PROGRESS_CHANGED
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
@@ -41,7 +42,8 @@ fun mangaGridItemAD(
bind { payloads ->
binding.textViewTitle.text = item.title
- binding.progressView.setPercent(item.progress, ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED in payloads)
+ binding.progressView.setProgress(item.progress, PAYLOAD_PROGRESS_CHANGED in payloads)
+ binding.imageViewFavorite.isVisible = item.isFavorite
binding.imageViewCover.newImageRequest(lifecycleOwner, item.coverUrl)?.run {
size(CoverSizeResolver(binding.imageViewCover))
defaultPlaceholders(context)
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt
index 5e575d225..aa8449e47 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt
@@ -16,13 +16,13 @@ import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemMangaListDetailsBinding
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
-import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
+import org.koitharu.kotatsu.list.ui.model.MangaDetailedListModel
fun mangaListDetailedItemAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: MangaDetailsClickListener,
-) = adapterDelegateViewBinding(
+) = adapterDelegateViewBinding(
{ inflater, parent -> ItemMangaListDetailsBinding.inflate(inflater, parent, false) },
) {
var badge: BadgeDrawable? = null
@@ -39,7 +39,10 @@ fun mangaListDetailedItemAD(
bind { payloads ->
binding.textViewTitle.text = item.title
binding.textViewAuthor.textAndVisible = item.manga.author
- binding.progressView.setPercent(item.progress, ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED in payloads)
+ binding.progressView.setProgress(
+ value = item.progress,
+ animate = ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED in payloads,
+ )
binding.imageViewCover.newImageRequest(lifecycleOwner, item.coverUrl)?.run {
size(CoverSizeResolver(binding.imageViewCover))
defaultPlaceholders(context)
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt
index 6a92013f5..8115b990b 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt
@@ -15,14 +15,14 @@ import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemMangaListBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
-import org.koitharu.kotatsu.list.ui.model.MangaListModel
+import org.koitharu.kotatsu.list.ui.model.MangaCompactListModel
import org.koitharu.kotatsu.parsers.model.Manga
fun mangaListItemAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener,
-) = adapterDelegateViewBinding(
+) = adapterDelegateViewBinding(
{ inflater, parent -> ItemMangaListBinding.inflate(inflater, parent, false) },
) {
var badge: BadgeDrawable? = null
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt
index 42a847b46..d489538b2 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt
@@ -3,74 +3,8 @@ package org.koitharu.kotatsu.list.ui.model
import androidx.annotation.StringRes
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
-import org.koitharu.kotatsu.core.prefs.ListMode
-import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.getDisplayIcon
import org.koitharu.kotatsu.core.util.ext.ifZero
-import org.koitharu.kotatsu.history.data.PROGRESS_NONE
-import org.koitharu.kotatsu.list.domain.ListExtraProvider
-import org.koitharu.kotatsu.parsers.model.Manga
-
-suspend fun Manga.toListModel(
- extraProvider: ListExtraProvider?
-) = MangaListModel(
- id = id,
- title = title,
- subtitle = tags.joinToString(", ") { it.title },
- coverUrl = coverUrl,
- manga = this,
- counter = extraProvider?.getCounter(id) ?: 0,
- progress = extraProvider?.getProgress(id) ?: PROGRESS_NONE,
-)
-
-suspend fun Manga.toListDetailedModel(
- extraProvider: ListExtraProvider?,
-) = MangaListDetailedModel(
- id = id,
- title = title,
- subtitle = altTitle,
- coverUrl = coverUrl,
- manga = this,
- counter = extraProvider?.getCounter(id) ?: 0,
- progress = extraProvider?.getProgress(id) ?: PROGRESS_NONE,
- tags = tags.map {
- ChipsView.ChipModel(
- tint = extraProvider?.getTagTint(it) ?: 0,
- title = it.title,
- data = it,
- )
- },
-)
-
-suspend fun Manga.toGridModel(
- extraProvider: ListExtraProvider?,
-) = MangaGridModel(
- id = id,
- title = title,
- coverUrl = coverUrl,
- manga = this,
- counter = extraProvider?.getCounter(id) ?: 0,
- progress = extraProvider?.getProgress(id) ?: PROGRESS_NONE,
-)
-
-suspend fun List.toUi(
- mode: ListMode,
- extraProvider: ListExtraProvider,
-): List = if (isEmpty()) {
- emptyList()
-} else {
- toUi(ArrayList(size), mode, extraProvider)
-}
-
-suspend fun > List.toUi(
- destination: C,
- mode: ListMode,
- extraProvider: ListExtraProvider,
-): C = when (mode) {
- ListMode.LIST -> mapTo(destination) { it.toListModel(extraProvider) }
- ListMode.DETAILED_LIST -> mapTo(destination) { it.toListDetailedModel(extraProvider) }
- ListMode.GRID -> mapTo(destination) { it.toGridModel(extraProvider) }
-}
fun Throwable.toErrorState(canRetry: Boolean = true, @StringRes secondaryAction: Int = 0) = ErrorState(
exception = this,
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaCompactListModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaCompactListModel.kt
new file mode 100644
index 000000000..fae6ccfb1
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaCompactListModel.kt
@@ -0,0 +1,15 @@
+package org.koitharu.kotatsu.list.ui.model
+
+import org.koitharu.kotatsu.list.domain.ReadingProgress
+import org.koitharu.kotatsu.parsers.model.Manga
+
+data class MangaCompactListModel(
+ override val id: Long,
+ override val title: String,
+ val subtitle: String,
+ override val coverUrl: String,
+ override val manga: Manga,
+ override val counter: Int,
+ override val progress: ReadingProgress?,
+ override val isFavorite: Boolean,
+) : MangaListModel()
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaListDetailedModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaDetailedListModel.kt
similarity index 64%
rename from app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaListDetailedModel.kt
rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaDetailedListModel.kt
index e64c47ada..8b6633788 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaListDetailedModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaDetailedListModel.kt
@@ -1,15 +1,17 @@
package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
+import org.koitharu.kotatsu.list.domain.ReadingProgress
import org.koitharu.kotatsu.parsers.model.Manga
-data class MangaListDetailedModel(
+data class MangaDetailedListModel(
override val id: Long,
override val title: String,
val subtitle: String?,
override val coverUrl: String,
override val manga: Manga,
override val counter: Int,
- override val progress: Float,
+ override val progress: ReadingProgress?,
+ override val isFavorite: Boolean,
val tags: List,
-) : MangaItemModel()
+) : MangaListModel()
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaGridModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaGridModel.kt
index 2b94795c6..244c3f7b7 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaGridModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaGridModel.kt
@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.list.ui.model
+import org.koitharu.kotatsu.list.domain.ReadingProgress
import org.koitharu.kotatsu.parsers.model.Manga
data class MangaGridModel(
@@ -8,5 +9,6 @@ data class MangaGridModel(
override val coverUrl: String,
override val manga: Manga,
override val counter: Int,
- override val progress: Float,
-) : MangaItemModel()
+ override val progress: ReadingProgress?,
+ override val isFavorite: Boolean,
+) : MangaListModel()
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaItemModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaItemModel.kt
deleted file mode 100644
index 95ea83b60..000000000
--- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaItemModel.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package org.koitharu.kotatsu.list.ui.model
-
-import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
-import org.koitharu.kotatsu.parsers.model.Manga
-import org.koitharu.kotatsu.parsers.model.MangaSource
-
-sealed class MangaItemModel : ListModel {
-
- abstract val id: Long
- abstract val manga: Manga
- abstract val title: String
- abstract val coverUrl: String
- abstract val counter: Int
- abstract val progress: Float
-
- val source: MangaSource
- get() = manga.source
-
- override fun areItemsTheSame(other: ListModel): Boolean {
- return other is MangaItemModel && other.javaClass == javaClass && id == other.id
- }
-
- override fun getChangePayload(previousState: ListModel): Any? {
- return when {
- previousState !is MangaItemModel -> super.getChangePayload(previousState)
- progress != previousState.progress -> ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED
- counter != previousState.counter -> ListModelDiffCallback.PAYLOAD_ANYTHING_CHANGED
- else -> null
- }
- }
-}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaListModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaListModel.kt
index 54b0a800a..322bf0b6a 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaListModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaListModel.kt
@@ -1,13 +1,34 @@
package org.koitharu.kotatsu.list.ui.model
+import org.koitharu.kotatsu.list.domain.ReadingProgress
+import org.koitharu.kotatsu.list.ui.ListModelDiffCallback.Companion.PAYLOAD_ANYTHING_CHANGED
+import org.koitharu.kotatsu.list.ui.ListModelDiffCallback.Companion.PAYLOAD_PROGRESS_CHANGED
import org.koitharu.kotatsu.parsers.model.Manga
+import org.koitharu.kotatsu.parsers.model.MangaSource
-data class MangaListModel(
- override val id: Long,
- override val title: String,
- val subtitle: String,
- override val coverUrl: String,
- override val manga: Manga,
- override val counter: Int,
- override val progress: Float,
-) : MangaItemModel()
+sealed class MangaListModel : ListModel {
+
+ abstract val id: Long
+ abstract val manga: Manga
+ abstract val title: String
+ abstract val coverUrl: String
+ abstract val counter: Int
+ abstract val isFavorite: Boolean
+ abstract val progress: ReadingProgress?
+
+ val source: MangaSource
+ get() = manga.source
+
+ override fun areItemsTheSame(other: ListModel): Boolean {
+ return other is MangaListModel && other.javaClass == javaClass && id == other.id
+ }
+
+ override fun getChangePayload(previousState: ListModel): Any? = when {
+ previousState !is MangaListModel || previousState.manga != manga -> null
+
+ previousState.progress != progress -> PAYLOAD_PROGRESS_CHANGED
+ previousState.isFavorite != isFavorite || previousState.counter != counter -> PAYLOAD_ANYTHING_CHANGED
+
+ else -> null
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewViewModel.kt
index 360190b46..45398c9da 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewViewModel.kt
@@ -25,18 +25,17 @@ import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.ui.BaseViewModel
-import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.core.util.ext.sanitize
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
-import org.koitharu.kotatsu.list.domain.ListExtraProvider
+import org.koitharu.kotatsu.list.domain.MangaListMapper
import javax.inject.Inject
@HiltViewModel
class PreviewViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
- private val extraProvider: ListExtraProvider,
+ private val mangaListMapper: MangaListMapper,
private val repositoryFactory: MangaRepository.Factory,
private val historyRepository: HistoryRepository,
private val imageGetter: Html.ImageGetter,
@@ -81,13 +80,7 @@ class PreviewViewModel @Inject constructor(
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), null)
val tagsChips = manga.map {
- it.tags.map { tag ->
- ChipsView.ChipModel(
- title = tag.title,
- tint = extraProvider.getTagTint(tag),
- data = tag,
- )
- }
+ mangaListMapper.mapTags(it.tags)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
init {
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt
index b0eba4608..bafffd7fa 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt
@@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible
+import org.koitharu.kotatsu.core.model.LocalMangaSource
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
@@ -29,7 +30,6 @@ 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
@@ -49,7 +49,7 @@ class LocalMangaRepository @Inject constructor(
private val settings: AppSettings,
) : MangaRepository {
- override val source = MangaSource.LOCAL
+ override val source = LocalMangaSource
private val locks = MultiMutex()
private val localMappingCache = LocalMangaMappingCache()
@@ -100,7 +100,7 @@ class LocalMangaRepository @Inject constructor(
}
override suspend fun getDetails(manga: Manga): Manga = when {
- manga.source != MangaSource.LOCAL -> requireNotNull(findSavedManga(manga)?.manga) {
+ !manga.isLocal -> requireNotNull(findSavedManga(manga)?.manga) {
"Manga is not local or saved"
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/MangaIndex.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/MangaIndex.kt
index c859ffe66..394746604 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/MangaIndex.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/MangaIndex.kt
@@ -4,6 +4,7 @@ import androidx.annotation.WorkerThread
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.BuildConfig
+import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
@@ -58,7 +59,7 @@ class MangaIndex(source: String?) {
}
fun getMangaInfo(): Manga? = if (json.length() == 0) null else runCatching {
- val source = MangaSource.valueOf(json.getString("source"))
+ val source = MangaSource(json.getString("source"))
Manga(
id = json.getLong("id"),
title = json.getString("title"),
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt
index 3781a7c4e..3667f1e31 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt
@@ -4,6 +4,7 @@ import androidx.core.net.toFile
import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
+import org.koitharu.kotatsu.core.model.LocalMangaSource
import org.koitharu.kotatsu.core.util.AlphanumComparator
import org.koitharu.kotatsu.core.util.ext.children
import org.koitharu.kotatsu.core.util.ext.creationTime
@@ -18,7 +19,6 @@ import org.koitharu.kotatsu.local.domain.model.LocalManga
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.model.MangaSource
import org.koitharu.kotatsu.parsers.util.toCamelCase
import java.io.File
import java.util.TreeMap
@@ -47,7 +47,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
index?.getCoverEntry() ?: findFirstImageEntry().orEmpty(),
)
val manga = info?.copy2(
- source = MangaSource.LOCAL,
+ source = LocalMangaSource,
url = mangaUri,
coverUrl = cover,
largeCoverUrl = cover,
@@ -59,14 +59,14 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
// old downloads
chapterFiles.values.elementAtOrNull(i)
} ?: return@mapIndexedNotNull null
- c.copy(url = file.toUri().toString(), source = MangaSource.LOCAL)
+ c.copy(url = file.toUri().toString(), source = LocalMangaSource)
},
) ?: Manga(
id = root.absolutePath.longHashCode(),
title = root.name.toHumanReadable(),
url = mangaUri,
publicUrl = mangaUri,
- source = MangaSource.LOCAL,
+ source = LocalMangaSource,
coverUrl = findFirstImageEntry().orEmpty(),
chapters = chapterFiles.values.mapIndexed { i, f ->
MangaChapter(
@@ -74,7 +74,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
name = f.nameWithoutExtension.toHumanReadable(),
number = 0f,
volume = 0,
- source = MangaSource.LOCAL,
+ source = LocalMangaSource,
uploadDate = f.creationTime,
url = f.toUri().toString(),
scanlator = null,
@@ -106,7 +106,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
.toListSorted(compareBy(AlphanumComparator()) { x -> x.name })
.map {
val pageUri = it.toUri().toString()
- MangaPage(pageUri.longHashCode(), pageUri, null, MangaSource.LOCAL)
+ MangaPage(pageUri.longHashCode(), pageUri, null, LocalMangaSource)
}
} else {
ZipFile(file).use { zip ->
@@ -121,7 +121,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
id = pageUri.longHashCode(),
url = pageUri,
preview = null,
- source = MangaSource.LOCAL,
+ source = LocalMangaSource,
)
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaZipInput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaZipInput.kt
index d2aa8f71d..36f839979 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaZipInput.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaZipInput.kt
@@ -7,6 +7,7 @@ import androidx.core.net.toFile
import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
+import org.koitharu.kotatsu.core.model.LocalMangaSource
import org.koitharu.kotatsu.core.util.AlphanumComparator
import org.koitharu.kotatsu.core.util.ext.longHashCode
import org.koitharu.kotatsu.core.util.ext.readText
@@ -17,7 +18,6 @@ import org.koitharu.kotatsu.local.domain.model.LocalManga
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.model.MangaSource
import org.koitharu.kotatsu.parsers.util.toCamelCase
import java.io.File
import java.util.Enumeration
@@ -47,12 +47,12 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
entryName = index.getCoverEntry() ?: findFirstImageEntry(zip.entries())?.name.orEmpty(),
)
return@use info.copy2(
- source = MangaSource.LOCAL,
+ source = LocalMangaSource,
url = fileUri,
coverUrl = cover,
largeCoverUrl = cover,
chapters = info.chapters?.map { c ->
- c.copy(url = fileUri, source = MangaSource.LOCAL)
+ c.copy(url = fileUri, source = LocalMangaSource)
},
)
}
@@ -70,7 +70,7 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
title = title,
url = fileUri,
publicUrl = fileUri,
- source = MangaSource.LOCAL,
+ source = LocalMangaSource,
coverUrl = zipUri(root, findFirstImageEntry(zip.entries())?.name.orEmpty()),
chapters = chapters.sortedWith(AlphanumComparator())
.mapIndexed { i, s ->
@@ -79,7 +79,7 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
name = s.ifEmpty { title },
number = 0f,
volume = 0,
- source = MangaSource.LOCAL,
+ source = LocalMangaSource,
uploadDate = 0L,
url = uriBuilder.fragment(s).build().toString(),
scanlator = null,
@@ -135,7 +135,7 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
id = entryUri.longHashCode(),
url = entryUri,
preview = null,
- source = MangaSource.LOCAL,
+ source = LocalMangaSource,
)
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaUtil.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaUtil.kt
index d2fb72122..08df5832a 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaUtil.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaUtil.kt
@@ -4,17 +4,15 @@ import androidx.core.net.toFile
import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
+import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.parsers.model.Manga
-import org.koitharu.kotatsu.parsers.model.MangaSource
class LocalMangaUtil(
private val manga: Manga,
) {
init {
- require(manga.source == MangaSource.LOCAL) {
- "Expected LOCAL source but ${manga.source} found"
- }
+ require(manga.isLocal) { "Expected LOCAL source but ${manga.source} found" }
}
suspend fun deleteChapters(ids: Set) {
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt
index f82d06de8..069240354 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt
@@ -14,6 +14,7 @@ import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.LocalMangaSource
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.widgets.TipView
import org.koitharu.kotatsu.core.util.ShareHelper
@@ -26,7 +27,6 @@ import org.koitharu.kotatsu.filter.ui.FilterOwner
import org.koitharu.kotatsu.filter.ui.MangaFilter
import org.koitharu.kotatsu.filter.ui.sheet.FilterSheetFragment
import org.koitharu.kotatsu.list.ui.MangaListFragment
-import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
import org.koitharu.kotatsu.settings.storage.RequestStorageManagerPermissionContract
import org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity
@@ -47,9 +47,9 @@ class LocalListFragment : MangaListFragment(), FilterOwner {
init {
withArgs(1) {
- putSerializable(
+ putString(
RemoteListFragment.ARG_SOURCE,
- MangaSource.LOCAL,
+ LocalMangaSource.name,
) // required by FilterCoordinator
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt
index 4eed4c626..7e566c679 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt
@@ -16,10 +16,10 @@ import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.explore.domain.ExploreRepository
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
-import org.koitharu.kotatsu.list.domain.ListExtraProvider
+import org.koitharu.kotatsu.list.domain.MangaListMapper
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
-import org.koitharu.kotatsu.list.ui.model.MangaItemModel
+import org.koitharu.kotatsu.list.ui.model.MangaListModel
import org.koitharu.kotatsu.list.ui.model.TipModel
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.data.LocalStorageManager
@@ -35,7 +35,7 @@ class LocalListViewModel @Inject constructor(
filter: FilterCoordinator,
private val settings: AppSettings,
downloadScheduler: DownloadWorker.Scheduler,
- listExtraProvider: ListExtraProvider,
+ mangaListMapper: MangaListMapper,
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
exploreRepository: ExploreRepository,
@LocalStorageChanges private val localStorageChanges: SharedFlow,
@@ -46,7 +46,7 @@ class LocalListViewModel @Inject constructor(
mangaRepositoryFactory,
filter,
settings,
- listExtraProvider,
+ mangaListMapper,
downloadScheduler,
exploreRepository,
sourcesRepository,
@@ -70,7 +70,7 @@ class LocalListViewModel @Inject constructor(
return
}
for (item in list) {
- if (item !is MangaItemModel) {
+ if (item !is MangaListModel) {
continue
}
val file = item.manga.url.toUriOrNull()?.toFileOrNull() ?: continue
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/CoverRestoreInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/CoverRestoreInterceptor.kt
index 1f09d1920..fcf77379c 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/CoverRestoreInterceptor.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/CoverRestoreInterceptor.kt
@@ -12,7 +12,7 @@ import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.model.findById
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
-import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
+import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.exception.ParseException
@@ -73,7 +73,7 @@ class CoverRestoreInterceptor @Inject constructor(
if (dataRepository.findMangaById(manga.id) == null) {
return false
}
- val repo = repositoryFactory.create(manga.source) as? RemoteMangaRepository ?: return false
+ val repo = repositoryFactory.create(manga.source) as? ParserMangaRepository ?: return false
val fixed = repo.find(manga) ?: return false
return if (fixed != manga) {
dataRepository.storeManga(fixed)
@@ -100,7 +100,7 @@ class CoverRestoreInterceptor @Inject constructor(
}
private suspend fun restoreBookmarkImpl(bookmark: Bookmark): Boolean {
- val repo = repositoryFactory.create(bookmark.manga.source) as? RemoteMangaRepository ?: return false
+ val repo = repositoryFactory.create(bookmark.manga.source) as? ParserMangaRepository ?: return false
val chapter = repo.getDetails(bookmark.manga).chapters?.findById(bookmark.chapterId) ?: return false
val page = repo.getPages(chapter)[bookmark.page]
val imageUrl = page.preview.ifNullOrEmpty { page.url }
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/ReadingResumeEnabledUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/ReadingResumeEnabledUseCase.kt
index 26e9d26c3..8828edcfd 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/ReadingResumeEnabledUseCase.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/ReadingResumeEnabledUseCase.kt
@@ -4,11 +4,11 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
+import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.history.data.HistoryRepository
-import org.koitharu.kotatsu.parsers.model.MangaSource
import javax.inject.Inject
class ReadingResumeEnabledUseCase @Inject constructor(
@@ -24,7 +24,7 @@ class ReadingResumeEnabledUseCase @Inject constructor(
flowOf(false)
} else {
combine(networkState, historyRepository.observeLast()) { isOnline, last ->
- last != null && (isOnline || last.source == MangaSource.LOCAL)
+ last != null && (isOnline || last.isLocal)
}
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/welcome/WelcomeViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/welcome/WelcomeViewModel.kt
index e97d7950a..350fd8c67 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/welcome/WelcomeViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/welcome/WelcomeViewModel.kt
@@ -15,7 +15,7 @@ import org.koitharu.kotatsu.core.util.ext.toLocale
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
import org.koitharu.kotatsu.parsers.model.ContentType
-import org.koitharu.kotatsu.parsers.model.MangaSource
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.util.mapToSet
import java.util.EnumSet
import java.util.Locale
@@ -103,7 +103,7 @@ class WelcomeViewModel @Inject constructor(
private suspend fun commit() {
val languages = locales.value.selectedItems.mapToSet { it.language }
val types = types.value.selectedItems
- val enabledSources = allSources.filterTo(EnumSet.noneOf(MangaSource::class.java)) { x ->
+ val enabledSources = allSources.filterTo(EnumSet.noneOf(MangaParserSource::class.java)) { x ->
x.contentType in types && x.locale in languages
}
repository.setSourcesEnabledExclusive(enabledSources)
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt
index 68d4adb55..216804742 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt
@@ -32,7 +32,7 @@ import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
import org.koitharu.kotatsu.core.parser.MangaRepository
-import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
+import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
@@ -91,7 +91,7 @@ class PageLoader @Inject constructor(
private val edgeDetector = EdgeDetector(context)
fun isPrefetchApplicable(): Boolean {
- return repository is RemoteMangaRepository
+ return repository is ParserMangaRepository
&& settings.isPagesPreloadEnabled
&& !context.isPowerSaveMode()
&& !isLowRam()
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ImageServerDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ImageServerDelegate.kt
index 8090ead79..b4460ab8c 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ImageServerDelegate.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ImageServerDelegate.kt
@@ -7,7 +7,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaRepository
-import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
+import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.util.ext.mapToArray
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -20,7 +20,7 @@ class ImageServerDelegate(
) {
private val repositoryLazy = SuspendLazy {
- mangaRepositoryFactory.create(checkNotNull(mangaSource)) as RemoteMangaRepository
+ mangaRepositoryFactory.create(checkNotNull(mangaSource)) as ParserMangaRepository
}
suspend fun isAvailable() = withContext(Dispatchers.Default) {
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/ReaderPage.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/ReaderPage.kt
index a9c97cf96..f9b4a45dd 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/ReaderPage.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/ReaderPage.kt
@@ -2,10 +2,13 @@ package org.koitharu.kotatsu.reader.ui.pager
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
+import kotlinx.parcelize.TypeParceler
+import org.koitharu.kotatsu.core.model.parcelable.MangaSourceParceler
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
@Parcelize
+@TypeParceler
data class ReaderPage(
val id: Long,
val url: String,
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt
index ce7673c1e..28795aac5 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt
@@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.drop
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.BrowserActivity
+import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
@@ -75,7 +76,12 @@ class RemoteListFragment : MangaListFragment(), FilterOwner {
override fun onSecondaryErrorActionClick(error: Throwable) {
viewModel.browserUrl?.also { url ->
startActivity(
- BrowserActivity.newIntent(requireContext(), url, viewModel.source, viewModel.source.title),
+ BrowserActivity.newIntent(
+ requireContext(),
+ url,
+ viewModel.source,
+ viewModel.source.getTitle(requireContext()),
+ ),
)
} ?: Snackbar.make(requireViewBinding().recyclerView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT)
.show()
@@ -165,8 +171,8 @@ class RemoteListFragment : MangaListFragment(), FilterOwner {
const val ARG_SOURCE = "provider"
- fun newInstance(provider: MangaSource) = RemoteListFragment().withArgs(1) {
- putSerializable(ARG_SOURCE, provider)
+ fun newInstance(source: MangaSource) = RemoteListFragment().withArgs(1) {
+ putString(ARG_SOURCE, source.name)
}
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt
index 05653b7fc..6c96b8ff7 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt
@@ -18,21 +18,21 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.distinctById
import org.koitharu.kotatsu.core.parser.MangaRepository
-import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
+import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
-import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.core.util.ext.sizeOrZero
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.explore.domain.ExploreRepository
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
import org.koitharu.kotatsu.filter.ui.MangaFilter
-import org.koitharu.kotatsu.list.domain.ListExtraProvider
+import org.koitharu.kotatsu.list.domain.MangaListMapper
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -40,11 +40,9 @@ import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorFooter
import org.koitharu.kotatsu.list.ui.model.toErrorState
-import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
-import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import javax.inject.Inject
@@ -56,13 +54,13 @@ open class RemoteListViewModel @Inject constructor(
mangaRepositoryFactory: MangaRepository.Factory,
private val filter: FilterCoordinator,
settings: AppSettings,
- listExtraProvider: ListExtraProvider,
+ mangaListMapper: MangaListMapper,
downloadScheduler: DownloadWorker.Scheduler,
private val exploreRepository: ExploreRepository,
sourcesRepository: MangaSourcesRepository,
) : MangaListViewModel(settings, downloadScheduler), MangaFilter by filter {
- val source = savedStateHandle.require(RemoteListFragment.ARG_SOURCE)
+ val source = MangaSource(savedStateHandle[RemoteListFragment.ARG_SOURCE])
val isRandomLoading = MutableStateFlow(false)
val onOpenManga = MutableEventFlow()
@@ -77,11 +75,11 @@ open class RemoteListViewModel @Inject constructor(
get() = repository.isSearchSupported
val browserUrl: String?
- get() = (repository as? RemoteMangaRepository)?.domain?.let { "https://$it" }
+ get() = (repository as? ParserMangaRepository)?.domain?.let { "https://$it" }
override val content = combine(
mangaList.map { it?.skipNsfwIfNeeded() },
- listMode,
+ observeListModeWithTriggers(),
listError,
hasNextPage,
) { list, mode, error, hasNext ->
@@ -97,7 +95,7 @@ open class RemoteListViewModel @Inject constructor(
list == null -> add(LoadingState)
list.isEmpty() -> add(createEmptyState(canResetFilter = header.value.isFilterApplied))
else -> {
- list.toUi(this, mode, listExtraProvider)
+ mangaListMapper.toListModelList(this, list, mode)
when {
error != null -> add(error.toErrorFooter())
hasNext -> add(LoadingFooter())
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblerMangaSelectionDecoration.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblerMangaSelectionDecoration.kt
index 46715d84c..c5ed4ebb7 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblerMangaSelectionDecoration.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblerMangaSelectionDecoration.kt
@@ -14,7 +14,11 @@ import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
class ScrobblerMangaSelectionDecoration(context: Context) : MangaSelectionDecoration(context) {
var checkedItemId: Long
- get() = selection.singleOrNull() ?: NO_ID
+ get() = if (selection.size == 1) {
+ selection.first()
+ } else {
+ NO_ID
+ }
set(value) {
clearSelection()
if (value != NO_ID) {
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaListActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaListActivity.kt
index 75f472d85..3819bb9ce 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaListActivity.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaListActivity.kt
@@ -21,7 +21,9 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.LocalMangaSource
import org.koitharu.kotatsu.core.model.MangaSource
+import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags
@@ -77,7 +79,7 @@ class MangaListActivity :
finishAfterTransition()
} else {
viewBinding.buttonOrder?.setOnClickListener(this)
- title = if (src == MangaSource.LOCAL) getString(R.string.local_storage) else src.title
+ title = src.getTitle(this)
initList(src, tags)
}
}
@@ -125,7 +127,7 @@ class MangaListActivity :
} else {
fm.commit {
setReorderingAllowed(true)
- val fragment = if (source == MangaSource.LOCAL) {
+ val fragment = if (source == LocalMangaSource) {
LocalListFragment()
} else {
RemoteListFragment.newInstance(source)
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchActivity.kt
index 02860a3e8..e88057a82 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchActivity.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchActivity.kt
@@ -11,8 +11,9 @@ import androidx.core.view.updatePadding
import androidx.fragment.app.commit
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.MangaSource
+import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.ui.BaseActivity
-import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.showKeyboard
import org.koitharu.kotatsu.databinding.ActivitySearchBinding
@@ -28,15 +29,12 @@ class SearchActivity : BaseActivity(), SearchView.OnQuery
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivitySearchBinding.inflate(layoutInflater))
- source = intent.getSerializableExtraCompat(EXTRA_SOURCE) ?: run {
- finishAfterTransition()
- return
- }
+ source = MangaSource(intent.getStringExtra(EXTRA_SOURCE))
val query = intent.getStringExtra(EXTRA_QUERY)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
searchSuggestionViewModel.isIncognitoModeEnabled.observe(this, this::onIncognitoModeChanged)
with(viewBinding.searchView) {
- queryHint = getString(R.string.search_on_s, source.title)
+ queryHint = getString(R.string.search_on_s, source.getTitle(context))
setOnQueryTextListener(this@SearchActivity)
if (query.isNullOrBlank()) {
@@ -52,7 +50,7 @@ class SearchActivity : BaseActivity(), SearchView.OnQuery
viewBinding.toolbar.updatePadding(
left = insets.left,
right = insets.right,
- top = insets.top
+ top = insets.top,
)
viewBinding.container.updatePadding(
bottom = insets.bottom,
@@ -93,7 +91,7 @@ class SearchActivity : BaseActivity(), SearchView.OnQuery
fun newIntent(context: Context, source: MangaSource, query: String?) =
Intent(context, SearchActivity::class.java)
- .putExtra(EXTRA_SOURCE, source)
+ .putExtra(EXTRA_SOURCE, source.name)
.putExtra(EXTRA_QUERY, query)
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchFragment.kt
index 6e8b9683a..7662f9eb9 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchFragment.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchFragment.kt
@@ -30,7 +30,7 @@ class SearchFragment : MangaListFragment() {
const val ARG_SOURCE = "source"
fun newInstance(source: MangaSource, query: String) = SearchFragment().withArgs(2) {
- putSerializable(ARG_SOURCE, source)
+ putString(ARG_SOURCE, source.name)
putString(ARG_QUERY, query)
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchViewModel.kt
index 57eb09333..0bd53ef68 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchViewModel.kt
@@ -13,13 +13,14 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.distinctById
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.core.util.ext.sizeOrZero
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
-import org.koitharu.kotatsu.list.domain.ListExtraProvider
+import org.koitharu.kotatsu.list.domain.MangaListMapper
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -27,7 +28,6 @@ import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorFooter
import org.koitharu.kotatsu.list.ui.model.toErrorState
-import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import javax.inject.Inject
@@ -37,12 +37,12 @@ class SearchViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
repositoryFactory: MangaRepository.Factory,
settings: AppSettings,
- private val extraProvider: ListExtraProvider,
+ private val mangaListMapper: MangaListMapper,
downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler) {
private val query = savedStateHandle.require(SearchFragment.ARG_QUERY)
- private val repository = repositoryFactory.create(savedStateHandle.require(SearchFragment.ARG_SOURCE))
+ private val repository = repositoryFactory.create(MangaSource(savedStateHandle.get(SearchFragment.ARG_SOURCE)))
private val mangaList = MutableStateFlow?>(null)
private val hasNextPage = MutableStateFlow(false)
private val listError = MutableStateFlow(null)
@@ -50,7 +50,7 @@ class SearchViewModel @Inject constructor(
override val content = combine(
mangaList.map { it?.skipNsfwIfNeeded() },
- listMode,
+ observeListModeWithTriggers(),
listError,
hasNextPage,
) { list, mode, error, hasNext ->
@@ -68,7 +68,7 @@ class SearchViewModel @Inject constructor(
else -> {
val result = ArrayList(list.size + 1)
- list.toUi(result, mode, extraProvider)
+ mangaListMapper.toListModelList(result, list, mode)
when {
error != null -> result += error.toErrorFooter()
hasNext -> result += LoadingFooter()
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchListModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchListModel.kt
index 7d6f802fc..317652294 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchListModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchListModel.kt
@@ -2,13 +2,13 @@ package org.koitharu.kotatsu.search.ui.multi
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
-import org.koitharu.kotatsu.list.ui.model.MangaItemModel
+import org.koitharu.kotatsu.list.ui.model.MangaListModel
import org.koitharu.kotatsu.parsers.model.MangaSource
data class MultiSearchListModel(
val source: MangaSource,
val hasMore: Boolean,
- val list: List,
+ val list: List,
val error: Throwable?,
) : ListModel {
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt
index 27c2e5001..686271449 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt
@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.search.ui.multi
import androidx.annotation.CheckResult
+import androidx.collection.LongSet
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -13,8 +14,10 @@ import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.onEmpty
import kotlinx.coroutines.flow.runningFold
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.sync.Semaphore
@@ -28,12 +31,11 @@ import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
-import org.koitharu.kotatsu.list.domain.ListExtraProvider
+import org.koitharu.kotatsu.list.domain.MangaListMapper
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState
-import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
@@ -45,7 +47,7 @@ private const val MIN_HAS_MORE_ITEMS = 8
@HiltViewModel
class MultiSearchViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
- private val extraProvider: ListExtraProvider,
+ private val mangaListMapper: MangaListMapper,
private val mangaRepositoryFactory: MangaRepository.Factory,
private val downloadScheduler: DownloadWorker.Scheduler,
private val sourcesRepository: MangaSourcesRepository,
@@ -81,7 +83,7 @@ class MultiSearchViewModel @Inject constructor(
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
- fun getItems(ids: Set): Set {
+ fun getItems(ids: LongSet): Set {
val snapshot = listData.value ?: return emptySet()
val result = HashSet(ids.size)
snapshot.forEach { x ->
@@ -112,35 +114,39 @@ class MultiSearchViewModel @Inject constructor(
return@channelFlow
}
val semaphore = Semaphore(MAX_PARALLELISM)
- for (source in sources) {
+ sources.mapNotNull { source ->
val repository = mangaRepositoryFactory.create(source)
if (!repository.isSearchSupported) {
- continue
- }
- launch {
- val item = runCatchingCancellable {
- semaphore.withPermit {
- repository.getList(offset = 0, filter = MangaListFilter.Search(q))
- .toUi(ListMode.GRID, extraProvider)
- }
- }.fold(
- onSuccess = { list ->
- if (list.isEmpty()) {
- null
- } else {
- MultiSearchListModel(source, list.size > MIN_HAS_MORE_ITEMS, list, null)
+ null
+ } else {
+ launch {
+ val item = runCatchingCancellable {
+ semaphore.withPermit {
+ mangaListMapper.toListModelList(
+ manga = repository.getList(offset = 0, filter = MangaListFilter.Search(q)),
+ mode = ListMode.GRID,
+ )
}
- },
- onFailure = { error ->
- error.printStackTraceDebug()
- MultiSearchListModel(source, true, emptyList(), error)
- },
- )
- if (item != null) {
- send(item)
+ }.fold(
+ onSuccess = { list ->
+ if (list.isEmpty()) {
+ null
+ } else {
+ MultiSearchListModel(source, list.size > MIN_HAS_MORE_ITEMS, list, null)
+ }
+ },
+ onFailure = { error ->
+ error.printStackTraceDebug()
+ MultiSearchListModel(source, true, emptyList(), error)
+ },
+ )
+ if (item != null) {
+ send(item)
+ }
}
}
- }
+ }.joinAll()
}.runningFold?>(null) { list, item -> list.orEmpty() + item }
.filterNotNull()
+ .onEmpty { emit(emptyList()) }
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt
index 6f945677f..9363051e8 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt
@@ -8,6 +8,7 @@ import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
@@ -45,7 +46,7 @@ fun searchResultsAD(
binding.buttonMore.setOnClickListener(eventListener)
bind {
- binding.textViewTitle.text = item.source.title
+ binding.textViewTitle.text = item.source.getTitle(context)
binding.buttonMore.isVisible = item.hasMore
adapter.items = item.list
adapter.notifyDataSetChanged()
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt
index 36f952dc1..c604a2c11 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt
@@ -22,7 +22,6 @@ import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.sizeOrZero
-import org.koitharu.kotatsu.core.util.ext.toEnumSet
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
@@ -104,7 +103,7 @@ class SearchSuggestionViewModel @Inject constructor(
suggestionJob?.cancel()
suggestionJob = combine(
query.debounce(DEBOUNCE_TIMEOUT),
- sourcesRepository.observeEnabledSources().map { it.toEnumSet() },
+ sourcesRepository.observeEnabledSources().map { it.toSet() },
settings.observeAsFlow(AppSettings.KEY_SEARCH_SUGGESTION_TYPES) { searchSuggestionTypes },
::Triple,
).mapLatest { (searchQuery, enabledSources, types) ->
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionSourceAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionSourceAD.kt
index 9029d7ef4..47415984d 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionSourceAD.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionSourceAD.kt
@@ -7,6 +7,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
+import org.koitharu.kotatsu.core.ui.image.AnimatedFaviconDrawable
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
@@ -37,7 +38,7 @@ fun searchSuggestionSourceAD(
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewCover.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
fallback(fallbackIcon)
- placeholder(fallbackIcon)
+ placeholder(AnimatedFaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name))
error(fallbackIcon)
source(item.source)
enqueueWith(coil)
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionSourceTipAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionSourceTipAD.kt
index 55a71a06d..7dad1c854 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionSourceTipAD.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionSourceTipAD.kt
@@ -7,6 +7,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
+import org.koitharu.kotatsu.core.ui.image.AnimatedFaviconDrawable
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
@@ -34,7 +35,7 @@ fun searchSuggestionSourceTipAD(
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewCover.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
fallback(fallbackIcon)
- placeholder(fallbackIcon)
+ placeholder(AnimatedFaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name))
error(fallbackIcon)
source(item.source)
enqueueWith(coil)
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt
index f1c997956..27f877460 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt
@@ -4,7 +4,6 @@ import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
-import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -52,7 +51,7 @@ sealed interface SearchSuggestionItem : ListModel {
) : SearchSuggestionItem {
val isNsfw: Boolean
- get() = source.contentType == ContentType.HENTAI
+ get() = source.isNsfw()
override fun areItemsTheSame(other: ListModel): Boolean {
return other is Source && other.source == source
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt
index e0c96fb31..f00a95fa4 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt
@@ -14,6 +14,7 @@ import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
+import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
import org.koitharu.kotatsu.core.util.LocaleComparator
@@ -44,6 +45,10 @@ class AppearanceSettingsFragment :
entryValues = ListMode.entries.names()
setDefaultValueCompat(ListMode.GRID.name)
}
+ findPreference(AppSettings.KEY_PROGRESS_INDICATORS)?.run {
+ entryValues = ProgressIndicatorMode.entries.names()
+ setDefaultValueCompat(ProgressIndicatorMode.PERCENT_READ.name)
+ }
findPreference(AppSettings.KEY_APP_LOCALE)?.run {
initLocalePicker(this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt
index f9ae0f40d..7fc0bfd28 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt
@@ -2,7 +2,9 @@ package org.koitharu.kotatsu.settings
import android.content.Context
import android.content.Intent
+import android.net.Uri
import android.os.Bundle
+import android.provider.Settings
import android.view.ViewGroup.MarginLayoutParams
import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams
@@ -16,8 +18,10 @@ import com.google.android.material.appbar.AppBarLayout
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.MangaSource
+import org.koitharu.kotatsu.core.model.MangaSourceInfo
+import org.koitharu.kotatsu.core.parser.external.ExternalMangaSource
import org.koitharu.kotatsu.core.ui.BaseActivity
-import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ActivitySettingsBinding
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
@@ -110,7 +114,7 @@ class SettingsActivity :
ACTION_SOURCES -> SourcesSettingsFragment()
ACTION_MANAGE_DOWNLOADS -> DownloadsSettingsFragment()
ACTION_SOURCE -> SourceSettingsFragment.newInstance(
- intent.getSerializableExtraCompat(EXTRA_SOURCE) ?: MangaSource.LOCAL,
+ MangaSource(intent.getStringExtra(EXTRA_SOURCE)),
)
ACTION_MANAGE_SOURCES -> SourcesManageFragment()
@@ -174,9 +178,14 @@ class SettingsActivity :
Intent(context, SettingsActivity::class.java)
.setAction(ACTION_MANAGE_DOWNLOADS)
- fun newSourceSettingsIntent(context: Context, source: MangaSource) =
- Intent(context, SettingsActivity::class.java)
+ fun newSourceSettingsIntent(context: Context, source: MangaSource): Intent = when (source) {
+ is MangaSourceInfo -> newSourceSettingsIntent(context, source.mangaSource)
+ is ExternalMangaSource -> Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ .setData(Uri.fromParts("package", source.packageName, null))
+
+ else -> Intent(context, SettingsActivity::class.java)
.setAction(ACTION_SOURCE)
- .putExtra(EXTRA_SOURCE, source)
+ .putExtra(EXTRA_SOURCE, source.name)
+ }
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsExt.kt
index ef58ffc15..dbd2f23bd 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsExt.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsExt.kt
@@ -7,7 +7,9 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import org.koitharu.kotatsu.R
-import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
+import org.koitharu.kotatsu.core.parser.EmptyMangaRepository
+import org.koitharu.kotatsu.core.parser.MangaRepository
+import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.util.ext.mapToArray
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.network.UserAgents
@@ -17,7 +19,14 @@ import org.koitharu.kotatsu.settings.utils.EditTextDefaultSummaryProvider
import org.koitharu.kotatsu.settings.utils.validation.DomainValidator
import org.koitharu.kotatsu.settings.utils.validation.HeaderValidator
-fun PreferenceFragmentCompat.addPreferencesFromRepository(repository: RemoteMangaRepository) {
+fun PreferenceFragmentCompat.addPreferencesFromRepository(repository: MangaRepository) = when (repository) {
+ is ParserMangaRepository -> addPreferencesFromParserRepository(repository)
+ is EmptyMangaRepository -> addPreferencesFromEmptyRepository()
+ else -> Unit
+}
+
+private fun PreferenceFragmentCompat.addPreferencesFromParserRepository(repository: ParserMangaRepository) {
+ addPreferencesFromResource(R.xml.pref_source_parser)
val configKeys = repository.getConfigKeys()
val screen = preferenceScreen
for (key in configKeys) {
@@ -100,6 +109,16 @@ fun PreferenceFragmentCompat.addPreferencesFromRepository(repository: RemoteMang
}
}
+private fun PreferenceFragmentCompat.addPreferencesFromEmptyRepository() {
+ val preference = Preference(requireContext())
+ preference.setIcon(R.drawable.ic_alert_outline)
+ preference.isPersistent = false
+ preference.isSelectable = false
+ preference.order = 200
+ preference.setSummary(R.string.unsupported_source)
+ preferenceScreen.addPreference(preference)
+}
+
private fun Array.toStringArray(): Array {
return Array(size) { i -> this[i] as? String ?: "" }
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsFragment.kt
index 55923a382..d9b592942 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsFragment.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsFragment.kt
@@ -9,7 +9,11 @@ import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
+import org.koitharu.kotatsu.core.model.getTitle
+import org.koitharu.kotatsu.core.parser.EmptyMangaRepository
+import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.util.ext.observe
@@ -17,6 +21,7 @@ import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
+import java.io.File
@AndroidEntryPoint
class SourceSettingsFragment : BasePreferenceFragment(0), Preference.OnPreferenceChangeListener {
@@ -26,23 +31,28 @@ class SourceSettingsFragment : BasePreferenceFragment(0), Preference.OnPreferenc
override fun onResume() {
super.onResume()
- setTitle(viewModel.source.title)
+ context?.let { ctx ->
+ setTitle(viewModel.source.getTitle(ctx))
+ }
viewModel.onResume()
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
- preferenceManager.sharedPreferencesName = viewModel.source.name
+ preferenceManager.sharedPreferencesName = viewModel.source.name.replace(File.separatorChar, '$')
addPreferencesFromResource(R.xml.pref_source)
addPreferencesFromRepository(viewModel.repository)
+ val isValidSource = viewModel.repository !is EmptyMangaRepository
findPreference(KEY_ENABLE)?.run {
+ isVisible = isValidSource
onPreferenceChangeListener = this@SourceSettingsFragment
}
findPreference(KEY_AUTH)?.run {
- val authProvider = viewModel.repository.getAuthProvider()
+ val authProvider = (viewModel.repository as? ParserMangaRepository)?.getAuthProvider()
isVisible = authProvider != null
isEnabled = authProvider?.isAuthorized == false
}
+ findPreference(SourceSettings.KEY_SLOWDOWN)?.isVisible = isValidSource
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -101,7 +111,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0), Preference.OnPreferenc
const val EXTRA_SOURCE = "source"
fun newInstance(source: MangaSource) = SourceSettingsFragment().withArgs(1) {
- putSerializable(EXTRA_SOURCE, source)
+ putString(EXTRA_SOURCE, source.name)
}
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsViewModel.kt
index bd55af447..b241030a0 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsViewModel.kt
@@ -8,18 +8,19 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import okhttp3.HttpUrl
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
+import org.koitharu.kotatsu.core.parser.CachingMangaRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
-import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
+import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
-import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
+import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
-import org.koitharu.kotatsu.parsers.model.MangaSource
import javax.inject.Inject
@HiltViewModel
@@ -30,8 +31,8 @@ class SourceSettingsViewModel @Inject constructor(
private val mangaSourcesRepository: MangaSourcesRepository,
) : BaseViewModel(), SharedPreferences.OnSharedPreferenceChangeListener {
- val source = savedStateHandle.require(SourceSettingsFragment.EXTRA_SOURCE)
- val repository = mangaRepositoryFactory.create(source) as RemoteMangaRepository
+ val source = MangaSource(savedStateHandle.get(SourceSettingsFragment.EXTRA_SOURCE))
+ val repository = mangaRepositoryFactory.create(source)
val onActionDone = MutableEventFlow()
val username = MutableStateFlow(null)
@@ -39,28 +40,41 @@ class SourceSettingsViewModel @Inject constructor(
private var usernameLoadJob: Job? = null
init {
- repository.getConfig().subscribe(this)
- loadUsername()
+ when (repository) {
+ is ParserMangaRepository -> {
+ repository.getConfig().subscribe(this)
+ loadUsername(repository.getAuthProvider())
+ }
+ }
}
override fun onCleared() {
- repository.getConfig().unsubscribe(this)
+ when (repository) {
+ is ParserMangaRepository -> {
+ repository.getConfig().unsubscribe(this)
+ }
+ }
super.onCleared()
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
- if (key != SourceSettings.KEY_SLOWDOWN && key != SourceSettings.KEY_SORT_ORDER) {
- repository.invalidateCache()
+ when (repository) {
+ is CachingMangaRepository -> {
+ if (key != SourceSettings.KEY_SLOWDOWN && key != SourceSettings.KEY_SORT_ORDER) {
+ repository.invalidateCache()
+ }
+ }
}
}
fun onResume() {
- if (usernameLoadJob?.isActive != true) {
- loadUsername()
+ if (usernameLoadJob?.isActive != true && repository is ParserMangaRepository) {
+ loadUsername(repository.getAuthProvider())
}
}
fun clearCookies() {
+ if (repository !is ParserMangaRepository) return
launchLoadingJob(Dispatchers.Default) {
val url = HttpUrl.Builder()
.scheme("https")
@@ -68,7 +82,7 @@ class SourceSettingsViewModel @Inject constructor(
.build()
cookieJar.removeCookies(url, null)
onActionDone.call(ReversibleAction(R.string.cookies_cleared, null))
- loadUsername()
+ loadUsername(repository.getAuthProvider())
}
}
@@ -78,11 +92,11 @@ class SourceSettingsViewModel @Inject constructor(
}
}
- private fun loadUsername() {
+ private fun loadUsername(authProvider: MangaParserAuthProvider?) {
launchLoadingJob(Dispatchers.Default) {
try {
username.value = null
- username.value = repository.getAuthProvider()?.getUsername()
+ username.value = authProvider?.getUsername()
} catch (_: AuthRequiredException) {
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt
index 3470c28de..ca269f4a0 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt
@@ -14,6 +14,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
+import org.koitharu.kotatsu.core.ui.image.AnimatedFaviconDrawable
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.list.OnTipCloseListener
import org.koitharu.kotatsu.core.util.ext.crossfade
@@ -62,7 +63,7 @@ fun sourceConfigItemDelegate2(
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
crossfade(context)
error(fallbackIcon)
- placeholder(fallbackIcon)
+ placeholder(AnimatedFaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name))
fallback(fallbackIcon)
source(item.source)
enqueueWith(coil)
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt
index 11c33dd17..588dda11a 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt
@@ -18,15 +18,16 @@ import org.koitharu.kotatsu.browser.BrowserCallback
import org.koitharu.kotatsu.browser.BrowserClient
import org.koitharu.kotatsu.browser.ProgressChromeClient
import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
+import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository
-import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
+import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.TaggedActivityResult
import org.koitharu.kotatsu.core.util.ext.configureForParser
-import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import javax.inject.Inject
import com.google.android.material.R as materialR
@@ -46,12 +47,12 @@ class SourceAuthActivity : BaseActivity(), BrowserCallba
if (!setContentViewWebViewSafe { ActivityBrowserBinding.inflate(layoutInflater) }) {
return
}
- val source = intent?.getSerializableExtraCompat(EXTRA_SOURCE)
- if (source == null) {
+ val source = MangaSource(intent?.getStringExtra(EXTRA_SOURCE))
+ if (source !is MangaParserSource) {
finishAfterTransition()
return
}
- val repository = mangaRepositoryFactory.create(source) as? RemoteMangaRepository
+ val repository = mangaRepositoryFactory.create(source) as? ParserMangaRepository
authProvider = (repository)?.getAuthProvider() ?: run {
Toast.makeText(
this,
@@ -148,7 +149,7 @@ class SourceAuthActivity : BaseActivity(), BrowserCallba
fun newIntent(context: Context, source: MangaSource): Intent {
return Intent(context, SourceAuthActivity::class.java)
- .putExtra(EXTRA_SOURCE, source)
+ .putExtra(EXTRA_SOURCE, source.name)
}
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItem.kt
index d5ce18d8e..350c56b57 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItem.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItem.kt
@@ -3,12 +3,12 @@ package org.koitharu.kotatsu.settings.sources.catalog
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import org.koitharu.kotatsu.list.ui.model.ListModel
-import org.koitharu.kotatsu.parsers.model.MangaSource
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
sealed interface SourceCatalogItem : ListModel {
data class Source(
- val source: MangaSource,
+ val source: MangaParserSource,
) : SourceCatalogItem {
override fun areItemsTheSame(other: ListModel): Boolean {
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItemAD.kt
index fa7d0441c..035437529 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItemAD.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItemAD.kt
@@ -1,9 +1,7 @@
package org.koitharu.kotatsu.settings.sources.catalog
import androidx.core.content.ContextCompat
-import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
-import androidx.core.view.updatePadding
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
@@ -12,16 +10,15 @@ import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier.Companion.ignoreC
import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
+import org.koitharu.kotatsu.core.ui.image.AnimatedFaviconDrawable
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
-import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.core.util.ext.crossfade
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
import org.koitharu.kotatsu.core.util.ext.source
-import org.koitharu.kotatsu.databinding.ItemCatalogPageBinding
import org.koitharu.kotatsu.databinding.ItemEmptyHintBinding
import org.koitharu.kotatsu.databinding.ItemSourceCatalogBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -55,7 +52,7 @@ fun sourceCatalogItemSourceAD(
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
crossfade(context)
error(fallbackIcon)
- placeholder(fallbackIcon)
+ placeholder(AnimatedFaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name))
fallback(fallbackIcon)
source(item.source)
ignoreCaptchaErrors()
@@ -79,30 +76,3 @@ fun sourceCatalogItemHintAD(
binding.textSecondary.setTextAndVisible(item.text)
}
}
-
-fun sourceCatalogPageAD(
- listener: OnListItemClickListener,
- coil: ImageLoader,
- lifecycleOwner: LifecycleOwner,
-) = adapterDelegateViewBinding(
- { inflater, parent -> ItemCatalogPageBinding.inflate(inflater, parent, false) },
-) {
-
- val sourcesAdapter = SourcesCatalogAdapter(listener, coil, lifecycleOwner)
- with(binding.recyclerView) {
- setHasFixedSize(true)
- adapter = sourcesAdapter
- }
- val insetsDelegate = WindowInsetsDelegate()
- ViewCompat.setOnApplyWindowInsetsListener(itemView, insetsDelegate)
- itemView.addOnLayoutChangeListener(insetsDelegate)
- insetsDelegate.addInsetsListener { insets ->
- binding.recyclerView.updatePadding(
- bottom = insets.bottom + binding.recyclerView.paddingTop,
- )
- }
-
- bind {
- sourcesAdapter.items = item.items
- }
-}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogAdapter.kt
index 030a0f661..0e2ebbf01 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogAdapter.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogAdapter.kt
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.settings.sources.catalog
import android.content.Context
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
+import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
@@ -23,6 +24,6 @@ class SourcesCatalogAdapter(
}
override fun getSectionText(context: Context, position: Int): CharSequence? {
- return (items.getOrNull(position) as? SourceCatalogItem.Source)?.source?.title?.take(1)
+ return (items.getOrNull(position) as? SourceCatalogItem.Source)?.source?.getTitle(context)?.take(1)
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogViewModel.kt
index 227e70412..a924dfc5b 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogViewModel.kt
@@ -98,7 +98,7 @@ class SourcesCatalogViewModel @Inject constructor(
}
private suspend fun buildSourcesList(filter: SourcesCatalogFilter, query: String?): List {
- val sources = repository.getAvailableSources(
+ val sources = repository.queryParserSources(
isDisabledOnly = true,
isNewOnly = filter.isNewOnly,
excludeBroken = false,
@@ -124,9 +124,7 @@ class SourcesCatalogViewModel @Inject constructor(
},
)
} else {
- sources.sortedBy {
- it.isBroken
- }.map {
+ sources.map {
SourceCatalogItem.Source(source = it)
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesListProducer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesListProducer.kt
index f088acd2a..85b06587b 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesListProducer.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesListProducer.kt
@@ -1,7 +1,9 @@
package org.koitharu.kotatsu.settings.sources.manage
+import android.content.Context
import androidx.room.InvalidationTracker
import dagger.hilt.android.ViewModelLifecycle
+import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancelAndJoin
@@ -14,10 +16,10 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.TABLE_SOURCES
+import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
-import org.koitharu.kotatsu.core.util.ext.toEnumSet
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
@@ -26,6 +28,7 @@ import javax.inject.Inject
@ViewModelScoped
class SourcesListProducer @Inject constructor(
lifecycle: ViewModelLifecycle,
+ @ApplicationContext private val context: Context,
private val repository: MangaSourcesRepository,
private val settings: AppSettings,
) : InvalidationTracker.Observer(TABLE_SOURCES) {
@@ -65,10 +68,10 @@ class SourcesListProducer @Inject constructor(
val isNsfwDisabled = settings.isNsfwContentDisabled
val isReorderAvailable = settings.sourcesSortOrder == SourcesSortOrder.MANUAL
val withTip = isReorderAvailable && settings.isTipEnabled(TIP_REORDER)
- val enabledSet = enabledSources.toEnumSet()
+ val enabledSet = enabledSources.toSet()
if (query.isNotEmpty()) {
return enabledSources.mapNotNull {
- if (!it.title.contains(query, ignoreCase = true)) {
+ if (!it.getTitle(context).contains(query, ignoreCase = true)) {
return@mapNotNull null
}
SourceConfigItem.SourceItem(
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt
index b5ef3263b..321a54334 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt
@@ -2,8 +2,8 @@ package org.koitharu.kotatsu.settings.sources.model
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
+import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.list.ui.model.ListModel
-import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
sealed interface SourceConfigItem : ListModel {
@@ -17,7 +17,7 @@ sealed interface SourceConfigItem : ListModel {
) : SourceConfigItem {
val isNsfw: Boolean
- get() = source.contentType == ContentType.HENTAI
+ get() = source.isNsfw()
override fun areItemsTheSame(other: ListModel): Boolean {
return other is SourceItem && other.source == source
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt
index a87d532f7..9d3d40eec 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt
@@ -14,12 +14,11 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.util.ext.onFirst
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
-import org.koitharu.kotatsu.list.domain.ListExtraProvider
+import org.koitharu.kotatsu.list.domain.MangaListMapper
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorState
-import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository
import javax.inject.Inject
@@ -27,7 +26,7 @@ import javax.inject.Inject
class SuggestionsViewModel @Inject constructor(
repository: SuggestionRepository,
settings: AppSettings,
- private val extraProvider: ListExtraProvider,
+ private val mangaListMapper: MangaListMapper,
downloadScheduler: DownloadWorker.Scheduler,
private val suggestionsScheduler: SuggestionsWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler) {
@@ -37,7 +36,7 @@ class SuggestionsViewModel @Inject constructor(
override val content = combine(
repository.observeAll(),
- listMode,
+ observeListModeWithTriggers(),
) { list, mode ->
when {
list.isEmpty() -> listOf(
@@ -49,7 +48,7 @@ class SuggestionsViewModel @Inject constructor(
),
)
- else -> list.toUi(mode, extraProvider)
+ else -> mangaListMapper.toListModelList(list, mode)
}
}.onStart {
loadingCounter.increment()
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt
index 9b19ea0ff..dc7ce0661 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt
@@ -5,7 +5,7 @@ import coil.request.CachePolicy
import dagger.Reusable
import org.koitharu.kotatsu.core.model.getPreferredBranch
import org.koitharu.kotatsu.core.parser.MangaRepository
-import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
+import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.util.MultiMutex
import org.koitharu.kotatsu.core.util.ext.toInstantOrNull
import org.koitharu.kotatsu.history.data.HistoryRepository
@@ -36,7 +36,7 @@ class Tracker @Inject constructor(
): MangaUpdates = mangaMutex.withLock(track.manga.id) {
val updates = runCatchingCancellable {
val repo = mangaRepositoryFactory.create(track.manga.source)
- require(repo is RemoteMangaRepository) { "Repository ${repo.javaClass.simpleName} is not supported" }
+ require(repo is ParserMangaRepository) { "Repository ${repo.javaClass.simpleName} is not supported" }
val manga = repo.getDetails(track.manga, CachePolicy.WRITE_ONLY)
compare(track, manga, getBranch(manga))
}.getOrElse { error ->
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackDebugAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackDebugAD.kt
index 9590267eb..e1a62459a 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackDebugAD.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackDebugAD.kt
@@ -58,7 +58,7 @@ fun trackDebugAD(
append(" - ")
bold {
color(context.getThemeColor(materialR.attr.colorError, Color.RED)) {
- append(getString(R.string.error))
+ append(item.lastError ?: getString(R.string.error))
}
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackDebugItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackDebugItem.kt
index 0e59c15ef..851c39427 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackDebugItem.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackDebugItem.kt
@@ -11,6 +11,7 @@ data class TrackDebugItem(
val lastCheckTime: Instant?,
val lastChapterDate: Instant?,
val lastResult: Int,
+ val lastError: String?,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackerDebugViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackerDebugViewModel.kt
index 942741882..cf591c685 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackerDebugViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackerDebugViewModel.kt
@@ -31,6 +31,7 @@ class TrackerDebugViewModel @Inject constructor(
lastCheckTime = it.track.lastCheckTime.toInstantOrNull(),
lastChapterDate = it.track.lastChapterDate.toInstantOrNull(),
lastResult = it.track.lastResult,
+ lastError = it.track.lastError,
)
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt
index 511ef1aae..70871c4a1 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt
@@ -20,12 +20,11 @@ import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.calculateTimeAgo
import org.koitharu.kotatsu.core.util.ext.call
-import org.koitharu.kotatsu.list.domain.ListExtraProvider
+import org.koitharu.kotatsu.list.domain.MangaListMapper
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState
-import org.koitharu.kotatsu.list.ui.model.toGridModel
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
import org.koitharu.kotatsu.tracker.ui.feed.model.FeedItem
@@ -42,7 +41,7 @@ class FeedViewModel @Inject constructor(
private val settings: AppSettings,
private val repository: TrackingRepository,
private val scheduler: TrackWorker.Scheduler,
- private val listExtraProvider: ListExtraProvider,
+ private val mangaListMapper: MangaListMapper,
) : BaseViewModel() {
private val limit = MutableStateFlow(PAGE_SIZE)
@@ -135,7 +134,7 @@ class FeedViewModel @Inject constructor(
null
} else {
UpdatedMangaHeader(
- mangaList.map { it.manga.toGridModel(listExtraProvider) },
+ mangaList.map { mangaListMapper.toGridModel(it.manga) },
)
}
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/model/UpdatedMangaHeader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/model/UpdatedMangaHeader.kt
index 71e2ce8de..d4687fd6e 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/model/UpdatedMangaHeader.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/model/UpdatedMangaHeader.kt
@@ -2,10 +2,10 @@ package org.koitharu.kotatsu.tracker.ui.feed.model
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
-import org.koitharu.kotatsu.list.ui.model.MangaItemModel
+import org.koitharu.kotatsu.list.ui.model.MangaListModel
data class UpdatedMangaHeader(
- val list: List,
+ val list: List,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/updates/UpdatesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/updates/UpdatesViewModel.kt
index 8bd8290be..e1ab3c5ed 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/updates/UpdatesViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/updates/UpdatesViewModel.kt
@@ -17,16 +17,13 @@ import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.core.util.ext.calculateTimeAgo
import org.koitharu.kotatsu.core.util.ext.onFirst
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
-import org.koitharu.kotatsu.list.domain.ListExtraProvider
+import org.koitharu.kotatsu.list.domain.MangaListMapper
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorState
-import org.koitharu.kotatsu.list.ui.model.toGridModel
-import org.koitharu.kotatsu.list.ui.model.toListDetailedModel
-import org.koitharu.kotatsu.list.ui.model.toListModel
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
import javax.inject.Inject
@@ -35,14 +32,14 @@ import javax.inject.Inject
class UpdatesViewModel @Inject constructor(
private val repository: TrackingRepository,
settings: AppSettings,
- private val extraProvider: ListExtraProvider,
+ private val mangaListMapper: MangaListMapper,
downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler) {
override val content = combine(
repository.observeUpdatedManga(),
settings.observeAsFlow(AppSettings.KEY_UPDATED_GROUPING) { isUpdatedGroupingEnabled },
- listMode,
+ observeListModeWithTriggers(),
) { mangaList, grouping, mode ->
when {
mangaList.isEmpty() -> listOf(
@@ -93,11 +90,7 @@ class UpdatesViewModel @Inject constructor(
prevHeader = header
}
}
- result += when (mode) {
- ListMode.LIST -> item.manga.toListModel(extraProvider)
- ListMode.DETAILED_LIST -> item.manga.toListDetailedModel(extraProvider)
- ListMode.GRID -> item.manga.toGridModel(extraProvider)
- }
+ result += mangaListMapper.toListModel(item.manga, mode)
}
return result
}
diff --git a/app/src/main/res/layout-w600dp-land/activity_details.xml b/app/src/main/res/layout-w600dp-land/activity_details.xml
index cb96897e4..2b2addde4 100644
--- a/app/src/main/res/layout-w600dp-land/activity_details.xml
+++ b/app/src/main/res/layout-w600dp-land/activity_details.xml
@@ -254,9 +254,8 @@
android:id="@+id/chips_tags"
android:layout_width="0dp"
android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/screen_padding"
android:layout_marginTop="@dimen/margin_small"
- android:paddingStart="@dimen/screen_padding"
- android:paddingEnd="@dimen/screen_padding"
app:chipSpacingHorizontal="6dp"
app:chipSpacingVertical="6dp"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/app/src/main/res/layout/activity_details.xml b/app/src/main/res/layout/activity_details.xml
index 9645983aa..ecb78409f 100644
--- a/app/src/main/res/layout/activity_details.xml
+++ b/app/src/main/res/layout/activity_details.xml
@@ -173,6 +173,7 @@
layout="@layout/layout_details_chips"
android:layout_width="0dp"
android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/screen_padding"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier_header" />
@@ -263,9 +264,8 @@
android:id="@+id/chips_tags"
android:layout_width="0dp"
android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/screen_padding"
android:layout_marginTop="@dimen/margin_normal"
- android:paddingStart="@dimen/screen_padding"
- android:paddingEnd="@dimen/screen_padding"
app:chipSpacingHorizontal="6dp"
app:chipSpacingVertical="6dp"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/app/src/main/res/layout/fragment_preview.xml b/app/src/main/res/layout/fragment_preview.xml
index 2f9018499..1539c93b2 100644
--- a/app/src/main/res/layout/fragment_preview.xml
+++ b/app/src/main/res/layout/fragment_preview.xml
@@ -164,8 +164,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
- android:paddingStart="@dimen/screen_padding"
- android:paddingEnd="@dimen/screen_padding"
+ android:layout_marginHorizontal="@dimen/screen_padding"
app:chipSpacingHorizontal="6dp"
app:chipSpacingVertical="6dp"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/app/src/main/res/layout/item_explore_source_grid.xml b/app/src/main/res/layout/item_explore_source_grid.xml
index e1cef0936..27e7a59d2 100644
--- a/app/src/main/res/layout/item_explore_source_grid.xml
+++ b/app/src/main/res/layout/item_explore_source_grid.xml
@@ -27,15 +27,17 @@
+ tools:drawableStart="@drawable/ic_pin_small"
+ tools:text="@tools:sample/lorem[0]" />
diff --git a/app/src/main/res/layout/item_explore_source_list.xml b/app/src/main/res/layout/item_explore_source_list.xml
index 0666e7f8f..2bd02a729 100644
--- a/app/src/main/res/layout/item_explore_source_list.xml
+++ b/app/src/main/res/layout/item_explore_source_list.xml
@@ -37,9 +37,11 @@
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:drawablePadding="2dp"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceTitleSmall"
+ tools:drawableStart="@drawable/ic_pin_small"
tools:text="@tools:sample/lorem[2]" />
+
+
diff --git a/app/src/main/res/layout/layout_details_chips.xml b/app/src/main/res/layout/layout_details_chips.xml
index 99244809c..f53a1bc5b 100644
--- a/app/src/main/res/layout/layout_details_chips.xml
+++ b/app/src/main/res/layout/layout_details_chips.xml
@@ -6,7 +6,6 @@
android:id="@+id/info_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:paddingHorizontal="@dimen/screen_padding"
app:chipSpacingHorizontal="6dp"
app:chipSpacingVertical="6dp">
diff --git a/app/src/main/res/layout/sheet_filter.xml b/app/src/main/res/layout/sheet_filter.xml
index a6645052a..3c9e2836d 100644
--- a/app/src/main/res/layout/sheet_filter.xml
+++ b/app/src/main/res/layout/sheet_filter.xml
@@ -24,13 +24,13 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
+ android:paddingHorizontal="@dimen/screen_padding"
android:paddingBottom="@dimen/margin_normal">
@@ -59,7 +58,6 @@
android:id="@+id/textView_locale_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_normal"
android:singleLine="true"
android:text="@string/language"
@@ -71,7 +69,6 @@
android:id="@+id/card_locale"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginHorizontal="16dp"
android:layout_marginTop="@dimen/margin_normal"
android:visibility="gone"
tools:visibility="visible">
@@ -90,7 +87,6 @@
android:id="@+id/textView_genres_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_normal"
android:singleLine="true"
android:text="@string/genres"
@@ -103,7 +99,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
- android:paddingHorizontal="@dimen/margin_normal"
android:visibility="gone"
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter"
tools:visibility="visible" />
@@ -115,7 +110,6 @@
android:layout_marginTop="8dp"
android:drawablePadding="16dp"
android:gravity="center_vertical"
- android:paddingHorizontal="@dimen/margin_normal"
android:paddingVertical="8dp"
android:textAppearance="?textAppearanceBodySmall"
android:visibility="gone"
@@ -128,7 +122,6 @@
android:id="@+id/textView_genresExclude_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_normal"
android:singleLine="true"
android:text="@string/genres_exclude"
@@ -141,7 +134,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
- android:paddingHorizontal="@dimen/margin_normal"
android:visibility="gone"
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter"
tools:visibility="visible" />
@@ -150,7 +142,6 @@
android:id="@+id/textView_state_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_normal"
android:singleLine="true"
android:text="@string/state"
@@ -163,7 +154,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
- android:paddingHorizontal="@dimen/margin_normal"
android:visibility="gone"
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter"
tools:visibility="visible" />
@@ -172,7 +162,6 @@
android:id="@+id/textView_contentRating_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_normal"
android:singleLine="true"
android:text="@string/content_rating"
@@ -185,7 +174,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
- android:paddingHorizontal="@dimen/margin_normal"
android:visibility="gone"
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter"
tools:visibility="visible" />
diff --git a/app/src/main/res/layout/sheet_welcome.xml b/app/src/main/res/layout/sheet_welcome.xml
index eb696b8bf..c6b4382d4 100644
--- a/app/src/main/res/layout/sheet_welcome.xml
+++ b/app/src/main/res/layout/sheet_welcome.xml
@@ -29,10 +29,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal"
+ android:drawablePadding="16dp"
android:singleLine="true"
android:text="@string/welcome"
android:textAppearance="?textAppearanceTitleLarge"
- android:drawablePadding="16dp"
app:drawableStartCompat="@drawable/ic_welcome" />
diff --git a/app/src/main/res/menu/mode_source.xml b/app/src/main/res/menu/mode_source.xml
index 8807324e7..1e19cc00e 100644
--- a/app/src/main/res/menu/mode_source.xml
+++ b/app/src/main/res/menu/mode_source.xml
@@ -10,9 +10,10 @@
app:showAsAction="ifRoom|withText" />
+
+
-
-
- %1$d فصل جديد
+ - لا يوجد فصل جديد
- %1$d فصل جديد
- - %1$d فصول جديدة
+ - %1$d فصلين جديدين
- %1$d فصول جديدة
- %1$d فصول جديدة
- %1$d فصول جديدة
- - %1$d فصل
- - %1$d فصل
- - %1$d فصلين
+ - لا يوجد فصول
+ - فصل واحد
+ - فصلين
- %1$d فصول
- %1$d فصول
- %1$d فصول
- - %1$d دقيقة مضت
- - %1$d دقيقة مضت
- - %1$d دقيقتين مضت
- - %1$d دقائق مضت
- - %1$d دقائق مضت
- - %1$d دقائق مضت
+ - %1$dدقائق مضت
+ - %1$dدقيقة مضت
+ - %1$dدقائق مضت
+ - %1$dدقائق مضت
+ - %1$dدقائق مضت
+ - %1$dدقائق مضت
- %1$d عنصر
- %1$d عنصر
- - %1$d عناصر
+ - %1$d عنصران
- %1$d عناصر
- %1$d عناصر
- %1$d عناصر
- - %1$d شهر مضا
- - %1$d شهر مضا
+ - %1$d شهر مضى
+ - %1$d شهر مضى
- %1$d شهرين مضت
- %1$d أشهر مضت
- %1$d أشهر مضت
- %1$d أشهر مضت
- - %1$d يوم مضا
- - %1$d يوم مضا
+ - %1$d يوم مضى
+ - %1$d يوم مضى
- %1$d يومين مضت
- %1$d أيام مضت
- %1$d أيام مضت
@@ -51,25 +51,25 @@
- %1$d ساعة مضت
- %1$d ساعة مضت
- - %1$d ساعات مضت
+ - %1$d ساعتين مضت
- %1$d ساعات مضت
- %1$d ساعات مضت
- %1$d ساعات مضت
-
- - دقيقة
- - دقيقتان
- - ثلاث دقائق
- - إحدى عشر دقيقة
- - مئة دقيقة
+ - %1$d دقيقة
+ - %1$d دقيقة
+ - %1$d دقيقتان
+ - %1$d دقائق
+ - %1$d دقيقة
+ - %1$d ساعة
- - العربية
-
-
-
-
-
+ - %1$d ساعة
+ - ساعة
+ - ساعتان
+ - %1$d ساعات
+ - %1$d ساعة
+ - %1$d ساعة
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 892de7473..da3948b8c 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -5,10 +5,10 @@
التفاصيل
شبكة
وضع القائمة
- إعدادات
+ الإعدادات
مصادر المانجا
- فصول
- المفضلة
+ الفصول
+ المُفضلة
خطاء في الشبكة
جار التحميل…
فصل %1$d من %2$d
@@ -17,7 +17,7 @@
جاري الحوسبة …
التخزين المحلي
السجل
- قائمة
+ القائمه
محو سجل
أضف للمفضلة
أضف
@@ -39,16 +39,16 @@
مظهر
حسب النظام
شارك %s
- في طور معالجة…
+ في طور المعالجة…
محدث
- فلتر
+ تصفية
ترتيب الفرز
فاتح
داكن
مسح
- ازالة
+ حذف
شائع
- فئة جديدة
+ قائمة جديدة
تم التنزيل
احفظ الصفحة
تم الحفظ
@@ -76,17 +76,17 @@
يمكنك استخدام سيرفر التزامن ذاتياً أو السيرفر الافتراضي. لا تغير هذا إن لم تكن متأكداً مما تفعله.
سيتم تسجيل خروجك من جميع المصادر
مسح ملفات تعريف الارتباط
- الفئات المفضلة
+ قوائم المُفضلة
مسح ذاكرة التخزين المؤقت للصور المصغرة
تدوير الشاشة
هل تريد مسح سجل التحديث بشكل دائم؟
تفعيل الاقتراحات
- مسح الخلاصة
+ مسح الموجز
مرحبا
ترجمة هذا التطبيق
اهتزاز
لا يوجد تحديثات
- حذف الفئة
+ حذف
التخزين الداخلي
اقرأ لاحقا
تم حفظ النسخة الاحتياطية
@@ -109,16 +109,16 @@
هل تريد إزالة استعلامات البحث الأخيرة نهائيًا؟
تم المسح
تحديث
- سيبدأ تحديث الخلاصة قريبًا
+ سيبدأ تحديث الموجز قريبًا
تتوفر نسخة جديدة من التطبيق
نسخة جديدة: %s
إعدادات التزامن
- أضف فئة جديدة
+ قائمة جديدة
صوت الإشعار
النسخ الاحتياطي و الاستعادة
إظهار أرقام الصفحات
تم المسح
- تم ازالة \"%s\" من التخزين المحلي
+ تم حذف \"%s\" من التخزين المحلي
المانجا المحفوظة
الفتح في المتصفح
ترجمة
@@ -141,7 +141,7 @@
كلمة السر غير مطابقة
أمس
تحقق من وجود تحديثات
- اطلب كلمة السر عند تشغيل كوتاتسو
+ اطلب كلمة السر عند تشغيل التطبيق
من اليمين الى اليسار
سيتم تذكر الاعدادات المختارة لهذه المانجا
الافتراضي: %s
@@ -163,7 +163,7 @@
ادخل كلمة السر
كرر كلمة السر
استعيدت جميع البيانات
- يمكنك إنشاء نسخة احتياطية من السجل الخاص بك والمفضلة واستعادتها
+ يمكنك إنشاء نسخة احتياطية من السجل الخاص بك والمُفضلة واستعادتها
تخزين خارجي
صامت
اليوم
@@ -177,8 +177,8 @@
وضع القارئ التلقائي
رف
تتبع
- ابحث عن ما تقرأه في قسم «استكشاف».
- جميع المفضلة
+ ابحث عن ما تقرأه في قسم «استكشاف»
+ كل المُفضلة
أدخل بريدك الإلكتروني للمتابعة
تعطيل الجميع
لا توجد فصول في هذه المانجا
@@ -186,8 +186,8 @@
إظهار مؤشرات التقدم في القراءة
معالجة المانجا المحفوظة
لاتوجد مساحة تخزين كافية
- "لن تتلقى إشعارات ولكن سيتم تمييز الفصول الجديدة في القوائم"
- فئة فارغة
+ لن تتلقى إشعارات ولكن سيتم تمييز الفصول الجديدة في القوائم
+ قائمة فارغة
ستتلقى إشعارات حول تحديثات المانجا التي تقرأها
مجلد التحميلات
أقرأها
@@ -196,8 +196,8 @@
اكتملت عملية الإزالة
تعديل
مطلوب التحقق من الCAPTCHA
- تمت الحذف من السجل
- "حدث خطأ ما. يرجى إرسال تقرير بالخطأ إلى المطورين لمساعدتنا في إصلاحه."
+ تم الحذف من السجل
+ حدث خطأ ما. يرجى إرسال تقرير بالخطأ إلى المطورين لمساعدتنا في إصلاحه.
اكتشف تلقائيًا ما إذا كانت المانجا عبارة عن webtoon
المانجا التي قرأتها مؤخرًا
مظهر
@@ -206,9 +206,9 @@
تسجيل الدخول على %s غير مدعوم
معلقَّة
اسم
- تغيير الفئة
+ تعديل القائمة
تتميز بعض الأجهزة بسلوك نظام مختلف، مما قد يؤدي إلى تعطيل مهام الخلفية.
- حدد الأنواع التي لا تريد رؤيتها في الاقتراحات
+ حدد التصنيفات التي لا تريد رؤيتها في الاقتراحات
وضع القياس
فقط على Wi-Fi
أسود
@@ -216,7 +216,7 @@
السماح
DNS مع HTTPS
مزامنة بياناتك
- مانغا من المفضلة لديك
+ مانجا من المُفضلة لديك
إرسال
اضافة إشارة مرجعية
احظر دائما
@@ -228,7 +228,7 @@
يتم تحليل جميع البيانات محليًا فقط على هذا الجهاز ولا يتم إرسالها إلى أي مكان.
تراجع
مناسب للمركز
- استبعاد مانغا +18 من سجل التصفح
+ استبعد المانجات NSFW من سجل التصفح
يساعد في تجنب حظر عنوان IP الخاص بك
هل تريد حذف العناصر المحددة من الجهاز نهائيًا؟
في قائمة الانتظار
@@ -237,7 +237,7 @@
التزامن
البحث عن الفصل
دائما
- استبعاد الأنواع
+ أقصاء التصنيفات
ألغيت
الحساب موجود بالفعل
أخفِ
@@ -257,19 +257,19 @@
+18
يستهلك طاقة بطارية أقل على شاشات AMOLED
تمكين الإشعارات
- لا تقترح مانغا +18
+ لا تقترح مانجات NSFW
أبداً
تعطيل \"إستهلاك أقل للبطارية\"
أنوي قرأتها
انيميشن الصفحة
الأنواع
خيارات تخزين أخرى
- حظر على محتوى +18
+ حظر على NSFW
إبقاء في البداية
احفظ شيئًا أو قم باستيراده من ملف.
قم بحفظ شيءٍ أولاً
المحفوظات
- لا توجد فئات مفضلة
+ لا توجد قوائم مُفضلة
سياسة لقطة الشاشة
تم
لا شكرا
@@ -278,7 +278,7 @@
ليس لديك أية تنزيلات
تم الإضافة للمحفظة
حذف البيانات
- اظهر نسبة القراءة في السجل و المفضلة
+ اظهر نسبة القراءة في السجل و المُفضلة
اظهر الكل
مجال غير صالح
تم مسح سجل التصفح
@@ -310,20 +310,21 @@
فصل %1$d/%2$d صفحة %3$d/%4$d
عرض شريط المعلومات في قارئ الصفحات
مجلد مع صور
- المانغا +18 لن يتم إضافتها إلى السجل، ولن يتم حفظ تقدمك فيها.\"
- يمكنها المساعدة في حالة حدوث مشاكل. سيتم إلغاء جميع التفويضات.
- اضغط زر العود للخلف مرة أخرى للخروج.
- لتتبع تقدم القراءة، اختر القائمة → \"تتبع\" على شاشة تفاصيل المانغا.\"
- UserAgent header
+ المانجات المعلمة كـ(NSFW) لن يتم إضافتها إلى السجل، ولن يتم حفظ تقدمك فيها
+ يمكنها المساعدة في حالة حدوث مشاكل. سيتم إلغاء جميع التفويضات
+ اضغط زر العود للخلف مرة أخرى للخروج
+ لتتبع تقدم القراءة، اختر القائمة ← \"تتبع\" على شاشة تفاصيل المانجا.
+ موجه UserAgent
يرجى إعادة تشغيل التطبيق لرؤية التغييرات.\"
- ازالة من المفضلة
+ تم الحذف من المُفضلة
لا توجد فصول
السماح بالتحديثات غير مستقرة
تلقي إشعارات حول الإصدارات الغير مستقرة
- هل أنت متأكد أنك تريد حذف فئات المفضلة المحددة؟ سيتم فقدان جميع المانغا فيها ولا يمكن التراجع عن هذا.
+ هل أنت متأكد أنك تريد حذف قوائم المُفضلة المحددة
+\n؟ سيتم فقدان جميع المانجا فيها ولا يمكن التراجع عن هذا.
بيانات التخزين المؤقت للصفحات
المحتوى غير موجود أو تمت إزالته
- يمكنك اختيار ملف أو أكثر بتنسيق cbz أو zip ، سيتم التعرف على كل ملف على أنه مانغا منفصلة
+ يمكنك اختيار ملف أو أكثر بتنسيق cbz أو zip ، سيتم التعرف على كل ملف على أنه مانغا منفصلة.
يمكنك اختيار مكان في الذاكرة يحتوي على أرشيفات أو صور. سيتم التعرف على كل أرشيف (أو مجلد فرعي) على أنه فصل.
السرعة
الموجز
@@ -339,7 +340,7 @@
هل تريد حفظ أو تجاهل التغييرات الغير المحفوظة؟
لا توجد مساحة متبقية على الجهاز
إظهار شريط التمرير لتبديل الصفحات
- خطأ من جانب الخادم (%1$s). الرجاء المحاولة مرة أخرى لاحقًا
+ خطأ من جانب الخادم (%1$d). الرجاء المحاولة مرة أخرى لاحقًا
عرض الشبكة
تفاصيل الخطأ:<br><tt>%1$s</tt><br><br>1. حاول <a href=\"%2$s\">فتح المانجا في متصفح ويب</a> للتأكد من أنها متوفرة على مصدرها<br>2. تأكد من أنك تستخدم <a href=kotatsu://about>أحدث إصدار من Kotatsu</a><br>3. إذا كانت متوفرة، أرسل تقرير خطأ إلى المطورين.
إتاحة المانجا الحديثة بالضغط المطول على أيقونة التطبيق
@@ -365,7 +366,7 @@
إيقاف التحميل عند الانتقال إلى شبكة الهاتف المحمول
استئناف
إلغاء الكل
- المصدر معطل
+ تم تعطيل المصدر
ريكا
ساكورا
إيقاف مؤقت
@@ -384,5 +385,273 @@
التحميل عبر شبكة الوايفاي فقط
إظهار الإشعارات أحيانًا بالمانغا المقترحة
اللغة العربية
-
+ هل ترغب في تلقي اقتراحات المانجا الشخصية؟
+ وكيل تحسين الصور
+ السماح بإيماءة التكبير في وضع الويب تون
+ مشاركة السجلات
+ تم التنزيل
+ اسم المستخدم
+ كلمة المرور
+ التفويض (اختياري)
+ إظهار الوقت الحالي و تقدم القراءة في الجزء العلوي من الشاشة
+ إظهار أرقام الصفحات في الزاوية السفلية
+ قيد التنفيذ
+ طلبات كثيرة جدا. حاول مرة أخرى في وقت لاحق
+ أدخل بريدك الإلكتروني وكلمة المرور للمتابعة
+ اتصال
+ البيانات والخصوصية
+ أولاً%s
+ رقم المنفذ غير صالح
+ استعادة النسخة الاحتياطية التي تم إنشاؤها مسبقًا
+ جميع الفصول غير المقروءة (%s)
+ التالي غير المقروء %s
+ ليس لديك حق الوصول إلى هذا الملف أو المجلد
+ اختر مجلد مخصص
+ مجلدات المانجا المحلية
+ وصف
+ هذا الشهر
+ البحث الصوتي
+ المانجا ذات الصلة
+ فاتح
+ غامق
+ ابيض
+ أسود
+ خلفية
+ لم تتم استعادة البيانات
+ لا تتحقق من وجود فصول جديدة باستخدام اتصالات الشبكة المقاسة
+ أدخل عنوان المانجا أو التصنيف أو اسم المصدر
+ تأكد من تحديد ملف النسخ الاحتياطي الصحيح
+ لا تقم بتحديث الاقتراحات باستخدام اتصالات الشبكة المقيدة
+ %s يتطلب حل CAPTCHA للعمل بشكل صحيح
+ تقدم
+ تمت الإضافة
+ أعرض
+ مجهول
+ عرض قائمة المانغا ذات الصلة. وفي بعض الحالات قد تكون غير دقيقة أو مفقودة
+ عكس الألوان
+ استخدم خدمة wsrv.nl لتقليل استهلاك الانترنت وتسريع تحميل الصور إن أمكن
+ أكتب
+ اللغات
+ المدمج
+ عنوان
+ حدد الفصول يدويا
+ اختر المرآة تلقائيًا
+ اقتراح : %s
+ تم استئناف التنزيلات
+ لقد تم إيقاف التنزيلات مؤقتًا
+ تمت إزالة التنزيلات
+ تم إلغاء التنزيلات
+ WebView غير متوفر: تحقق من تثبيت موفر الWebView
+ مسح ذاكرة التخزين المؤقت للشبكة
+ الوكيل
+ قيمة غير صالحة
+ منفذ
+ مسح ملفات تعريف الارتباط للمجال المحدد فقط. في معظم الحالات سوف يبطل الترخيص
+ جميع الفصول مع الترجمة : %s
+ المانجا كلها
+ جميع الفصول غير المقروءة
+ إدارة القوائم
+ مُتقدم
+ القائمة
+ تم تفعيل المصدر
+ قائمة المصادر
+ إبقاء الشاشة قيد التشغيل
+ إبقاء الشاشة قيد التشغيل اثناء القرائه
+ مانجا
+ القوائم
+ قائمة المانجا
+ تم تحريكه الى الاعلى
+ تكبير
+ تم استلام بيانات خاطئه او الملف تالف
+ يقلل من النطاقات، ولكنه قد يؤثر على الأداء
+ متروك
+ اظهر ازرار التكبير/التصغير
+ ما إذا كان سيتم إظهار أزرار التحكم في التكبير/التصغير في الزاوية اليمنى السفلية
+ وضع الألوان 32-bit
+ تكرار إنشاء النسخ الاحتياطي
+ النسخ الاحتياطي الدوري
+ أقترح مصادر جديدة عند تحديث البرنامج
+ المطالبة بتمكين المصادر المضافة حديثًا بعد تحديث التطبيق
+ كل يوم
+ البديل على الانترنت
+ كل يومين
+ مرة في الأسبوع
+ مرتين في الشهر
+ مرة في الشهر
+ تفعيل النسخ الاحتياطي الدوري
+ مجلد إخراج النسخ الاحتياطية
+ آخر نسخ احتياطي ناجح: %s
+ قفل دوران الشاشة
+ متاح: %1$d
+ لا يدعم مصدر المانجا هذا التصفية حسب التصنيفات المتعددة
+ قم بتعطيل مصادر NSFW وإخفاء المانجا للبالغين من القائمة إن أمكن
+ قم بتقليل جودة الصفحات الموجودة خارج الشاشة لاستخدام ذاكرة أقل
+ لا توجد مصادر متاحة في هذا القسم، أو ربما تمت إضافتها كلها بالفعل.
+\nابقوا متابعين
+ لا يدعم مصدر المانجا هذا التصفية حسب الحالات المتعددة
+ البحث غير مدعوم من مصدر المانجا هذا
+ يدوي
+ في الجهاز
+ الى الاعلى
+ تصغير
+ تقليل استهلاك الذاكرة (تجريبي)
+ أقسام الشاشه الرئيسيه
+ آخر
+ كوميكس
+ هينتاي
+ لا يمكن اضافة المزيد من العناصر
+ المجلدات
+ مصادر المانجات
+ متوقف
+ الحالة
+ خيارات القائمة
+ ملاءمة
+ لم يتم العثور على مصادر مانجا متاحة من خلال طلبك
+ يمكنك تمكين تباطؤ التنزيل لكل مصدر مانغا بشكل فردي في إعدادات المصدر إذا كنت تواجه مشكلات في الحظر من جانب الخادم
+ لا يدعم هذا المصدر التصفية حسب التصنيفات والإعدادات المحلية
+ لا يدعم هذا المصدر التصفية حسب التصنيفات والحالات
+ قد يساعد في بدء التنزيل إذا كان لديك أي مشاكل معه
+ ابدأ بكتابة اسم التصنيف
+ س%.1f
+ تخطى
+ تدرج الرمادي
+ عالماً
+ هذه المانجا
+ يمكن تطبيق هذه الإعدادات عالمياً أو على المانجا الحالية فقط. إذا تم تطبيقه عالمياً، فلن يتم تجاوز الإعدادات الفردية.
+ طَبِق
+ الرجاء تحديد مصادر المحتوى التي ترغب في تمكينها. يمكن أيضًا تكوين هذا لاحقًا في الإعدادات
+ مجلد مجهول
+ تقييم المحتوى
+ آخر ما تم قراءته
+ إظهار التسميات في شريط التنقل
+ ميزة الاقتراحات معطلة
+ تم تعطيل التحقق من الفصول الجديدة
+ ثلاث أشهر
+ نقل
+ قادم
+ الاسم معكوس
+ عَلِّم المانجا المحددة كمقروءة بالكامل
+\n
+\nتحذير: سيتم فقدان تقدم القراءة الحالي.
+ تم إخفاء هذه القائمة من الشاشة الرئيسية ويمكن الوصول إليها من خلال القائمة ← إدارة القوائم
+ استخدم أزرار الصوت للتبديل بين الصفحات
+ عمودي
+ أظهر القائمة
+ تمكين أزرار الصوت
+ لا شئ
+ اضغط مطولاً على الإجراء
+ شهر
+ كل الوقت
+ وضع ملء الشاشة
+ إخفاء حالة النظام وأشرطة التنقل
+ قد تكون قيمة تقدير الوقت غير دقيقة
+ عرض توقيت القراءة المقدرة
+ سجل المزامنة
+ أسترجع
+ تاريخ النسخ الاحتياطي: %s
+ اقصاء التصنيفات
+ آمن
+ للكبار
+ المجلد %d
+ لن يتم حفظ تقدم القراءة الخاص بك
+ إظهر/إخفِ واجهة المستخدم
+ الصفحة السابقة
+ أداء القارئ
+ اضغط على الإجراء
+ إعادة ضبط الإعدادات على القيم الافتراضية؟ لا يمكن التراجع عن هذا الإجراء.
+ استخدم تصميم الصفحتين في الاتجاه الأفقي (تجريبي)
+ تصغير الويبتون الافتراضي
+ حفظ الصفحات
+ يوم
+ نقل المانجا
+ لم يتم حذف أي فصول
+ احذف الفصول التي قرأتها بالفعل من وحدة التخزين المحلية لتحرير المساحة
+ سيؤدي هذا إلى حذف جميع الفصول المُعَلمة كـ\"مقروءة\" من وحدة التخزين المحلية لديك بشكل دائم. يمكنك إعادة تنزيلها لاحقًا، ولكن قد يتم فقدان الفصول المستوردة إلى الأبد
+ عَلِّم كمقروء
+ الفصل السابق
+ الفصل التالي
+ الصفحة التالية
+ سيتم استبدال المانجا \"%1$s\" من \"%2$s\" بـ \"%3$s\" من \"%4$s\" في سجلك ومفضلاتك (إن وجدت)
+ تم النقل بنجاح
+ حذف الفصول المقروءة
+ تمت إزالة %1$s، وتم مسح %2$s
+ حذف فصول المقروءة تلقائياً
+ آخر أستخدام
+ جديد
+ حذف من السجل
+ كل اللغات
+ الحظر عند وضع التصفح المتخفي
+ بدائل
+ لا توجد إحصائيات للفترة المحددة
+ خادم الصور المُفضل
+ قص الصفحات
+ تثبيت
+ إلغاء التثبيت
+ تم إلغاء تثبيت المصدر
+ تم تثبيت المصدر
+ تم إلغاء تثبيت المصادر
+ تم تثبيت المصادر
+ الصفحة الافتراضية
+ المصادر الحديثة
+ أقل من دقيقة
+ هل تريد حقًا مسح جميع إحصائيات القراءة؟ لا يمكن التراجع عن هذا الإجراء.
+ أسبوع
+ الصفحات التي قرأت: %s
+ قم بتمكين علامة التبويب \"الصفحات\" في شاشة التفاصيل
+ تعطيل التحقق من الاتصال
+ عرض التحديثات
+ موحية
+ تهيئة الإجراءات لمناطق الشاشة القابلة للنقر عليها
+ اطلب دليل الوجهة في كل مرة
+ مجلد حفظ الصفحة الافتراضية
+ تنسيق التحميل المُفضل
+ تلقائي
+ ملفات CBZ متعددة
+ إحصائيات القراءة
+ مانجا أخرى
+ الإحصائيات
+ حذف الإحصائيات
+ تم حذف الإحصائيات
+ تقسيم عن طريق الترجمة
+ اعرض الفصول ذات الترجمات المختلفة بشكل منفصل، وليس في قائمة واحدة
+ الأقدم
+ قرأت منذ وقت طويل
+ غير مقروءة
+ إظهار الصور المصغرة للصفحات
+ الرجاء تحديد ملف النسخ الاحتياطي السليم لKotatsu
+ %d ش
+ %1$d س %2$d ش
+ الفجوات في وضع الويبتون
+ المكان
+ تعطيل اشعارات NSFW
+ لا تعرض إشعارات حول تحديثات مانجات NSFW
+ التحقق من سجل الفصول الجديدة
+ تصحيح المعلومات حول عمليات التحقق من الخلفية للفصول الجديدة
+ اقتراحات البحث
+ الأستعلامات الأخيره
+ الاستعلامات المقترحة
+ ملف CBZ واحد
+ %d س
+ إظهار الفجوات الرأسية بين الصفحات في وضع الويبتون
+ المؤلفون
+ تخطي التحقق من الاتصال في حالة وجود مشكلات في الاتصال (على سبيل المثال، الانتقال إلى وضع عدم الاتصال بالإنترنت على الرغم من اتصال الشبكة)
+ يمكنك تعطيل التحقق من شهادات SSL في حالة مواجهة مشكلات متعلقة بـ SSL عند الوصول إلى موارد الشبكة. قد يؤثر هذا على أمانك. مطلوب إعادة تشغيل التطبيق بعد تغيير هذا الإعداد.
+ يعمل عند بدء تشغيل التطبيق
+ لم يتم تلقي أي بيانات من الخادم
+ تعطيل
+ تم تعطيل المصادر
+ تمكين المصدر
+ مصدر المانجا هذا غير مدعوم
+ لقد تم حظرك من قبل الخادم. حاول استخدام اتصال شبكة مختلف (VPN, Proxy, الخ.)
+ أقل ترددا
+ أكثر ترددا
+ تردد الفحص
+ تثبيت واجهة المستخدم
+ لا تخفي شريط التنقل وعرض البحث عند التمرير
+ اصلاح
+ لا يوجد إذن للوصول إلى المانجا على وحدة التخزين الخارجية
+ نسبة القراءة
+ النسبة المتبقية
+ الفصول المقروءة
+ الفصول المتبقية
\ No newline at end of file
diff --git a/app/src/main/res/values-be/plurals.xml b/app/src/main/res/values-be/plurals.xml
index e0e95afdf..bb85fbea9 100644
--- a/app/src/main/res/values-be/plurals.xml
+++ b/app/src/main/res/values-be/plurals.xml
@@ -38,4 +38,16 @@
- %1$d месяцаў таму
- %1$d месяцаў таму
+
+ - %1$dгадзіна
+ - %1$dгадзіны
+ - %1$dгадзін
+ - %1$dгадзін
+
+
+ - %1$dхвіліна
+ - %1$dхвіліны
+ - %1$dхвілін
+ - %1$dхвілін
+
\ No newline at end of file
diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml
index a9a5121bc..30863bd95 100644
--- a/app/src/main/res/values-be/strings.xml
+++ b/app/src/main/res/values-be/strings.xml
@@ -74,13 +74,13 @@
Ачысціць кэш мініяцюр
Гісторыя пошуку ачышчана
Ачысціць гісторыю пошуку
- Ўнутранае сховішча
+ Унутранае сховішча
Знешняе сховішча
Дамен
Даступна абнаўленне праграмы
Адкрыць у браўзеры
Паведамленні
- Ўключана %1$d з %2$d
+ Уключана %1$d з %2$d
Новыя раздзелы
Спампаваць
Налады апавяшчэнняў
@@ -97,8 +97,8 @@
Не атрымалася знайсці ніводнага даступнага сховішча
Іншае сховішча
Гатова
- Ўсе абраныя
- Ў гэтай катэгорыі нічога няма
+ Усе абраныя
+ У гэтай катэгорыі нічога няма
Прачытаць пазней
Абнаўленні
Тут паказваюцца новыя раздзелы таго, што вы чытаеце
@@ -112,7 +112,7 @@
Абнаўленне хутка пачнецца
Правяраць абнаўленні мангі
Не правяраць
- Ўвядзіце пароль
+ Увядзіце пароль
Няверны пароль
Абараніць праграму
Запытваць пароль пры запуску праграмы
@@ -141,7 +141,7 @@
Данныя адноўлены, але ўзніклі некаторыя памылкі
Вы можаце стварыць рэзервовую копію абранага і гісторыі і потым аднавіць іх
Толькі што
- Ўчора
+ Учора
Даўно
Групаваць
Сёння
@@ -151,12 +151,12 @@
Неабходна прайсці CAPTCHA
Прайсці
Ачысціць кукi
- Ўсе кукi выдалены
+ Усе кукi выдалены
Ачысціць стужку
Уся гісторыя абнаўленняў будзе ачышчана і яе нельга будзе вярнуць. Вы ўпэўненыя?
Праверка новых глаў
- Ў адваротным парадку
- Ўвайсці
+ У адваротным парадку
+ Увайсці
Для прагляду гэтага кантэнту патрабуецца аўтарызацыя
Прадвызначаны: %s
Далей
@@ -175,7 +175,7 @@
Паспрабуйце перафармуляваць запыт.
Неяк тут пуста…
Глава адсутнічае
- Ў чарзе
+ У чарзе
Дапамагчы з перакладам праграмы
Пераклад
Вы выйдзеце з усіх крыніц, у якіх вы аўтарызаваны
@@ -193,12 +193,12 @@
Заўсёды блакуйце
Забараніць для NSFW
Адкл.
- Ўключаны
+ Уключаны
Ня прапаноўваць NSFW мангу
Пачніце чытаць мангу, і вы атрымаеце персаналізаваныя прапановы
Усе даныя аналізуюцца толькі лакальна на гэтай прыладзе і нікуды не адпраўляюцца.
Прапануеце мангу, заснаваную на вашых перавагах
- Ўключыць прапановы
+ Уключыць прапановы
Прапанова
Выберыце мову, на якой вы хочаце чытаць мангу. Вы зможаце змяніць гэта пазней.
Скінуць фільтр
@@ -224,7 +224,7 @@
Правяраць новыя главы і паведамляць пра іх
Вы будзеце атрымліваць апавяшчэнні пра абнаўленні мангі, якую вы чытаеце
Вы не будзеце атрымліваць паведамленні, але новыя главы будуць паказаны ў спісе
- Ўключыць апавяшчэнні
+ Уключыць апавяшчэнні
Закладкі
Закладка выдалена
Закладка дадазена
@@ -293,7 +293,7 @@
Дынамічны
Каляровая гама
Мова
- Ўліковы запіс ужо існуе
+ Уліковы запіс ужо існуе
Назад
Сінхранізацыя
Сінхранізацыя вашых дадзеных
@@ -311,9 +311,9 @@
Скінуць
Захаваць ці адхіліць незахаваныя змены\?
Адмяніць
- Ўключыць запіс
+ Уключыць запіс
Падзяліцца логамі
- Запішыце некаторыя дзеянні для адладкі. Ўключайце толькі калі ведаеце, што робіце
+ Запішыце некаторыя дзеянні для адладкі. Уключайце толькі калі ведаеце, што робіце
Паказаць падазроны кантэнт
Адменена
Кіраваць
@@ -381,7 +381,7 @@
Спыніць загрузку пры пераключэнні на мабільную сетку
Часам паказваць апавяшчэнні з прапанаванай мангай
Больш
- Ўключыць
+ Уключыць
Усе актыўныя спампоўкі будуць адменены, часткова спампаваныя даныя будуць страчаны
Хочаце атрымліваць персаналізаваныя прапановы мангі\?
Прапанова: %s
@@ -416,10 +416,10 @@
Выдаліць файлы cookie толькі для вызначанага дамена. У большасці выпадкаў гэта робіць аўтарызацыю несапраўднай
Манга цалкам
Лакальныя каталогі мангі
- Ўсе раздзелы з перакладам %s
+ Усе раздзелы з перакладам %s
Першыя %s
- Ўсе непрачытаныя раздзелы (%s)
- Ўсе непрачытаныя раздзелы
+ Усе непрачытаныя раздзелы (%s)
+ Усе непрачытаныя раздзелы
Выбірайце раздзелы ўручную
Выберыце карыстальніцкі каталог
Наступная непрачытаная %s
@@ -437,7 +437,7 @@
Чорны
Фон
Пераканайцеся, што вы выбралі правільны файл рэзервовай копіі
- Ўвядзіце назву мангі, жанр або назву крыніцы
+ Увядзіце назву мангі, жанр або назву крыніцы
Прагрэс
Кіраванне катэгорыямі
Дададзена
@@ -445,10 +445,10 @@
Для правільнай працы %s патрабуецца праверка captcha
Мовы
Невядомы
- Ў працэсе
+ У працэсе
Адключыць NSFW
Занадта шмат запытаў. Паўтарыце спробу пазней
- Паказаць спіс звязанай мангі. Ў некаторых выпадках ён можа быць недакладным або адсутнічаць
+ Паказаць спіс звязанай мангі. У некаторых выпадках ён можа быць недакладным або адсутнічаць
Прасунутая
Спіс мангі
Вяртаюцца няправільныя дадзеныя ці файл пашкоджаны
@@ -457,7 +457,7 @@
Больш нельга дадаваць элементы
Каталогі
Раздзелы галоўнага экрана
- Ўверх
+ Уверх
Павялічыць
Ці паказваць кнопкі кіравання маштабаваннем у правым ніжнім куце
Паказаць кнопкі маштабавання
@@ -475,7 +475,7 @@
Анлайн варыянт
Кожны дзень
Частата стварэння рэзервовых копій
- Ўключыць перыядычнае рэзервовае капіраванне
+ Уключыць рэзервовае капіраванне па раскладзе
Кожныя 2 дні
Раз на тыдзень
Перыядычнае рэзервовае капіраванне
@@ -493,7 +493,7 @@
Каталог
Кіраванне крыніцамі
Па вашаму запыту не знойдзена даступных крыніц мангі
- Ўручную
+ Уручную
Крыніца ўключана
Адключыць крыніцы NSFW і схавайць мангу для дарослых са спісу, калі гэта магчыма
У гэтым раздзеле няма даступных крыніц, ці ўсе яны маглі быць ужо дададзены.
@@ -543,7 +543,7 @@
Папярэдняя старонка
Наступная старонка
Дзеянні ў рэжыме чытання
- Ўключыць кнопкі гучнасці
+ Уключыць кнопкі гучнасці
Выкарыстоўвайце кнопкі гучнасці для гартання старонак
Дзеянне пры націску
Дзеянне пры доўгім націску
@@ -578,7 +578,7 @@
Тыдзень
Статыстыка ачышчана
Месяц
- Ўвесь час
+ Увесь час
Дзень
Статыстыка за абраны перыяд адсутнічае
Прачытана старонак: %s
@@ -606,7 +606,7 @@
Самы стары
Непрачытаная
Аўтаматычна выдаляць прачытаныя раздзелы
- Ўключыць крыніцу
+ Уключыць крыніцу
Гэтая крыніца мангі не падтрымліваецца
Паказаць мініяцюры старонак
Уключыце ўкладку «Старонкі» на экране звестак
@@ -644,4 +644,13 @@
Новае
Усе мовы
Блакіраваць у рэжыме інкогніта
+ Сервер малюнкаў
+ Замацаваць
+ Адмацаваць
+ Крыніца адмацавана
+ Крыніца замацавана
+ Крыніцы адмацаваны
+ Нядаўнія крыніцы
+ Крыніцы замацаваны
+ Абрэзаць старонкі
\ No newline at end of file
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 950531d70..359bdc190 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -646,4 +646,16 @@
Bloquear en modo incógnito
Servidor de imágenes preferido
Páginas de recortes
+ Fuentes recientes
+ Fijar
+ No fijar
+ Fuente fijada
+ Fuente no fijada
+ Fuentes no fijadas
+ Fuentes ancladas
+ Porcentaje leído
+ Porcentaje restante
+ Capítulos leídos
+ Capítulos restantes
+ Externo/plugin
\ No newline at end of file
diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml
index e7ca1b992..db3354b0f 100644
--- a/app/src/main/res/values-fil/strings.xml
+++ b/app/src/main/res/values-fil/strings.xml
@@ -644,4 +644,17 @@
Mga bago
Harangan pag naka-incognito mode
Lahat ng wika
+ Piniling image server
+ Mag-crop ng pahina
+ Na-unpin ang source
+ Mga source na na-unpin
+ I-unpin
+ Mga source na na-pin
+ I-pin
+ Naka-pin ang source
+ Kamakailang mga source
+ Porsyentong natitira
+ Kabanatang natitira
+ Porsyentong nabasa
+ Kabanatang nabasa
\ No newline at end of file
diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml
index 8576f23ce..f476606de 100644
--- a/app/src/main/res/values-hi/strings.xml
+++ b/app/src/main/res/values-hi/strings.xml
@@ -646,4 +646,11 @@
गुप्त मोड में ब्लॉक करें
पसंदीदा छवि सर्वर
पृष्ठ काटें
+ पिन करें
+ अनपिन करें
+ स्रोत अनपिन किए गए
+ स्रोत पिन किए गए
+ स्रोत पिन किया गया
+ स्रोत अनपिन किया गया
+ हालिया स्रोत
\ No newline at end of file
diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml
index c18822f71..f379a9fc3 100644
--- a/app/src/main/res/values-in/strings.xml
+++ b/app/src/main/res/values-in/strings.xml
@@ -3,8 +3,8 @@
Penyimpanan lokal
Favorit
Riwayat
- Terjadi galat
- Galat jaringan
+ Terjadi kesalahan
+ Kesalahan jaringan
Detail
Kisi
Mode daftar
@@ -15,7 +15,7 @@
Bab %1$d dari %2$d
Tutup
Coba lagi
- Tidak ketemu
+ Tidak ditemukan
Riwayat kosong
Baca
Belum ada favorit
@@ -643,4 +643,6 @@
Jangan sembunyikan bilah navigasi dan tampilan pencarian saat menggulir
Dihapus, dibersihkan
Pertanyaan yang disarankan
+ Sumber disematkan
+ Sumber sumbver terbaru
\ No newline at end of file
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 685cc35e8..2b77d9242 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -435,7 +435,7 @@
Proxy ottimizzazione immagini
Nome utente
Due volte al mese
- Sezioni principali schermo
+ Sezioni schermata principale
Avanzate
Se stai avendo problemi di blocco dal server, puoi abilitare il rallentamento dei download per ogni fonte di manga nelle impostazioni delle fonti
Impostazioni di sincronizzazione
@@ -498,7 +498,7 @@
Stato
Lista manga
Bianco e nero
- Disabilita contenuti per adulti
+ Disabilita contenuti NSFW
Ultimo backup completato: %s
Bianco
I download sono stati ripresi
@@ -609,4 +609,52 @@
File CBZ singolo
Molti file CBZ
Lettura statistiche
+ Rimuovi dalla cronologia
+ Statistiche
+ Migrare
+ Alternative
+ Server delle immagini preferito
+ Fissa
+ Fonti fissate
+ Fonte fissta
+ Fonti recenti
+ Cancella statistiche
+ Statistiche cancellate
+ Vuoi veramente cancellare le statistiche di lettura? Questa azione non può essere annullata.
+ Settimana
+ Mese
+ Pagine lette: %s
+ Disbilita controllo della connettività
+ Altri Manga
+ Sempre
+ Disabilita notifiche NSFW
+ Disabilita le notifiche su fonti per adulti ed aggiornamenti di manga per adulti
+ Autori
+ Cartella di salvataggo predefinita di una pagina
+ Meno di un minuto
+ Il manga \"%1$s\" da \"%2$s\" sarà sostituito con \"%3$s\" da \"%4$s\" sia nella cronologia che nei preferiti (se presente)
+ Query recenti
+ Sei stato bloccato dal server. Prova ad usare una connessione differente (VPN, Proxy, ecc.)
+ Puoi disabilitare la verifica dei certificati SSL in caso incontri degli errori legati a SSL quando si tenta di accedere a una risorsa di rete. Questo può compromettera la tua sicurezza. È richiesto riavviare l\'applicazione in seguto.
+ Salta la verifica di connessione in caso di problemi con essa (es. andare in modalità offline pur essendo conessi ad una rete)
+ Blocca in modalità incognito
+ Query suggerite
+ Disabilita
+ Fonti disabilitate
+ Blocca UI di navigazione
+ Non nascone la barra di navigazione e di ricerca durante lo scorrimento
+ Percentuale di lettura
+ Percentuale rimasta
+ Capitoli letti
+ Capitoli rimasti
+ Riduzione dello zoom Webtoon predefinito
+ Ritagliare le pagine
+ Sblocca
+ Fonte non individuata
+ Fonti non individuate
+ Informazioni di debug sui controlli in background per i nuovi capitoli
+ Posizione
+ Rimosso %1$s, cancellato %2$s
+ Suggerimenti per la ricerca
+ Controllo del registro di nuovi capitoli
\ No newline at end of file
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 4e694e265..1e22585c5 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -644,4 +644,17 @@
Nowy
Blokuj w trybie incognito
Wszystkie języki
+ Preferowany serwer obrazów
+ Kadrowanie stron
+ Przypnij
+ Odepnij
+ Przypięte źródło
+ Źródło odpięte
+ Odpięte źródła
+ Przypięte źródła
+ Najnowsze źródła
+ Procent przeczytania
+ Pozostały procent
+ Przeczytane rozdziały
+ Pozostałe rozdziały
\ No newline at end of file
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 060e4cd53..6455f03ef 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -637,4 +637,11 @@
%1$s: %2$d
Fixar interface de navegação
Não esconder barra de navegação e visualização de pesquisa ao rolar
+ Novo
+ Todas as línguas
+ Bloquear no modo de navegação anônima
+ Servidor de imagem preferido
+ Cortar páginas
+ Desativar notificações NSFW
+ Não mostrar notificações sobre atualizações de mangás NSFW
\ No newline at end of file
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 2e3849546..f2fb51395 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -653,4 +653,8 @@
Открепить
Источник откреплён
Сервер изображений
+ Процент прочитанного
+ Глав прочитано
+ Процент оставшегося
+ Глав осталось
\ No newline at end of file
diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml
index 6ace27805..6d61b797d 100644
--- a/app/src/main/res/values-sr/strings.xml
+++ b/app/src/main/res/values-sr/strings.xml
@@ -644,4 +644,17 @@
Нови
Сви језици
Блокирај у режиму без архивирања
+ Преостали постотак
+ Постотак читања
+ Прочитана поглавља
+ Остала поглавља
+ Закачи
+ Откачи
+ Извор је закачен
+ Извор је откачен
+ Извори су откачени
+ Извори су закачени
+ Недавни извори
+ Жељени послуживач слика
+ Изрежи странице
\ No newline at end of file
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 0b7cccaf7..a2bdd4818 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -644,4 +644,18 @@
Yeni
Tüm diller
Gizli moddayken engelle
+ Tercih edilen resim sunucusu
+ Sayfaları kırp
+ Sabitle
+ Sabitlemeyi kaldır
+ Kaynak sabitlendi
+ Kaynağın sabitlenmesi kaldırıldı
+ Kaynakların sabitlenmesi kaldırıldı
+ Kaynaklar sabitlendi
+ Son kaynaklar
+ Okunan yüzde
+ Kalan yüzde
+ Okunan bölüm
+ Kalan bölüm
+ Harici/eklenti
\ No newline at end of file
diff --git a/app/src/main/res/values-vi/plurals.xml b/app/src/main/res/values-vi/plurals.xml
index 95a0c9ff1..833b63d53 100644
--- a/app/src/main/res/values-vi/plurals.xml
+++ b/app/src/main/res/values-vi/plurals.xml
@@ -1,7 +1,7 @@
- - %1$d chương mới
+ - %1$d Chương mới
- %1$d chương
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index 623fc60fe..f39046063 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -312,7 +312,7 @@
Bạn có thể xoá file gốc khỏi bộ nhớ để tiết kiệm dung lượng
Đặt lại
Bảng tin
- Chạm vào bên phải màn hình hoặc nhấn nút phải để chuyển tới trang tiếp theo
+ Chạm vào bên phải màn hình hoặc nhấn nút phải để chuyển tới trang tiếp theo.
Hiển thị lối tắt truyện đọc gần đây
Tương phản
Hiện thanh trượt chuyển trang
@@ -632,4 +632,29 @@
Truy vấn được đề xuất
Tác giả
Bạn bị chặn bởi máy chủ. Hãy thử sử dụng kết nối mạng khác (VPN, Proxy, v.v.)
+ Mới
+ Tất cả các ngôn ngữ
+ Chặn khi sử dụng chế độ ẩn danh
+ Máy chủ hình ảnh đã thích
+ Cắt ảnh trang
+ Ghim
+ Xóa ghim
+ Nguồn truyện đã được ghim
+ Đã xóa ghim nguồn truyện này
+ Đã xóa ghim các nguồn truyện này
+ Đã ghim các nguồn truyện này
+ Các nguồn truyện trước đó
+ Vô hiệu hóa tự động kiểm tra kết nối mạng
+ Vô hiệu hóa thông báo NSFW
+ Ẩn các thông báo liên quan đến cập nhật thông tin về những bộ truyện dành cho người lớn (NSFW)
+ Kiểm tra các bản ghi về chương mới
+ Bỏ qua kiểm tra kết nối mạng của bạn trong ứng dụng nếu bạn gặp vấn đề với nó (Ví dụ như bạn đã kết nối mạng nhưng ứng dụng nói \"Bạn đang ngoại tuyến\",...)
+ Bạn có thể vô hiệu hóa xác thực chứng chỉ SSL nếu bạn gặp phải sự cố liên quan đến SSL khi truy cập vào nguồn truyện. Điều này có thể ảnh hưởng đến việc bảo mật kết nối của bạn. Bắt buộc phải khởi động lại ứng dụng nếu bạn thay đổi thiết đặt này.
+ Vô hiệu hóa
+ Nguồn đã bị vô hiệu hóa
+ Gỡ lỗi các thông tin về tính năng kiểm tra chương mới trong nền
+ Tiến trình đọc
+ Tiến trình đọc còn lại
+ Chương đã đọc
+ Chương còn lại
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 5c2acb228..bf1f5ea13 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -435,7 +435,7 @@
使用移动网络时停止检查新章节
添加日期
本地
- 移动到顶部
+ 已移动至顶部
备份文件不正确
未知
进行中
@@ -454,7 +454,7 @@
漫画列表
隐藏成人内容
白色
- 置顶
+ 移动至顶部
显示
黑色
放大
@@ -636,12 +636,26 @@
图源已关闭
关闭连接连通性检查
若连通性检查存在问题可打开此选项(例:即使连接了网络但依旧提示网络断开)
- 若在连接到在线图源时SSL证书出现问题,可关闭SSL证书认证,关闭后对安全性有所影响,需要重启应用来更改设置。
- 关闭成人内容提醒
- 不显示成人漫画的更新提醒
+ 连接在线图源时若SSL证书出现问题,可关闭SSL证书认证,关闭后对安全性有所影响,需要重启应用来更改设置。
+ 关闭成人内容相关通知
+ 不接收成人漫画的更新通知
漫画更新日志
记录漫画后台更新时的调试日志
最新
开启无痕模式时禁止
所有语言
+ 图片服务器偏好
+ 裁剪页面
+ 置顶
+ 取消置顶
+ 图源已置顶
+ 图源置顶已取消
+ 图源置顶已取消
+ 图源已置顶
+ 最近使用图源
+ 已读章节百分比
+ 剩余章节百分比
+ 已读章节数
+ 剩余章节数
+ 外部插件
\ No newline at end of file
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 2107a6fe3..3b1f052ce 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -101,4 +101,11 @@
- @string/pages
- @string/webtoon
+
+ - @string/disabled
+ - @string/percent_read
+ - @string/percent_left
+ - @string/chapters_read
+ - @string/chapters_left
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 08495a58b..4424582d6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -664,4 +664,9 @@
Sources unpinned
Sources pinned
Recent sources
+ Percent read
+ Percent left
+ Chapters read
+ Chapters left
+ External/plugin
diff --git a/app/src/main/res/xml/pref_appearance.xml b/app/src/main/res/xml/pref_appearance.xml
index 1ab9e5660..8acb08a2e 100644
--- a/app/src/main/res/xml/pref_appearance.xml
+++ b/app/src/main/res/xml/pref_appearance.xml
@@ -38,11 +38,11 @@
android:valueTo="150"
app:defaultValue="100" />
-
+
diff --git a/app/src/main/res/xml/pref_source.xml b/app/src/main/res/xml/pref_source.xml
index 3ce86fef8..5600eed76 100644
--- a/app/src/main/res/xml/pref_source.xml
+++ b/app/src/main/res/xml/pref_source.xml
@@ -11,21 +11,6 @@
android:persistent="false"
android:title="@string/enable_source" />
-
-
-
-
+
+
+
+
+
+
+
diff --git a/app/src/test/kotlin/org/koitharu/kotatsu/core/backup/JsonSerializerTest.kt b/app/src/test/kotlin/org/koitharu/kotatsu/core/backup/JsonSerializerTest.kt
index 2ce335749..b4d2667ca 100644
--- a/app/src/test/kotlin/org/koitharu/kotatsu/core/backup/JsonSerializerTest.kt
+++ b/app/src/test/kotlin/org/koitharu/kotatsu/core/backup/JsonSerializerTest.kt
@@ -7,7 +7,7 @@ import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
-import org.koitharu.kotatsu.parsers.model.MangaSource
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.SortOrder
import java.util.concurrent.TimeUnit
@@ -42,7 +42,7 @@ class JsonSerializerTest {
largeCoverUrl = null,
state = MangaState.FINISHED.name,
author = "RERE",
- source = MangaSource.DUMMY.name,
+ source = MangaParserSource.DUMMY.name,
)
val json = JsonSerializer(entity).toJson()
val result = JsonDeserializer(json).toMangaEntity()
@@ -55,7 +55,7 @@ class JsonSerializerTest {
id = 934023534,
title = "Adventure",
key = "adventure",
- source = MangaSource.DUMMY.name,
+ source = MangaParserSource.DUMMY.name,
)
val json = JsonSerializer(entity).toJson()
val result = JsonDeserializer(json).toTagEntity()
diff --git a/app/src/test/kotlin/org/koitharu/kotatsu/reader/domain/ChapterPagesTest.kt b/app/src/test/kotlin/org/koitharu/kotatsu/reader/domain/ChapterPagesTest.kt
index 039115a00..6829c7b4c 100644
--- a/app/src/test/kotlin/org/koitharu/kotatsu/reader/domain/ChapterPagesTest.kt
+++ b/app/src/test/kotlin/org/koitharu/kotatsu/reader/domain/ChapterPagesTest.kt
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.reader.domain
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
-import org.koitharu.kotatsu.parsers.model.MangaSource
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import kotlin.random.Random
@@ -73,6 +73,6 @@ class ChapterPagesTest {
preview = null,
chapterId = chapterId,
index = Random.nextInt(),
- source = MangaSource.DUMMY,
+ source = MangaParserSource.DUMMY,
)
}
diff --git a/build.gradle b/build.gradle
index e4c4dcc09..90d839085 100644
--- a/build.gradle
+++ b/build.gradle
@@ -4,10 +4,10 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.5.0'
- classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.24'
+ classpath 'com.android.tools.build:gradle:8.5.1'
+ classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.10-RC'
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.51.1'
- classpath 'com.google.devtools.ksp:symbol-processing-gradle-plugin:1.9.24-1.0.20'
+ classpath 'com.google.devtools.ksp:symbol-processing-gradle-plugin:2.0.10-RC-1.0.23'
}
}