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' } }