|
|
|
|
@ -1,131 +1,106 @@
|
|
|
|
|
package org.koitharu.kotatsu.details.ui
|
|
|
|
|
|
|
|
|
|
import androidx.core.os.LocaleListCompat
|
|
|
|
|
import androidx.lifecycle.asFlow
|
|
|
|
|
import androidx.lifecycle.asLiveData
|
|
|
|
|
import androidx.lifecycle.liveData
|
|
|
|
|
import androidx.lifecycle.viewModelScope
|
|
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
|
|
import kotlinx.coroutines.Job
|
|
|
|
|
import java.io.IOException
|
|
|
|
|
import java.util.*
|
|
|
|
|
import kotlinx.coroutines.*
|
|
|
|
|
import kotlinx.coroutines.flow.*
|
|
|
|
|
import kotlinx.coroutines.launch
|
|
|
|
|
import kotlinx.coroutines.plus
|
|
|
|
|
import org.koitharu.kotatsu.BuildConfig
|
|
|
|
|
import org.koitharu.kotatsu.R
|
|
|
|
|
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
|
|
|
|
import org.koitharu.kotatsu.base.domain.MangaIntent
|
|
|
|
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
|
|
|
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
|
|
|
|
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
|
|
|
|
import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException
|
|
|
|
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
|
|
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
|
|
|
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
|
|
|
|
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
|
|
|
|
import org.koitharu.kotatsu.details.ui.model.toListItem
|
|
|
|
|
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
|
|
|
|
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
|
|
|
|
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
|
|
|
|
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.util.mapToSet
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
|
|
|
|
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
|
|
|
|
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
|
|
|
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
|
|
|
|
import org.koitharu.kotatsu.utils.ext.iterator
|
|
|
|
|
import java.io.IOException
|
|
|
|
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
|
|
|
|
|
|
|
|
|
class DetailsViewModel(
|
|
|
|
|
private val intent: MangaIntent,
|
|
|
|
|
intent: MangaIntent,
|
|
|
|
|
private val historyRepository: HistoryRepository,
|
|
|
|
|
private val favouritesRepository: FavouritesRepository,
|
|
|
|
|
favouritesRepository: FavouritesRepository,
|
|
|
|
|
private val localMangaRepository: LocalMangaRepository,
|
|
|
|
|
private val trackingRepository: TrackingRepository,
|
|
|
|
|
private val mangaDataRepository: MangaDataRepository,
|
|
|
|
|
mangaDataRepository: MangaDataRepository,
|
|
|
|
|
private val bookmarksRepository: BookmarksRepository,
|
|
|
|
|
private val settings: AppSettings,
|
|
|
|
|
) : BaseViewModel() {
|
|
|
|
|
|
|
|
|
|
private val delegate = MangaDetailsDelegate(
|
|
|
|
|
intent = intent,
|
|
|
|
|
settings = settings,
|
|
|
|
|
mangaDataRepository = mangaDataRepository,
|
|
|
|
|
historyRepository = historyRepository,
|
|
|
|
|
localMangaRepository = localMangaRepository,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
private var loadingJob: Job
|
|
|
|
|
private val mangaData = MutableStateFlow(intent.manga)
|
|
|
|
|
private val selectedBranch = MutableStateFlow<String?>(null)
|
|
|
|
|
|
|
|
|
|
val onShowToast = SingleLiveEvent<Int>()
|
|
|
|
|
|
|
|
|
|
private val history = mangaData.mapNotNull { it?.id }
|
|
|
|
|
.distinctUntilChanged()
|
|
|
|
|
.flatMapLatest { mangaId ->
|
|
|
|
|
historyRepository.observeOne(mangaId)
|
|
|
|
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
|
|
|
|
private val history = historyRepository.observeOne(delegate.mangaId)
|
|
|
|
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
|
|
|
|
|
|
|
|
|
private val favourite = mangaData.mapNotNull { it?.id }
|
|
|
|
|
.distinctUntilChanged()
|
|
|
|
|
.flatMapLatest { mangaId ->
|
|
|
|
|
favouritesRepository.observeCategoriesIds(mangaId).map { it.isNotEmpty() }
|
|
|
|
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
|
|
|
|
private val favourite = favouritesRepository.observeCategoriesIds(delegate.mangaId).map { it.isNotEmpty() }
|
|
|
|
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
|
|
|
|
|
|
|
|
|
private val newChapters = mangaData.mapNotNull { it?.id }
|
|
|
|
|
.distinctUntilChanged()
|
|
|
|
|
.mapLatest { mangaId ->
|
|
|
|
|
trackingRepository.getNewChaptersCount(mangaId)
|
|
|
|
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
|
|
|
|
|
private val newChapters = viewModelScope.async(Dispatchers.Default) {
|
|
|
|
|
trackingRepository.getNewChaptersCount(delegate.mangaId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remote manga for saved and saved for remote
|
|
|
|
|
private val relatedManga = MutableStateFlow<Manga?>(null)
|
|
|
|
|
private val chaptersQuery = MutableStateFlow("")
|
|
|
|
|
|
|
|
|
|
private val chaptersReversed = settings.observe()
|
|
|
|
|
.filter { it == AppSettings.KEY_REVERSE_CHAPTERS }
|
|
|
|
|
.map { settings.chaptersReverse }
|
|
|
|
|
.onStart { emit(settings.chaptersReverse) }
|
|
|
|
|
private val chaptersReversed = settings.observeAsFlow(AppSettings.KEY_REVERSE_CHAPTERS) { chaptersReverse }
|
|
|
|
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
|
|
|
|
|
|
|
|
|
val manga = mangaData.filterNotNull()
|
|
|
|
|
.asLiveData(viewModelScope.coroutineContext)
|
|
|
|
|
val favouriteCategories = favourite
|
|
|
|
|
.asLiveData(viewModelScope.coroutineContext)
|
|
|
|
|
val newChaptersCount = newChapters
|
|
|
|
|
.asLiveData(viewModelScope.coroutineContext)
|
|
|
|
|
val readingHistory = history
|
|
|
|
|
.asLiveData(viewModelScope.coroutineContext)
|
|
|
|
|
val isChaptersReversed = chaptersReversed
|
|
|
|
|
.asLiveData(viewModelScope.coroutineContext)
|
|
|
|
|
val manga = delegate.manga.filterNotNull().asLiveData(viewModelScope.coroutineContext)
|
|
|
|
|
val favouriteCategories = favourite.asLiveData(viewModelScope.coroutineContext)
|
|
|
|
|
val newChaptersCount = liveData(viewModelScope.coroutineContext) { emit(newChapters.await()) }
|
|
|
|
|
val readingHistory = history.asLiveData(viewModelScope.coroutineContext)
|
|
|
|
|
val isChaptersReversed = chaptersReversed.asLiveData(viewModelScope.coroutineContext)
|
|
|
|
|
|
|
|
|
|
val bookmarks = mangaData.flatMapLatest {
|
|
|
|
|
val bookmarks = delegate.manga.flatMapLatest {
|
|
|
|
|
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
|
|
|
|
|
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
|
|
|
|
|
|
|
|
|
|
val onMangaRemoved = SingleLiveEvent<Manga>()
|
|
|
|
|
|
|
|
|
|
val branches = mangaData.map {
|
|
|
|
|
it?.chapters?.mapToSet { x -> x.branch }?.sortedBy { x -> x }.orEmpty()
|
|
|
|
|
val branches = delegate.manga.map {
|
|
|
|
|
val chapters = it?.chapters ?: return@map emptySet()
|
|
|
|
|
chapters.mapTo(TreeSet()) { x -> x.branch }
|
|
|
|
|
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
|
|
|
|
|
|
|
|
|
|
val selectedBranchIndex = combine(
|
|
|
|
|
branches.asFlow(),
|
|
|
|
|
selectedBranch
|
|
|
|
|
delegate.selectedBranch
|
|
|
|
|
) { branches, selected ->
|
|
|
|
|
branches.indexOf(selected)
|
|
|
|
|
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
|
|
|
|
|
|
|
|
|
|
val isChaptersEmpty = mangaData.mapNotNull { m ->
|
|
|
|
|
m?.run { chapters.isNullOrEmpty() }
|
|
|
|
|
val isChaptersEmpty = delegate.manga.map { m ->
|
|
|
|
|
m?.chapters?.isEmpty() == true
|
|
|
|
|
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, false)
|
|
|
|
|
|
|
|
|
|
val chapters = combine(
|
|
|
|
|
combine(
|
|
|
|
|
mangaData.map { it?.chapters.orEmpty() },
|
|
|
|
|
relatedManga,
|
|
|
|
|
history.map { it?.chapterId },
|
|
|
|
|
newChapters,
|
|
|
|
|
selectedBranch
|
|
|
|
|
) { chapters, related, currentId, newCount, branch ->
|
|
|
|
|
val relatedChapters = related?.chapters
|
|
|
|
|
if (related?.source != MangaSource.LOCAL && !relatedChapters.isNullOrEmpty()) {
|
|
|
|
|
mapChaptersWithSource(chapters, relatedChapters, currentId, newCount, branch)
|
|
|
|
|
} else {
|
|
|
|
|
mapChapters(chapters, relatedChapters, currentId, newCount, branch)
|
|
|
|
|
}
|
|
|
|
|
delegate.manga,
|
|
|
|
|
delegate.relatedManga,
|
|
|
|
|
history,
|
|
|
|
|
delegate.selectedBranch,
|
|
|
|
|
) { manga, related, history, branch ->
|
|
|
|
|
delegate.mapChapters(manga, related, history, newChapters.await(), branch)
|
|
|
|
|
},
|
|
|
|
|
chaptersReversed,
|
|
|
|
|
chaptersQuery,
|
|
|
|
|
@ -134,7 +109,7 @@ class DetailsViewModel(
|
|
|
|
|
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
|
|
|
|
|
|
|
|
|
val selectedBranchValue: String?
|
|
|
|
|
get() = selectedBranch.value
|
|
|
|
|
get() = delegate.selectedBranch.value
|
|
|
|
|
|
|
|
|
|
init {
|
|
|
|
|
loadingJob = doLoad()
|
|
|
|
|
@ -146,7 +121,11 @@ class DetailsViewModel(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun deleteLocal() {
|
|
|
|
|
val m = mangaData.value ?: return
|
|
|
|
|
val m = delegate.manga.value
|
|
|
|
|
if (m == null) {
|
|
|
|
|
onShowToast.call(R.string.file_not_found)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
launchLoadingJob(Dispatchers.Default) {
|
|
|
|
|
val manga = if (m.source == MangaSource.LOCAL) m else localMangaRepository.findSavedManga(m)
|
|
|
|
|
checkNotNull(manga) { "Cannot find saved manga for ${m.title}" }
|
|
|
|
|
@ -171,11 +150,11 @@ class DetailsViewModel(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun setSelectedBranch(branch: String?) {
|
|
|
|
|
selectedBranch.value = branch
|
|
|
|
|
delegate.selectedBranch.value = branch
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getRemoteManga(): Manga? {
|
|
|
|
|
return relatedManga.value?.takeUnless { it.source == MangaSource.LOCAL }
|
|
|
|
|
return delegate.relatedManga.value?.takeUnless { it.source == MangaSource.LOCAL }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun performChapterSearch(query: String?) {
|
|
|
|
|
@ -183,7 +162,7 @@ class DetailsViewModel(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun onDownloadComplete(downloadedManga: Manga) {
|
|
|
|
|
val currentManga = mangaData.value ?: return
|
|
|
|
|
val currentManga = delegate.manga.value ?: return
|
|
|
|
|
if (currentManga.id != downloadedManga.id) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
@ -194,142 +173,16 @@ class DetailsViewModel(
|
|
|
|
|
runCatching {
|
|
|
|
|
localMangaRepository.getDetails(downloadedManga)
|
|
|
|
|
}.onSuccess {
|
|
|
|
|
relatedManga.value = it
|
|
|
|
|
delegate.relatedManga.value = it
|
|
|
|
|
}.onFailure {
|
|
|
|
|
if (BuildConfig.DEBUG) {
|
|
|
|
|
it.printStackTrace()
|
|
|
|
|
}
|
|
|
|
|
it.printStackTraceDebug()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
|
|
|
|
|
var manga = mangaDataRepository.resolveIntent(intent)
|
|
|
|
|
?: throw MangaNotFoundException("Cannot find manga")
|
|
|
|
|
mangaData.value = manga
|
|
|
|
|
manga = MangaRepository(manga.source).getDetails(manga)
|
|
|
|
|
// find default branch
|
|
|
|
|
val hist = historyRepository.getOne(manga)
|
|
|
|
|
selectedBranch.value = if (hist != null) {
|
|
|
|
|
val currentChapter = manga.chapters?.find { it.id == hist.chapterId }
|
|
|
|
|
if (currentChapter != null) currentChapter.branch else predictBranch(manga.chapters)
|
|
|
|
|
} else {
|
|
|
|
|
predictBranch(manga.chapters)
|
|
|
|
|
}
|
|
|
|
|
mangaData.value = manga
|
|
|
|
|
relatedManga.value = runCatching {
|
|
|
|
|
if (manga.source == MangaSource.LOCAL) {
|
|
|
|
|
val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null
|
|
|
|
|
MangaRepository(m.source).getDetails(m)
|
|
|
|
|
} else {
|
|
|
|
|
localMangaRepository.findSavedManga(manga)
|
|
|
|
|
}
|
|
|
|
|
}.onFailure { error ->
|
|
|
|
|
if (BuildConfig.DEBUG) error.printStackTrace()
|
|
|
|
|
}.getOrNull()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun mapChapters(
|
|
|
|
|
chapters: List<MangaChapter>,
|
|
|
|
|
downloadedChapters: List<MangaChapter>?,
|
|
|
|
|
currentId: Long?,
|
|
|
|
|
newCount: Int,
|
|
|
|
|
branch: String?,
|
|
|
|
|
): List<ChapterListItem> {
|
|
|
|
|
val result = ArrayList<ChapterListItem>(chapters.size)
|
|
|
|
|
val dateFormat = settings.getDateFormat()
|
|
|
|
|
val currentIndex = chapters.indexOfFirst { it.id == currentId }
|
|
|
|
|
val firstNewIndex = chapters.size - newCount
|
|
|
|
|
val downloadedIds = downloadedChapters?.mapToSet { it.id }
|
|
|
|
|
for (i in chapters.indices) {
|
|
|
|
|
val chapter = chapters[i]
|
|
|
|
|
if (chapter.branch != branch) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
result += chapter.toListItem(
|
|
|
|
|
isCurrent = i == currentIndex,
|
|
|
|
|
isUnread = i > currentIndex,
|
|
|
|
|
isNew = i >= firstNewIndex,
|
|
|
|
|
isMissing = false,
|
|
|
|
|
isDownloaded = downloadedIds?.contains(chapter.id) == true,
|
|
|
|
|
dateFormat = dateFormat,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun mapChaptersWithSource(
|
|
|
|
|
chapters: List<MangaChapter>,
|
|
|
|
|
sourceChapters: List<MangaChapter>,
|
|
|
|
|
currentId: Long?,
|
|
|
|
|
newCount: Int,
|
|
|
|
|
branch: String?,
|
|
|
|
|
): List<ChapterListItem> {
|
|
|
|
|
val chaptersMap = chapters.associateByTo(HashMap(chapters.size)) { it.id }
|
|
|
|
|
val result = ArrayList<ChapterListItem>(sourceChapters.size)
|
|
|
|
|
val currentIndex = sourceChapters.indexOfFirst { it.id == currentId }
|
|
|
|
|
val firstNewIndex = sourceChapters.size - newCount
|
|
|
|
|
val dateFormat = settings.getDateFormat()
|
|
|
|
|
for (i in sourceChapters.indices) {
|
|
|
|
|
val chapter = sourceChapters[i]
|
|
|
|
|
val localChapter = chaptersMap.remove(chapter.id)
|
|
|
|
|
if (chapter.branch != branch) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
result += localChapter?.toListItem(
|
|
|
|
|
isCurrent = i == currentIndex,
|
|
|
|
|
isUnread = i > currentIndex,
|
|
|
|
|
isNew = i >= firstNewIndex,
|
|
|
|
|
isMissing = false,
|
|
|
|
|
isDownloaded = false,
|
|
|
|
|
dateFormat = dateFormat,
|
|
|
|
|
) ?: chapter.toListItem(
|
|
|
|
|
isCurrent = i == currentIndex,
|
|
|
|
|
isUnread = i > currentIndex,
|
|
|
|
|
isNew = i >= firstNewIndex,
|
|
|
|
|
isMissing = true,
|
|
|
|
|
isDownloaded = false,
|
|
|
|
|
dateFormat = dateFormat,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if (chaptersMap.isNotEmpty()) { // some chapters on device but not online source
|
|
|
|
|
result.ensureCapacity(result.size + chaptersMap.size)
|
|
|
|
|
chaptersMap.values.mapNotNullTo(result) {
|
|
|
|
|
if (it.branch == branch) {
|
|
|
|
|
it.toListItem(
|
|
|
|
|
isCurrent = false,
|
|
|
|
|
isUnread = true,
|
|
|
|
|
isNew = false,
|
|
|
|
|
isMissing = false,
|
|
|
|
|
isDownloaded = false,
|
|
|
|
|
dateFormat = dateFormat,
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
result.sortBy { it.chapter.number }
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun predictBranch(chapters: List<MangaChapter>?): String? {
|
|
|
|
|
if (chapters.isNullOrEmpty()) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
val groups = chapters.groupBy { it.branch }
|
|
|
|
|
for (locale in LocaleListCompat.getAdjustedDefault()) {
|
|
|
|
|
var language = locale.getDisplayLanguage(locale).toTitleCase(locale)
|
|
|
|
|
if (groups.containsKey(language)) {
|
|
|
|
|
return language
|
|
|
|
|
}
|
|
|
|
|
language = locale.getDisplayName(locale).toTitleCase(locale)
|
|
|
|
|
if (groups.containsKey(language)) {
|
|
|
|
|
return language
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return groups.maxByOrNull { it.value.size }?.key
|
|
|
|
|
delegate.doLoad()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun List<ChapterListItem>.filterSearch(query: String): List<ChapterListItem> {
|
|
|
|
|
|