(
get() = progress.value
fun progressAsFlow(): Flow = progress
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/progress/ProgressJob.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/ProgressJob.kt
similarity index 85%
rename from app/src/main/java/org/koitharu/kotatsu/utils/progress/ProgressJob.kt
rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/ProgressJob.kt
index 919d952ab..826916ddf 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/progress/ProgressJob.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/ProgressJob.kt
@@ -1,4 +1,4 @@
-package org.koitharu.kotatsu.utils.progress
+package org.koitharu.kotatsu.core.util.progress
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
@@ -13,4 +13,4 @@ open class ProgressJob
(
get() = progress.value
fun progressAsFlow(): Flow
= progress
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/progress/ProgressResponseBody.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/ProgressResponseBody.kt
similarity index 96%
rename from app/src/main/java/org/koitharu/kotatsu/utils/progress/ProgressResponseBody.kt
rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/ProgressResponseBody.kt
index 20327a272..b66e5cd2a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/progress/ProgressResponseBody.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/ProgressResponseBody.kt
@@ -1,4 +1,4 @@
-package org.koitharu.kotatsu.utils.progress
+package org.koitharu.kotatsu.core.util.progress
import kotlinx.coroutines.flow.MutableStateFlow
import okhttp3.MediaType
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/progress/TimeLeftEstimator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/TimeLeftEstimator.kt
similarity index 96%
rename from app/src/main/java/org/koitharu/kotatsu/utils/progress/TimeLeftEstimator.kt
rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/TimeLeftEstimator.kt
index 97b83f52d..e83507ef1 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/progress/TimeLeftEstimator.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/TimeLeftEstimator.kt
@@ -1,4 +1,4 @@
-package org.koitharu.kotatsu.utils.progress
+package org.koitharu.kotatsu.core.util.progress
import android.os.SystemClock
import java.util.concurrent.TimeUnit
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/zip/ZipOutput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt
similarity index 100%
rename from app/src/main/java/org/koitharu/kotatsu/core/zip/ZipOutput.kt
rename to app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/domain/BranchComparator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/BranchComparator.kt
similarity index 100%
rename from app/src/main/java/org/koitharu/kotatsu/details/domain/BranchComparator.kt
rename to app/src/main/kotlin/org/koitharu/kotatsu/details/domain/BranchComparator.kt
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsInteractor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsInteractor.kt
new file mode 100644
index 000000000..350c66772
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsInteractor.kt
@@ -0,0 +1,80 @@
+package org.koitharu.kotatsu.details.domain
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.core.prefs.observeAsFlow
+import org.koitharu.kotatsu.details.domain.model.DoubleManga
+import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
+import org.koitharu.kotatsu.history.data.HistoryRepository
+import org.koitharu.kotatsu.local.data.LocalMangaRepository
+import org.koitharu.kotatsu.local.domain.model.LocalManga
+import org.koitharu.kotatsu.parsers.model.Manga
+import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
+import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
+import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
+import org.koitharu.kotatsu.tracker.domain.TrackingRepository
+import javax.inject.Inject
+
+@Deprecated("")
+class DetailsInteractor @Inject constructor(
+ private val historyRepository: HistoryRepository,
+ private val favouritesRepository: FavouritesRepository,
+ private val localMangaRepository: LocalMangaRepository,
+ private val trackingRepository: TrackingRepository,
+ private val settings: AppSettings,
+ private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
+) {
+
+ fun observeIsFavourite(mangaId: Long): Flow {
+ return favouritesRepository.observeCategoriesIds(mangaId)
+ .map { it.isNotEmpty() }
+ }
+
+ fun observeNewChapters(mangaId: Long): Flow {
+ return settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled }
+ .flatMapLatest { isEnabled ->
+ if (isEnabled) {
+ trackingRepository.observeNewChaptersCount(mangaId)
+ } else {
+ flowOf(0)
+ }
+ }
+ }
+
+ fun observeScrobblingInfo(mangaId: Long): Flow> {
+ return combine(
+ scrobblers.map { it.observeScrobblingInfo(mangaId) },
+ ) { scrobblingInfo ->
+ scrobblingInfo.filterNotNull()
+ }
+ }
+
+ fun observeIncognitoMode(mangaFlow: Flow): Flow {
+ return mangaFlow
+ .distinctUntilChangedBy { it?.isNsfw }
+ .flatMapLatest { manga ->
+ if (manga != null) {
+ historyRepository.observeShouldSkip(manga)
+ } else {
+ settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled }
+ }
+ }
+ }
+
+ suspend fun updateLocal(subject: DoubleManga?, localManga: LocalManga): DoubleManga? {
+ return if (subject?.any?.id == localManga.manga.id) {
+ subject.copy(
+ localManga = runCatchingCancellable {
+ localMangaRepository.getDetails(localManga.manga)
+ },
+ )
+ } else {
+ subject
+ }
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DoubleMangaLoadUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DoubleMangaLoadUseCase.kt
new file mode 100644
index 000000000..143d1ae24
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DoubleMangaLoadUseCase.kt
@@ -0,0 +1,65 @@
+package org.koitharu.kotatsu.details.domain
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+import org.koitharu.kotatsu.core.model.isLocal
+import org.koitharu.kotatsu.core.parser.MangaDataRepository
+import org.koitharu.kotatsu.core.parser.MangaIntent
+import org.koitharu.kotatsu.core.parser.MangaRepository
+import org.koitharu.kotatsu.details.domain.model.DoubleManga
+import org.koitharu.kotatsu.local.data.LocalMangaRepository
+import org.koitharu.kotatsu.parsers.exception.NotFoundException
+import org.koitharu.kotatsu.parsers.model.Manga
+import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
+import javax.inject.Inject
+
+class DoubleMangaLoadUseCase @Inject constructor(
+ private val mangaDataRepository: MangaDataRepository,
+ private val localMangaRepository: LocalMangaRepository,
+ private val mangaRepositoryFactory: MangaRepository.Factory,
+) {
+
+ suspend operator fun invoke(manga: Manga): DoubleManga = coroutineScope {
+ val remoteDeferred = async(Dispatchers.Default) { loadRemote(manga) }
+ val localDeferred = async(Dispatchers.Default) { loadLocal(manga) }
+ DoubleManga(
+ remoteManga = remoteDeferred.await(),
+ localManga = localDeferred.await(),
+ )
+ }
+
+ suspend operator fun invoke(mangaId: Long): DoubleManga {
+ val manga = mangaDataRepository.findMangaById(mangaId) ?: throwNFE()
+ return invoke(manga)
+ }
+
+ suspend operator fun invoke(intent: MangaIntent): DoubleManga {
+ val manga = mangaDataRepository.resolveIntent(intent) ?: throwNFE()
+ return invoke(manga)
+ }
+
+ private suspend fun loadLocal(manga: Manga): Result? {
+ return runCatchingCancellable {
+ if (manga.isLocal) {
+ localMangaRepository.getDetails(manga)
+ } else {
+ localMangaRepository.findSavedManga(manga)?.manga
+ } ?: return null
+ }
+ }
+
+ private suspend fun loadRemote(manga: Manga): Result? {
+ return runCatchingCancellable {
+ val seed = if (manga.isLocal) {
+ localMangaRepository.getRemoteManga(manga)
+ } else {
+ manga
+ } ?: return null
+ val repository = mangaRepositoryFactory.create(seed.source)
+ repository.getDetails(seed)
+ }
+ }
+
+ private fun throwNFE(): Nothing = throw NotFoundException("Cannot find manga", "")
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/model/DoubleManga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/model/DoubleManga.kt
new file mode 100644
index 000000000..732f59902
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/model/DoubleManga.kt
@@ -0,0 +1,76 @@
+package org.koitharu.kotatsu.details.domain.model
+
+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.reader.data.filterChapters
+
+data class DoubleManga(
+ private val remoteManga: Result?,
+ private val localManga: Result?,
+) {
+
+ constructor(manga: Manga) : this(
+ remoteManga = if (manga.source != MangaSource.LOCAL) Result.success(manga) else null,
+ localManga = if (manga.source == MangaSource.LOCAL) Result.success(manga) else null,
+ )
+
+ val remote: Manga?
+ get() = remoteManga?.getOrNull()
+
+ val local: Manga?
+ get() = localManga?.getOrNull()
+
+ val any: Manga?
+ get() = remote ?: local
+
+ val hasRemote: Boolean
+ get() = remoteManga?.isSuccess == true
+
+ val hasLocal: Boolean
+ get() = localManga?.isSuccess == true
+
+ val chapters: List? by lazy(LazyThreadSafetyMode.PUBLICATION) {
+ mergeChapters()
+ }
+
+ fun requireAny(): Manga {
+ val result = remoteManga?.getOrNull() ?: localManga?.getOrNull()
+ if (result != null) {
+ return result
+ }
+ throw (
+ remoteManga?.exceptionOrNull()
+ ?: localManga?.exceptionOrNull()
+ ?: IllegalStateException("No online either local manga available")
+ )
+ }
+
+ fun filterChapters(branch: String?) = DoubleManga(
+ remoteManga?.map { it.filterChapters(branch) },
+ localManga?.map { it.filterChapters(branch) },
+ )
+
+ private fun mergeChapters(): List? {
+ val remoteChapters = remote?.chapters
+ val localChapters = local?.chapters
+ if (localChapters == null && remoteChapters == null) {
+ return null
+ }
+ val localMap = if (!localChapters.isNullOrEmpty()) {
+ localChapters.associateByTo(LinkedHashMap(localChapters.size)) { it.id }
+ } else {
+ null
+ }
+ val result = ArrayList(maxOf(remoteChapters?.size ?: 0, localChapters?.size ?: 0))
+ remoteChapters?.forEach { r ->
+ localMap?.remove(r.id)?.let { l ->
+ result.add(l)
+ } ?: result.add(r)
+ }
+ localMap?.values?.let {
+ result.addAll(it)
+ }
+ return result
+ }
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt
similarity index 92%
rename from app/src/main/java/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt
rename to app/src/main/kotlin/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt
index 993894bfd..6c483d475 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt
@@ -4,18 +4,18 @@ import android.content.Context
import android.content.Intent
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.EntryPointAccessors
-import org.koitharu.kotatsu.base.ui.CoroutineIntentService
import org.koitharu.kotatsu.core.cache.ContentCache
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaChapters
import org.koitharu.kotatsu.core.parser.MangaRepository
-import org.koitharu.kotatsu.history.domain.HistoryRepository
+import org.koitharu.kotatsu.core.ui.CoroutineIntentService
+import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat
+import org.koitharu.kotatsu.history.data.HistoryRepository
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.utils.ext.getParcelableExtraCompat
-import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
-import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
+import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
+import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import javax.inject.Inject
@AndroidEntryPoint
@@ -116,7 +116,7 @@ class MangaPrefetchService : CoroutineIntentService() {
return false
}
val entryPoint = EntryPointAccessors.fromApplication(context, PrefetchCompanionEntryPoint::class.java)
- return entryPoint.contentCache.isCachingEnabled && entryPoint.settings.isContentPrefetchEnabled()
+ return entryPoint.contentCache.isCachingEnabled && entryPoint.settings.isContentPrefetchEnabled
}
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/service/PrefetchCompanionEntryPoint.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/service/PrefetchCompanionEntryPoint.kt
similarity index 100%
rename from app/src/main/java/org/koitharu/kotatsu/details/service/PrefetchCompanionEntryPoint.kt
rename to app/src/main/kotlin/org/koitharu/kotatsu/details/service/PrefetchCompanionEntryPoint.kt
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ButtonTip.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ButtonTip.kt
new file mode 100644
index 000000000..cc6a94502
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ButtonTip.kt
@@ -0,0 +1,92 @@
+package org.koitharu.kotatsu.details.ui
+
+import android.transition.TransitionManager
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.graphics.Insets
+import androidx.core.view.setMargins
+import androidx.core.view.updateLayoutParams
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
+import org.koitharu.kotatsu.core.util.ext.getThemeDimensionPixelSize
+import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
+import org.koitharu.kotatsu.databinding.ItemTipBinding
+import com.google.android.material.R as materialR
+
+class ButtonTip(
+ private val root: ViewGroup,
+ private val insetsDelegate: WindowInsetsDelegate,
+ private val viewModel: DetailsViewModel,
+) : View.OnClickListener, WindowInsetsDelegate.WindowInsetsListener {
+
+ private var selfBinding = ItemTipBinding.inflate(LayoutInflater.from(root.context), root, false)
+ private val actionBarSize = root.context.getThemeDimensionPixelSize(materialR.attr.actionBarSize)
+
+ init {
+ selfBinding.textView.setText(R.string.details_button_tip)
+ selfBinding.imageViewIcon.setImageResource(R.drawable.ic_tap)
+ selfBinding.root.id = R.id.layout_tip
+ selfBinding.buttonClose.setOnClickListener(this)
+ }
+
+ override fun onClick(v: View?) {
+ remove()
+ }
+
+ override fun onWindowInsetsChanged(insets: Insets) {
+ if (root is CoordinatorLayout) {
+ selfBinding.root.updateLayoutParams