Show not downloaded chapters in local manga

This commit is contained in:
Koitharu
2021-07-24 10:51:26 +03:00
parent e8e95a485b
commit 7f5ef227eb
17 changed files with 234 additions and 58 deletions

View File

@@ -15,7 +15,6 @@ import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding
import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter
@@ -27,7 +26,9 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState
class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
OnListItemClickListener<MangaChapter>, ActionMode.Callback, AdapterView.OnItemSelectedListener {
OnListItemClickListener<ChapterListItem>,
ActionMode.Callback,
AdapterView.OnItemSelectedListener {
private val viewModel by sharedViewModel<DetailsViewModel>()
@@ -105,9 +106,9 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
else -> super.onOptionsItemSelected(item)
}
override fun onItemClick(item: MangaChapter, view: View) {
override fun onItemClick(item: ChapterListItem, view: View) {
if (selectionDecoration?.checkedItemsCount != 0) {
selectionDecoration?.toggleItemChecked(item.id)
selectionDecoration?.toggleItemChecked(item.chapter.id)
if (selectionDecoration?.checkedItemsCount == 0) {
actionMode?.finish()
} else {
@@ -116,6 +117,10 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
}
return
}
if (item.isMissing) {
(activity as? DetailsActivity)?.showChapterMissingDialog(item.chapter.id)
return
}
val options = ActivityOptions.makeScaleUpAnimation(
view,
0,
@@ -127,17 +132,17 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
ReaderActivity.newIntent(
view.context,
viewModel.manga.value ?: return,
ReaderState(item.id, 0, 0)
ReaderState(item.chapter.id, 0, 0)
), options.toBundle()
)
}
override fun onItemLongClick(item: MangaChapter, view: View): Boolean {
override fun onItemLongClick(item: ChapterListItem, view: View): Boolean {
if (actionMode == null) {
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
}
return actionMode?.also {
selectionDecoration?.setItemIsChecked(item.id, true)
selectionDecoration?.setItemIsChecked(item.chapter.id, true)
binding.recyclerViewChapters.invalidateItemDecorations()
it.invalidate()
} != null
@@ -148,7 +153,7 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
R.id.action_save -> {
DownloadService.start(
context ?: return false,
viewModel.manga.value ?: return false,
viewModel.getRemoteManga() ?: viewModel.manga.value ?: return false,
selectionDecoration?.checkedItemsIds
)
mode.finish()
@@ -174,17 +179,20 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
val manga = viewModel.manga.value
mode.menuInflater.inflate(R.menu.mode_chapters, menu)
menu.findItem(R.id.action_save).isVisible = manga?.source != MangaSource.LOCAL
mode.title = manga?.title
return true
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
val count = selectionDecoration?.checkedItemsCount ?: return false
val selectedIds = selectionDecoration?.checkedItemsIds ?: return false
val items = chaptersAdapter?.items?.filter { x -> x.chapter.id in selectedIds }.orEmpty()
menu.findItem(R.id.action_save).isVisible = items.none { x ->
x.chapter.source == MangaSource.LOCAL
}
mode.subtitle = resources.getQuantityString(
R.plurals.chapters_from_x,
count,
count,
items.size,
items.size,
chaptersAdapter?.itemCount ?: 0
)
return true

View File

@@ -34,8 +34,11 @@ import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.os.ShortcutsRepository
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity
import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.buildAlertDialog
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class DetailsActivity : BaseActivity<ActivityDetailsBinding>(),
@@ -228,6 +231,33 @@ class DetailsActivity : BaseActivity<ActivityDetailsBinding>(),
binding.pager.isUserInputEnabled = true
}
fun showChapterMissingDialog(chapterId: Long) {
val remoteManga = viewModel.getRemoteManga()
if (remoteManga == null) {
Snackbar.make(binding.pager, R.string.chapter_is_missing, Snackbar.LENGTH_LONG)
.show()
return
}
buildAlertDialog(this) {
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(
this@DetailsActivity,
remoteManga,
ReaderState(chapterId, 0, 0)
)
)
}
setNeutralButton(R.string.download) { _, _ ->
DownloadService.start(this@DetailsActivity, remoteManga, setOf(chapterId))
}
setCancelable(true)
}.show()
}
companion object {
const val ACTION_MANGA_VIEW = "${BuildConfig.APPLICATION_ID}.action.VIEW_MANGA"

View File

@@ -128,23 +128,32 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
}
private fun onLoadingStateChanged(isLoading: Boolean) {
binding.progressBar.isVisible = isLoading
if (isLoading) {
binding.progressBar.show()
} else {
binding.progressBar.hide()
}
}
override fun onClick(v: View) {
val manga = viewModel.manga.value
val manga = viewModel.manga.value ?: return
when (v.id) {
R.id.button_favorite -> {
FavouriteCategoriesDialog.show(childFragmentManager, manga ?: return)
FavouriteCategoriesDialog.show(childFragmentManager, manga)
}
R.id.button_read -> {
startActivity(
ReaderActivity.newIntent(
context ?: return,
manga ?: return,
null
val chapterId = viewModel.readingHistory.value?.chapterId
if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) {
(activity as? DetailsActivity)?.showChapterMissingDialog(chapterId)
} else {
startActivity(
ReaderActivity.newIntent(
context ?: return,
manga,
null
)
)
)
}
}
}
}

View File

@@ -11,7 +11,11 @@ import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
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.ChapterExtra
@@ -29,7 +33,7 @@ class DetailsViewModel(
private val localMangaRepository: LocalMangaRepository,
private val trackingRepository: TrackingRepository,
private val mangaDataRepository: MangaDataRepository,
private val settings: AppSettings
private val settings: AppSettings,
) : BaseViewModel() {
private val mangaData = MutableStateFlow<Manga?>(intent.manga)
@@ -53,6 +57,18 @@ class DetailsViewModel(
trackingRepository.getNewChaptersCount(mangaId)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
private val remoteManga = MutableStateFlow<Manga?>(null)
/*private val remoteManga = mangaData.mapLatest {
if (it?.source == MangaSource.LOCAL) {
runCatching {
val m = localMangaRepository.getRemoteManga(it) ?: return@mapLatest null
MangaRepository(m.source).getDetails(m)
}.getOrNull()
} else {
null
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)*/
private val chaptersReversed = settings.observe()
.filter { it == AppSettings.KEY_REVERSE_CHAPTERS }
.map { settings.chaptersReverse }
@@ -85,24 +101,19 @@ class DetailsViewModel(
val chapters = combine(
mangaData.map { it?.chapters.orEmpty() },
remoteManga,
history.map { it?.chapterId },
newChapters,
chaptersReversed,
selectedBranch
) { chapters, currentId, newCount, reversed, branch ->
val currentIndex = chapters.indexOfFirst { it.id == currentId }
val firstNewIndex = chapters.size - newCount
val res = chapters.mapIndexed { index, chapter ->
chapter.toListItem(
when {
index >= firstNewIndex -> ChapterExtra.NEW
index == currentIndex -> ChapterExtra.CURRENT
index < currentIndex -> ChapterExtra.READ
else -> ChapterExtra.UNREAD
}
)
}.filter { it.chapter.branch == branch }
if (reversed) res.asReversed() else res
) { chapters, sourceManga, currentId, newCount, branch ->
val sourceChapters = sourceManga?.chapters
if (sourceChapters.isNullOrEmpty()) {
mapChapters(chapters, currentId, newCount, branch)
} else {
mapChaptersWithSource(chapters, sourceChapters, currentId, newCount, branch)
}
}.combine(chaptersReversed) { list, reversed ->
if (reversed) list.asReversed() else list
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
init {
@@ -121,6 +132,12 @@ class DetailsViewModel(
?.maxByOrNull { it.value.size }?.key
}
mangaData.value = manga
if (manga.source == MangaSource.LOCAL) {
remoteManga.value = runCatching {
val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null
MangaRepository(m.source).getDetails(m)
}.getOrNull()
}
}
}
@@ -142,4 +159,80 @@ class DetailsViewModel(
fun setSelectedBranch(branch: String?) {
selectedBranch.value = branch
}
fun getRemoteManga(): Manga? {
return remoteManga.value
}
private fun mapChapters(
chapters: 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
for (i in chapters.indices) {
val chapter = chapters[i]
if (chapter.branch != branch) {
continue
}
result += chapter.toListItem(
extra = when {
i >= firstNewIndex -> ChapterExtra.NEW
i == currentIndex -> ChapterExtra.CURRENT
i < currentIndex -> ChapterExtra.READ
else -> ChapterExtra.UNREAD
},
isMissing = false
)
}
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
for (i in sourceChapters.indices) {
val chapter = sourceChapters[i]
if (chapter.branch != branch) {
continue
}
val localChapter = chaptersMap.remove(chapter.id)
result += localChapter?.toListItem(
extra = when {
i >= firstNewIndex -> ChapterExtra.NEW
i == currentIndex -> ChapterExtra.CURRENT
i < currentIndex -> ChapterExtra.READ
else -> ChapterExtra.UNREAD
},
isMissing = false
) ?: chapter.toListItem(
extra = when {
i >= firstNewIndex -> ChapterExtra.NEW
i == currentIndex -> ChapterExtra.CURRENT
i < currentIndex -> ChapterExtra.READ
else -> ChapterExtra.UNREAD
},
isMissing = true
)
}
if (chaptersMap.isNotEmpty()) { // some chapters on device but not online source
result.ensureCapacity(result.size + chaptersMap.size)
chaptersMap.values.mapTo(result) {
it.toListItem(ChapterExtra.UNREAD, false)
}
result.sortBy { it.chapter.number }
}
return result
}
}

View File

@@ -3,23 +3,22 @@ package org.koitharu.kotatsu.details.ui.adapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.databinding.ItemChapterBinding
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.history.domain.ChapterExtra
import org.koitharu.kotatsu.utils.ext.getThemeColor
fun chapterListItemAD(
clickListener: OnListItemClickListener<MangaChapter>
clickListener: OnListItemClickListener<ChapterListItem>,
) = adapterDelegateViewBinding<ChapterListItem, ChapterListItem, ItemChapterBinding>(
{ inflater, parent -> ItemChapterBinding.inflate(inflater, parent, false) }
) {
itemView.setOnClickListener {
clickListener.onItemClick(item.chapter, it)
clickListener.onItemClick(item, it)
}
itemView.setOnLongClickListener {
clickListener.onItemLongClick(item.chapter, it)
clickListener.onItemLongClick(item, it)
}
bind { payload ->
@@ -43,5 +42,7 @@ fun chapterListItemAD(
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse))
}
}
binding.textViewTitle.alpha = if (item.isMissing) 0.3f else 1f
binding.textViewNumber.alpha = if (item.isMissing) 0.3f else 1f
}
}

View File

@@ -3,12 +3,11 @@ package org.koitharu.kotatsu.details.ui.adapter
import androidx.recyclerview.widget.DiffUtil
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import kotlin.jvm.internal.Intrinsics
class ChaptersAdapter(
onItemClickListener: OnListItemClickListener<MangaChapter>
onItemClickListener: OnListItemClickListener<ChapterListItem>,
) : AsyncListDifferDelegationAdapter<ChapterListItem>(DiffCallback()) {
init {
@@ -38,7 +37,7 @@ class ChaptersAdapter(
}
override fun getChangePayload(oldItem: ChapterListItem, newItem: ChapterListItem): Any? {
if (oldItem.extra != newItem.extra) {
if (oldItem.extra != newItem.extra && oldItem.chapter == newItem.chapter) {
return newItem.extra
}
return null

View File

@@ -5,5 +5,6 @@ import org.koitharu.kotatsu.history.domain.ChapterExtra
data class ChapterListItem(
val chapter: MangaChapter,
val extra: ChapterExtra
val extra: ChapterExtra,
val isMissing: Boolean,
)

View File

@@ -3,7 +3,11 @@ package org.koitharu.kotatsu.details.ui.model
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.history.domain.ChapterExtra
fun MangaChapter.toListItem(extra: ChapterExtra) = ChapterListItem(
fun MangaChapter.toListItem(
extra: ChapterExtra,
isMissing: Boolean,
) = ChapterListItem(
chapter = this,
extra = extra
extra = extra,
isMissing = isMissing,
)

View File

@@ -47,6 +47,7 @@ class DownloadService : BaseService() {
private val jobCount = MutableStateFlow(0)
private val mutex = Mutex()
private val controlReceiver = ControlReceiver()
private var binder: DownloadBinder? = null
override fun onCreate() {
super.onCreate()
@@ -75,11 +76,12 @@ class DownloadService : BaseService() {
override fun onBind(intent: Intent): IBinder {
super.onBind(intent)
return DownloadBinder()
return binder ?: DownloadBinder(this).also { binder = it }
}
override fun onDestroy() {
unregisterReceiver(controlReceiver)
binder = null
super.onDestroy()
}
@@ -141,10 +143,10 @@ class DownloadService : BaseService() {
}
}
inner class DownloadBinder : Binder() {
class DownloadBinder(private val service: DownloadService) : Binder() {
val downloads: Flow<Collection<JobStateFlow<DownloadManager.State>>>
get() = jobCount.mapLatest { jobs.values }
get() = service.jobCount.mapLatest { service.jobs.values }
}
companion object {
@@ -160,6 +162,9 @@ class DownloadService : BaseService() {
private const val EXTRA_CANCEL_ID = "cancel_id"
fun start(context: Context, manga: Manga, chaptersIds: Collection<Long>? = null) {
if (chaptersIds?.isEmpty() == true) {
return
}
confirmDataTransfer(context) {
val intent = Intent(context, DownloadService::class.java)
intent.putExtra(EXTRA_MANGA, manga)

View File

@@ -98,7 +98,10 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
entryName = index.getCoverEntry()
?: findFirstEntry(zip.entries(), isImage = true)?.name.orEmpty()
),
chapters = info.chapters?.map { c -> c.copy(url = fileUri) }
chapters = info.chapters?.map { c ->
c.copy(url = fileUri,
source = MangaSource.LOCAL)
}
)
}
// fallback

View File

@@ -15,16 +15,17 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.databinding.DialogChaptersBinding
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.toListItem
import org.koitharu.kotatsu.history.domain.ChapterExtra
import org.koitharu.kotatsu.utils.ext.withArgs
class ChaptersDialog : AlertDialogFragment<DialogChaptersBinding>(),
OnListItemClickListener<MangaChapter> {
OnListItemClickListener<ChapterListItem> {
override fun onInflateView(
inflater: LayoutInflater,
container: ViewGroup?
container: ViewGroup?,
) = DialogChaptersBinding.inflate(inflater, container, false)
override fun onBuildDialog(builder: AlertDialog.Builder) {
@@ -51,7 +52,8 @@ class ChaptersDialog : AlertDialogFragment<DialogChaptersBinding>(),
index < currentPosition -> ChapterExtra.READ
index == currentPosition -> ChapterExtra.CURRENT
else -> ChapterExtra.UNREAD
}
},
isMissing = false
)
}) {
if (currentPosition >= 0) {
@@ -66,11 +68,11 @@ class ChaptersDialog : AlertDialogFragment<DialogChaptersBinding>(),
}
}
override fun onItemClick(item: MangaChapter, view: View) {
override fun onItemClick(item: ChapterListItem, view: View) {
((parentFragment as? OnChapterChangeListener)
?: (activity as? OnChapterChangeListener))?.let {
dismiss()
it.onChapterChanged(item)
it.onChapterChanged(item.chapter)
}
}

View File

@@ -1,8 +1,10 @@
package org.koitharu.kotatsu.utils.ext
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkRequest
import androidx.appcompat.app.AlertDialog
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
@@ -19,4 +21,8 @@ suspend fun ConnectivityManager.waitForNetwork(): Network {
unregisterNetworkCallback(callback)
}
}
}
inline fun buildAlertDialog(context: Context, block: AlertDialog.Builder.() -> Unit): AlertDialog {
return AlertDialog.Builder(context).apply(block).create()
}

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.utils.ext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.transform
fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
var isFirstCall = true
@@ -16,4 +17,10 @@ fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
inline fun <T, R> Flow<List<T>>.mapItems(crossinline transform: (T) -> R): Flow<List<R>> {
return map { list -> list.map(transform) }
}
inline fun <T> Flow<T?>.filterNotNull(
crossinline predicate: suspend (T) -> Boolean,
): Flow<T> = transform { value ->
if (value != null && predicate(value)) return@transform emit(value)
}

View File

@@ -263,13 +263,15 @@
</LinearLayout>
<ProgressBar
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressBar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:showAnimationBehavior="inward"
app:hideAnimationBehavior="outward"
app:layout_constraintBottom_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@@ -270,13 +270,15 @@
tools:ignore="UnusedAttribute"
tools:text="@tools:sample/lorem/random[25]" />
<ProgressBar
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressBar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:showAnimationBehavior="inward"
app:hideAnimationBehavior="outward"
app:layout_constraintBottom_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@@ -277,6 +277,8 @@
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:showAnimationBehavior="inward"
app:hideAnimationBehavior="outward"
app:layout_constraintBottom_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@@ -222,4 +222,6 @@
<string name="read_more">Read more</string>
<string name="queued">Queued</string>
<string name="text_downloads_holder">There are currently no active downloads</string>
<string name="chapter_is_missing_text">This chapter is missing on your device. Download it or read online</string>
<string name="chapter_is_missing">Chapter is missing</string>
</resources>