Refactor chapters mapping

pull/377/head
Koitharu 3 years ago
parent 84567767a0
commit a8f5714b35
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -32,6 +32,9 @@ class ChaptersBottomSheetMediator(
override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) { override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) {
isEnabled = isExpanded isEnabled = isExpanded
if (!isExpanded) {
unlock()
}
} }
override fun onLayoutChange( override fun onLayoutChange(

@ -73,10 +73,6 @@ class ChaptersFragment :
if (selectionController?.onItemClick(item.chapter.id) == true) { if (selectionController?.onItemClick(item.chapter.id) == true) {
return return
} }
if (item.hasFlag(ChapterListItem.FLAG_MISSING)) {
(activity as? DetailsActivity)?.showChapterMissingDialog(item.chapter.id)
return
}
startActivity( startActivity(
ReaderActivity.newIntent( ReaderActivity.newIntent(
context = view.context, context = view.context,
@ -193,7 +189,7 @@ class ChaptersFragment :
private fun onChaptersChanged(list: List<ChapterListItem>) { private fun onChaptersChanged(list: List<ChapterListItem>) {
val adapter = chaptersAdapter ?: return val adapter = chaptersAdapter ?: return
if (adapter.itemCount == 0) { if (adapter.itemCount == 0) {
val position = list.indexOfFirst { it.hasFlag(ChapterListItem.FLAG_CURRENT) } - 1 val position = list.indexOfFirst { it.isCurrent } - 1
if (position > 0) { if (position > 0) {
val offset = (resources.getDimensionPixelSize(R.dimen.chapter_list_item_height) * 0.6).roundToInt() val offset = (resources.getDimensionPixelSize(R.dimen.chapter_list_item_height) * 0.6).roundToInt()
adapter.setItems( adapter.setItems(

@ -0,0 +1,60 @@
package org.koitharu.kotatsu.details.ui
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.toListItem
import org.koitharu.kotatsu.parsers.model.Manga
fun mapChapters(
remoteManga: Manga?,
localManga: Manga?,
history: MangaHistory?,
newCount: Int,
branch: String?,
): List<ChapterListItem> {
val remoteChapters = remoteManga?.getChapters(branch).orEmpty()
val localChapters = localManga?.getChapters(branch).orEmpty()
if (remoteChapters.isEmpty() && localChapters.isEmpty()) {
return emptyList()
}
val currentId = history?.chapterId ?: 0L
val newFrom = if (newCount == 0 || remoteChapters.isEmpty()) Int.MAX_VALUE else remoteChapters.size - newCount
val chaptersSize = maxOf(remoteChapters.size, localChapters.size)
val ids = buildSet(chaptersSize) {
remoteChapters.mapTo(this) { it.id }
localChapters.mapTo(this) { it.id }
}
val result = ArrayList<ChapterListItem>(chaptersSize)
val localMap = if (localChapters.isNotEmpty()) {
localChapters.associateByTo(LinkedHashMap(localChapters.size)) { it.id }
} else {
null
}
var isUnread = currentId !in ids
for (chapter in remoteChapters) {
val local = localMap?.remove(chapter.id)
if (chapter.id == currentId) {
isUnread = true
}
result += chapter.toListItem(
isCurrent = chapter.id == currentId,
isUnread = isUnread,
isNew = isUnread && result.size >= newFrom,
isDownloaded = local != null,
)
}
if (!localMap.isNullOrEmpty()) {
for (chapter in localMap.values) {
if (chapter.id == currentId) {
isUnread = true
}
result += chapter.toListItem(
isCurrent = chapter.id == currentId,
isUnread = isUnread,
isNew = false,
isDownloaded = false,
)
}
}
return result
}

@ -19,7 +19,6 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -45,7 +44,6 @@ import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
import javax.inject.Inject import javax.inject.Inject
@ -244,33 +242,6 @@ class DetailsActivity :
viewBadge.counter = newChapters viewBadge.counter = newChapters
} }
fun showChapterMissingDialog(chapterId: Long) {
val remoteManga = viewModel.getRemoteManga()
if (remoteManga == null) {
val snackbar = makeSnackbar(getString(R.string.chapter_is_missing), Snackbar.LENGTH_SHORT)
snackbar.show()
return
}
MaterialAlertDialogBuilder(this).apply {
setMessage(R.string.chapter_is_missing_text)
setTitle(R.string.chapter_is_missing)
setNegativeButton(android.R.string.cancel, null)
setPositiveButton(R.string.read) { _, _ ->
startActivity(
ReaderActivity.newIntent(
context = this@DetailsActivity,
manga = remoteManga,
state = ReaderState(chapterId, 0, 0),
),
)
}
setNeutralButton(R.string.download) { _, _ ->
viewModel.download(setOf(chapterId))
}
setCancelable(true)
}.show()
}
private fun showBranchPopupMenu() { private fun showBranchPopupMenu() {
var dialog: DialogInterface? = null var dialog: DialogInterface? = null
val listener = OnListItemClickListener<MangaBranch> { item, _ -> val listener = OnListItemClickListener<MangaBranch> { item, _ ->
@ -291,7 +262,8 @@ class DetailsActivity :
val manga = viewModel.manga.value ?: return val manga = viewModel.manga.value ?: return
val chapterId = viewModel.historyInfo.value?.history?.chapterId val chapterId = viewModel.historyInfo.value?.history?.chapterId
if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) { if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) {
showChapterMissingDialog(chapterId) val snackbar = makeSnackbar(getString(R.string.chapter_is_missing), Snackbar.LENGTH_SHORT)
snackbar.show()
} else { } else {
startActivity( startActivity(
ReaderActivity.newIntent( ReaderActivity.newIntent(
@ -339,7 +311,7 @@ class DetailsActivity :
} }
if (!isCalled) { if (!isCalled) {
isCalled = true isCalled = true
val item = value.find { it.hasFlag(ChapterListItem.FLAG_CURRENT) } ?: value.first() val item = value.find { it.isCurrent } ?: value.first()
MangaPrefetchService.prefetchPages(context, item.chapter) MangaPrefetchService.prefetchPages(context, item.chapter)
} }
} }

@ -78,6 +78,13 @@ class DetailsViewModel @Inject constructor(
val onShowToast = SingleLiveEvent<Int>() val onShowToast = SingleLiveEvent<Int>()
val onDownloadStarted = SingleLiveEvent<Unit>() val onDownloadStarted = SingleLiveEvent<Unit>()
private val mangaData = combine(
delegate.onlineManga,
delegate.localManga,
) { o, l ->
o ?: l
}.stateIn(viewModelScope, SharingStarted.Lazily, null)
private val history = historyRepository.observeOne(delegate.mangaId) private val history = historyRepository.observeOne(delegate.mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null) .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
@ -98,16 +105,16 @@ class DetailsViewModel @Inject constructor(
private val chaptersReversed = settings.observeAsFlow(AppSettings.KEY_REVERSE_CHAPTERS) { chaptersReverse } private val chaptersReversed = settings.observeAsFlow(AppSettings.KEY_REVERSE_CHAPTERS) { chaptersReverse }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false) .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
val manga = delegate.manga.filterNotNull().asLiveData(viewModelScope.coroutineContext) val manga = mangaData.filterNotNull().asLiveData(viewModelScope.coroutineContext)
val favouriteCategories = favourite.asLiveData(viewModelScope.coroutineContext) val favouriteCategories = favourite.asLiveData(viewModelScope.coroutineContext)
val newChaptersCount = newChapters.asLiveData(viewModelScope.coroutineContext) val newChaptersCount = newChapters.asLiveData(viewModelScope.coroutineContext)
val isChaptersReversed = chaptersReversed.asLiveData(viewModelScope.coroutineContext) val isChaptersReversed = chaptersReversed.asLiveData(viewModelScope.coroutineContext)
val historyInfo: LiveData<HistoryInfo> = combine( val historyInfo: LiveData<HistoryInfo> = combine(
delegate.manga, mangaData,
delegate.selectedBranch, delegate.selectedBranch,
history, history,
historyRepository.observeShouldSkip(delegate.manga), historyRepository.observeShouldSkip(mangaData),
) { m, b, h, im -> ) { m, b, h, im ->
HistoryInfo(m, b, h, im) HistoryInfo(m, b, h, im)
}.asFlowLiveData( }.asFlowLiveData(
@ -115,28 +122,21 @@ class DetailsViewModel @Inject constructor(
defaultValue = HistoryInfo(null, null, null, false), defaultValue = HistoryInfo(null, null, null, false),
) )
val bookmarks = delegate.manga.flatMapLatest { val bookmarks = mangaData.flatMapLatest {
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList()) if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
val localSize = combine( val localSize = delegate.localManga
delegate.manga, .map {
delegate.relatedManga, if (it != null) {
) { m1, m2 -> val file = it.url.toUri().toFileOrNull()
val url = when {
m1?.source == MangaSource.LOCAL -> m1.url
m2?.source == MangaSource.LOCAL -> m2.url
else -> null
}
if (url != null) {
val file = url.toUri().toFileOrNull()
file?.computeSize() ?: 0L file?.computeSize() ?: 0L
} else { } else {
0L 0L
} }
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, 0) }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, 0)
val description = delegate.manga val description = mangaData
.distinctUntilChangedBy { it?.description.orEmpty() } .distinctUntilChangedBy { it?.description.orEmpty() }
.transformLatest { .transformLatest {
val description = it?.description val description = it?.description
@ -159,10 +159,12 @@ class DetailsViewModel @Inject constructor(
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
val branches: LiveData<List<MangaBranch>> = combine( val branches: LiveData<List<MangaBranch>> = combine(
delegate.manga, delegate.onlineManga,
delegate.localManga,
delegate.selectedBranch, delegate.selectedBranch,
) { m, b -> ) { m, l, b ->
val chapters = m?.chapters ?: return@combine emptyList() val chapters = concat(m?.chapters, l?.chapters)
if (chapters.isEmpty()) return@combine emptyList()
chapters.groupBy { x -> x.branch } chapters.groupBy { x -> x.branch }
.map { x -> MangaBranch(x.key, x.value.size, x.key == b) } .map { x -> MangaBranch(x.key, x.value.size, x.key == b) }
.sortedWith(BranchComparator()) .sortedWith(BranchComparator())
@ -172,21 +174,24 @@ class DetailsViewModel @Inject constructor(
.asFlowLiveData(viewModelScope.coroutineContext, null) .asFlowLiveData(viewModelScope.coroutineContext, null)
val isChaptersEmpty: LiveData<Boolean> = combine( val isChaptersEmpty: LiveData<Boolean> = combine(
delegate.manga, delegate.onlineManga,
delegate.localManga,
isLoading.asFlow(), isLoading.asFlow(),
) { m, loading -> ) { manga, local, loading ->
m != null && m.chapters.isNullOrEmpty() && !loading (manga != null && manga.chapters.isNullOrEmpty()) &&
(local != null && local.chapters.isNullOrEmpty()) &&
!loading
}.asFlowLiveData(viewModelScope.coroutineContext, false) }.asFlowLiveData(viewModelScope.coroutineContext, false)
val chapters = combine( val chapters = combine(
combine( combine(
delegate.manga, delegate.onlineManga,
delegate.relatedManga, delegate.localManga,
history, history,
delegate.selectedBranch, delegate.selectedBranch,
newChapters, newChapters,
) { manga, related, history, branch, news -> ) { manga, local, history, branch, news ->
delegate.mapChapters(manga, related, history, news, branch) mapChapters(manga, local, history, news, branch)
}, },
chaptersReversed, chaptersReversed,
chaptersQuery, chaptersQuery,
@ -211,7 +216,7 @@ class DetailsViewModel @Inject constructor(
} }
fun deleteLocal() { fun deleteLocal() {
val m = delegate.manga.value val m = delegate.localManga.value
if (m == null) { if (m == null) {
onShowToast.call(R.string.file_not_found) onShowToast.call(R.string.file_not_found)
return return
@ -244,7 +249,7 @@ class DetailsViewModel @Inject constructor(
} }
fun getRemoteManga(): Manga? { fun getRemoteManga(): Manga? {
return delegate.relatedManga.value?.takeUnless { it.source == MangaSource.LOCAL } return delegate.onlineManga.value
} }
fun performChapterSearch(query: String?) { fun performChapterSearch(query: String?) {
@ -274,7 +279,7 @@ class DetailsViewModel @Inject constructor(
fun markChapterAsCurrent(chapterId: Long) { fun markChapterAsCurrent(chapterId: Long) {
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
val manga = checkNotNull(delegate.manga.value) val manga = checkNotNull(mangaData.value)
val chapters = checkNotNull(manga.getChapters(selectedBranchValue)) val chapters = checkNotNull(manga.getChapters(selectedBranchValue))
val chapterIndex = chapters.indexOfFirst { it.id == chapterId } val chapterIndex = chapters.indexOfFirst { it.id == chapterId }
check(chapterIndex in chapters.indices) { "Chapter not found" } check(chapterIndex in chapters.indices) { "Chapter not found" }
@ -286,7 +291,7 @@ class DetailsViewModel @Inject constructor(
fun download(chaptersIds: Set<Long>?) { fun download(chaptersIds: Set<Long>?) {
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
downloadScheduler.schedule( downloadScheduler.schedule(
getRemoteManga() ?: checkNotNull(manga.value), delegate.onlineManga.value ?: checkNotNull(manga.value),
chaptersIds, chaptersIds,
) )
onDownloadStarted.emitCall(Unit) onDownloadStarted.emitCall(Unit)
@ -308,7 +313,7 @@ class DetailsViewModel @Inject constructor(
private suspend fun onDownloadComplete(downloadedManga: LocalManga?) { private suspend fun onDownloadComplete(downloadedManga: LocalManga?) {
downloadedManga ?: return downloadedManga ?: return
val currentManga = delegate.manga.value ?: return val currentManga = mangaData.value ?: return
if (currentManga.id != downloadedManga.manga.id) { if (currentManga.id != downloadedManga.manga.id) {
return return
} }
@ -319,7 +324,7 @@ class DetailsViewModel @Inject constructor(
runCatchingCancellable { runCatchingCancellable {
localMangaRepository.getDetails(downloadedManga.manga) localMangaRepository.getDetails(downloadedManga.manga)
}.onSuccess { }.onSuccess {
delegate.relatedManga.value = it delegate.publishManga(it)
}.onFailure { }.onFailure {
it.printStackTraceDebug() it.printStackTraceDebug()
} }
@ -348,4 +353,18 @@ class DetailsViewModel @Inject constructor(
} }
return scrobbler return scrobbler
} }
private fun <T> concat(a: List<T>?, b: List<T>?): List<T> {
return when {
a == null && b == null -> emptyList<T>()
a == null && b != null -> b
a != null && b == null -> a
a != null && b != null -> buildList<T>(a.size + b.size) {
addAll(a)
addAll(b)
}
else -> error("This shouldn't have happened")
}
}
} }

@ -1,21 +1,23 @@
package org.koitharu.kotatsu.details.ui package org.koitharu.kotatsu.details.ui
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.ViewModelLifecycle
import dagger.hilt.android.scopes.ViewModelScoped import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.SharingStarted
import org.koitharu.kotatsu.core.model.MangaHistory import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import org.koitharu.kotatsu.core.model.getPreferredBranch import org.koitharu.kotatsu.core.model.getPreferredBranch
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
import org.koitharu.kotatsu.details.ui.model.toListItem
import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.util.ext.printStackTraceDebug
@ -24,31 +26,44 @@ import javax.inject.Inject
@ViewModelScoped @ViewModelScoped
class MangaDetailsDelegate @Inject constructor( class MangaDetailsDelegate @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
lifecycle: ViewModelLifecycle,
private val mangaDataRepository: MangaDataRepository, private val mangaDataRepository: MangaDataRepository,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val localMangaRepository: LocalMangaRepository, private val localMangaRepository: LocalMangaRepository,
private val mangaRepositoryFactory: MangaRepository.Factory, private val mangaRepositoryFactory: MangaRepository.Factory,
networkState: NetworkState,
) { ) {
private val viewModelScope = RetainedLifecycleCoroutineScope(lifecycle)
private val intent = MangaIntent(savedStateHandle) private val intent = MangaIntent(savedStateHandle)
private val mangaData = MutableStateFlow(intent.manga) private val onlineMangaStateFlow = MutableStateFlow<Manga?>(null)
private val localMangaStateFlow = MutableStateFlow<Manga?>(null)
val selectedBranch = MutableStateFlow<String?>(null) val onlineManga = combine(
onlineMangaStateFlow,
networkState,
) { m, s -> m.takeIf { s } }
.stateIn(viewModelScope, SharingStarted.Lazily, null)
val localManga = localMangaStateFlow.asStateFlow()
// Remote manga for saved and saved for remote val selectedBranch = MutableStateFlow<String?>(null)
val relatedManga = MutableStateFlow<Manga?>(null)
val manga: StateFlow<Manga?>
get() = mangaData
val mangaId = intent.manga?.id ?: intent.mangaId val mangaId = intent.manga?.id ?: intent.mangaId
init {
intent.manga?.let {
publishManga(it)
}
}
suspend fun doLoad() { suspend fun doLoad() {
var manga = mangaDataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", "") var manga = mangaDataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", "")
mangaData.value = manga publishManga(manga)
manga = mangaRepositoryFactory.create(manga.source).getDetails(manga) manga = mangaRepositoryFactory.create(manga.source).getDetails(manga)
// find default branch // find default branch
val hist = historyRepository.getOne(manga) val hist = historyRepository.getOne(manga)
selectedBranch.value = manga.getPreferredBranch(hist) selectedBranch.value = manga.getPreferredBranch(hist)
mangaData.value = manga publishManga(manga)
relatedManga.value = runCatchingCancellable { runCatchingCancellable {
if (manga.source == MangaSource.LOCAL) { if (manga.source == MangaSource.LOCAL) {
val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatchingCancellable null val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatchingCancellable null
mangaRepositoryFactory.create(m.source).getDetails(m) mangaRepositoryFactory.create(m.source).getDetails(m)
@ -57,106 +72,18 @@ class MangaDetailsDelegate @Inject constructor(
} }
}.onFailure { error -> }.onFailure { error ->
error.printStackTraceDebug() error.printStackTraceDebug()
}.getOrNull() }.onSuccess {
} if (it != null) {
publishManga(it)
fun mapChapters(
manga: Manga?,
related: Manga?,
history: MangaHistory?,
newCount: Int,
branch: String?,
): List<ChapterListItem> {
val chapters = manga?.chapters ?: return emptyList()
val relatedChapters = related?.chapters
return if (related?.source != MangaSource.LOCAL && !relatedChapters.isNullOrEmpty()) {
mapChaptersWithSource(chapters, relatedChapters, history?.chapterId, newCount, branch)
} else {
mapChapters(chapters, relatedChapters, history?.chapterId, newCount, branch)
}
} }
private fun mapChapters(
chapters: List<MangaChapter>,
downloadedChapters: List<MangaChapter>?,
currentId: Long?,
newCount: Int,
branch: String?,
): List<ChapterListItem> {
val result = ArrayList<ChapterListItem>(chapters.size)
val currentIndex = chapters.indexOfFirst { it.id == currentId }
val firstNewIndex = chapters.size - newCount
val downloadedIds = downloadedChapters?.mapTo(HashSet(downloadedChapters.size)) { 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,
)
}
if (result.size < chapters.size / 2) {
result.trimToSize()
} }
return result
} }
private fun mapChaptersWithSource( fun publishManga(manga: Manga) {
chapters: List<MangaChapter>, if (manga.source == MangaSource.LOCAL) {
sourceChapters: List<MangaChapter>, localMangaStateFlow
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
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,
) ?: chapter.toListItem(
isCurrent = i == currentIndex,
isUnread = i > currentIndex,
isNew = i >= firstNewIndex,
isMissing = true,
isDownloaded = false,
)
}
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,
)
} else { } else {
null onlineMangaStateFlow
} }.value = manga
}
result.sortBy { it.chapter.number }
}
if (result.size < sourceChapters.size / 2) {
result.trimToSize()
}
return result
} }
} }

@ -9,11 +9,7 @@ import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemChapterBinding import org.koitharu.kotatsu.databinding.ItemChapterBinding
import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_CURRENT import com.google.android.material.R as materialR
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_DOWNLOADED
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_MISSING
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_NEW
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_UNREAD
fun chapterListItemAD( fun chapterListItemAD(
clickListener: OnListItemClickListener<ChapterListItem>, clickListener: OnListItemClickListener<ChapterListItem>,
@ -31,15 +27,15 @@ fun chapterListItemAD(
binding.textViewNumber.text = item.chapter.number.toString() binding.textViewNumber.text = item.chapter.number.toString()
binding.textViewDescription.textAndVisible = item.description() binding.textViewDescription.textAndVisible = item.description()
} }
when (item.status) { when {
FLAG_UNREAD -> { item.isCurrent -> {
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_default) binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_primary)
binding.textViewNumber.setTextColor(context.getThemeColor(com.google.android.material.R.attr.colorOnTertiary)) binding.textViewNumber.setTextColor(context.getThemeColor(materialR.attr.colorOnPrimary))
} }
FLAG_CURRENT -> { item.isUnread -> {
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_accent) binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_default)
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse)) binding.textViewNumber.setTextColor(context.getThemeColor(materialR.attr.colorOnTertiary))
} }
else -> { else -> {
@ -47,12 +43,7 @@ fun chapterListItemAD(
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorTertiary)) binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorTertiary))
} }
} }
val isMissing = item.hasFlag(FLAG_MISSING) binding.imageViewDownloaded.isVisible = item.isDownloaded
binding.textViewTitle.alpha = if (isMissing) 0.3f else 1f binding.imageViewNew.isVisible = item.isNew
binding.textViewDescription.alpha = if (isMissing) 0.3f else 1f
binding.textViewNumber.alpha = if (isMissing) 0.3f else 1f
binding.imageViewDownloaded.isVisible = item.hasFlag(FLAG_DOWNLOADED)
binding.imageViewNew.isVisible = item.hasFlag(FLAG_NEW)
} }
} }

@ -22,12 +22,17 @@ class ChapterListItem(
return field return field
} }
val status: Int val isCurrent: Boolean
get() = flags and MASK_STATUS get() = hasFlag(FLAG_CURRENT)
fun hasFlag(flag: Int): Boolean { val isUnread: Boolean
return (flags and flag) == flag get() = hasFlag(FLAG_UNREAD)
}
val isDownloaded: Boolean
get() = hasFlag(FLAG_DOWNLOADED)
val isNew: Boolean
get() = hasFlag(FLAG_NEW)
fun description(): CharSequence? { fun description(): CharSequence? {
val scanlator = chapter.scanlator?.takeUnless { it.isBlank() } val scanlator = chapter.scanlator?.takeUnless { it.isBlank() }
@ -38,6 +43,10 @@ class ChapterListItem(
} }
} }
private fun hasFlag(flag: Int): Boolean {
return (flags and flag) == flag
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false
@ -46,9 +55,7 @@ class ChapterListItem(
if (chapter != other.chapter) return false if (chapter != other.chapter) return false
if (flags != other.flags) return false if (flags != other.flags) return false
if (uploadDateMs != other.uploadDateMs) return false return uploadDateMs == other.uploadDateMs
return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -63,8 +70,6 @@ class ChapterListItem(
const val FLAG_UNREAD = 2 const val FLAG_UNREAD = 2
const val FLAG_CURRENT = 4 const val FLAG_CURRENT = 4
const val FLAG_NEW = 8 const val FLAG_NEW = 8
const val FLAG_MISSING = 16
const val FLAG_DOWNLOADED = 32 const val FLAG_DOWNLOADED = 32
const val MASK_STATUS = FLAG_UNREAD or FLAG_CURRENT
} }
} }

@ -2,7 +2,6 @@ package org.koitharu.kotatsu.details.ui.model
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_CURRENT import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_CURRENT
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_DOWNLOADED import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_DOWNLOADED
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_MISSING
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_NEW import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_NEW
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_UNREAD import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_UNREAD
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
@ -11,14 +10,12 @@ fun MangaChapter.toListItem(
isCurrent: Boolean, isCurrent: Boolean,
isUnread: Boolean, isUnread: Boolean,
isNew: Boolean, isNew: Boolean,
isMissing: Boolean,
isDownloaded: Boolean, isDownloaded: Boolean,
): ChapterListItem { ): ChapterListItem {
var flags = 0 var flags = 0
if (isCurrent) flags = flags or FLAG_CURRENT if (isCurrent) flags = flags or FLAG_CURRENT
if (isUnread) flags = flags or FLAG_UNREAD if (isUnread) flags = flags or FLAG_UNREAD
if (isNew) flags = flags or FLAG_NEW if (isNew) flags = flags or FLAG_NEW
if (isMissing) flags = flags or FLAG_MISSING
if (isDownloaded) flags = flags or FLAG_DOWNLOADED if (isDownloaded) flags = flags or FLAG_DOWNLOADED
return ChapterListItem( return ChapterListItem(
chapter = this, chapter = this,

@ -46,7 +46,6 @@ class ChaptersBottomSheet : BaseBottomSheet<SheetChaptersBinding>(), OnListItemC
isCurrent = index == currentPosition, isCurrent = index == currentPosition,
isUnread = index > currentPosition, isUnread = index > currentPosition,
isNew = false, isNew = false,
isMissing = false,
isDownloaded = false, isDownloaded = false,
) )
} }

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="?colorPrimary" />
<padding
android:bottom="2dp"
android:left="2dp"
android:right="2dp"
android:top="2dp" />
</shape>

@ -47,6 +47,7 @@
<string name="theme">Theme</string> <string name="theme">Theme</string>
<string name="light">Light</string> <string name="light">Light</string>
<string name="dark">Dark</string> <string name="dark">Dark</string>
<!-- Should be as abstract as possible -->
<string name="automatic">Follow system</string> <string name="automatic">Follow system</string>
<string name="pages">Pages</string> <string name="pages">Pages</string>
<string name="clear">Clear</string> <string name="clear">Clear</string>
@ -397,6 +398,7 @@
<string name="pause">Pause</string> <string name="pause">Pause</string>
<string name="resume">Resume</string> <string name="resume">Resume</string>
<string name="paused">Paused</string> <string name="paused">Paused</string>
<!-- Menu item; action to remove completed items -->
<string name="remove_completed">Remove completed</string> <string name="remove_completed">Remove completed</string>
<string name="cancel_all">Cancel all</string> <string name="cancel_all">Cancel all</string>
<string name="downloads_wifi_only">Download only via Wi-Fi</string> <string name="downloads_wifi_only">Download only via Wi-Fi</string>

Loading…
Cancel
Save