Show not downloaded chapters in local manga
This commit is contained in:
@@ -15,7 +15,6 @@ import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
|||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
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.core.model.MangaSource
|
||||||
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding
|
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding
|
||||||
import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter
|
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
|
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||||
|
|
||||||
class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
|
class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
|
||||||
OnListItemClickListener<MangaChapter>, ActionMode.Callback, AdapterView.OnItemSelectedListener {
|
OnListItemClickListener<ChapterListItem>,
|
||||||
|
ActionMode.Callback,
|
||||||
|
AdapterView.OnItemSelectedListener {
|
||||||
|
|
||||||
private val viewModel by sharedViewModel<DetailsViewModel>()
|
private val viewModel by sharedViewModel<DetailsViewModel>()
|
||||||
|
|
||||||
@@ -105,9 +106,9 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
|
|||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: MangaChapter, view: View) {
|
override fun onItemClick(item: ChapterListItem, view: View) {
|
||||||
if (selectionDecoration?.checkedItemsCount != 0) {
|
if (selectionDecoration?.checkedItemsCount != 0) {
|
||||||
selectionDecoration?.toggleItemChecked(item.id)
|
selectionDecoration?.toggleItemChecked(item.chapter.id)
|
||||||
if (selectionDecoration?.checkedItemsCount == 0) {
|
if (selectionDecoration?.checkedItemsCount == 0) {
|
||||||
actionMode?.finish()
|
actionMode?.finish()
|
||||||
} else {
|
} else {
|
||||||
@@ -116,6 +117,10 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (item.isMissing) {
|
||||||
|
(activity as? DetailsActivity)?.showChapterMissingDialog(item.chapter.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
val options = ActivityOptions.makeScaleUpAnimation(
|
val options = ActivityOptions.makeScaleUpAnimation(
|
||||||
view,
|
view,
|
||||||
0,
|
0,
|
||||||
@@ -127,17 +132,17 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
|
|||||||
ReaderActivity.newIntent(
|
ReaderActivity.newIntent(
|
||||||
view.context,
|
view.context,
|
||||||
viewModel.manga.value ?: return,
|
viewModel.manga.value ?: return,
|
||||||
ReaderState(item.id, 0, 0)
|
ReaderState(item.chapter.id, 0, 0)
|
||||||
), options.toBundle()
|
), options.toBundle()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemLongClick(item: MangaChapter, view: View): Boolean {
|
override fun onItemLongClick(item: ChapterListItem, view: View): Boolean {
|
||||||
if (actionMode == null) {
|
if (actionMode == null) {
|
||||||
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
|
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
|
||||||
}
|
}
|
||||||
return actionMode?.also {
|
return actionMode?.also {
|
||||||
selectionDecoration?.setItemIsChecked(item.id, true)
|
selectionDecoration?.setItemIsChecked(item.chapter.id, true)
|
||||||
binding.recyclerViewChapters.invalidateItemDecorations()
|
binding.recyclerViewChapters.invalidateItemDecorations()
|
||||||
it.invalidate()
|
it.invalidate()
|
||||||
} != null
|
} != null
|
||||||
@@ -148,7 +153,7 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
|
|||||||
R.id.action_save -> {
|
R.id.action_save -> {
|
||||||
DownloadService.start(
|
DownloadService.start(
|
||||||
context ?: return false,
|
context ?: return false,
|
||||||
viewModel.manga.value ?: return false,
|
viewModel.getRemoteManga() ?: viewModel.manga.value ?: return false,
|
||||||
selectionDecoration?.checkedItemsIds
|
selectionDecoration?.checkedItemsIds
|
||||||
)
|
)
|
||||||
mode.finish()
|
mode.finish()
|
||||||
@@ -174,17 +179,20 @@ class ChaptersFragment : BaseFragment<FragmentChaptersBinding>(),
|
|||||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
val manga = viewModel.manga.value
|
val manga = viewModel.manga.value
|
||||||
mode.menuInflater.inflate(R.menu.mode_chapters, menu)
|
mode.menuInflater.inflate(R.menu.mode_chapters, menu)
|
||||||
menu.findItem(R.id.action_save).isVisible = manga?.source != MangaSource.LOCAL
|
|
||||||
mode.title = manga?.title
|
mode.title = manga?.title
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
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(
|
mode.subtitle = resources.getQuantityString(
|
||||||
R.plurals.chapters_from_x,
|
R.plurals.chapters_from_x,
|
||||||
count,
|
items.size,
|
||||||
count,
|
items.size,
|
||||||
chaptersAdapter?.itemCount ?: 0
|
chaptersAdapter?.itemCount ?: 0
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -34,8 +34,11 @@ import org.koitharu.kotatsu.core.model.MangaSource
|
|||||||
import org.koitharu.kotatsu.core.os.ShortcutsRepository
|
import org.koitharu.kotatsu.core.os.ShortcutsRepository
|
||||||
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
|
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
|
||||||
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
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.search.ui.global.GlobalSearchActivity
|
||||||
import org.koitharu.kotatsu.utils.ShareHelper
|
import org.koitharu.kotatsu.utils.ShareHelper
|
||||||
|
import org.koitharu.kotatsu.utils.ext.buildAlertDialog
|
||||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
|
|
||||||
class DetailsActivity : BaseActivity<ActivityDetailsBinding>(),
|
class DetailsActivity : BaseActivity<ActivityDetailsBinding>(),
|
||||||
@@ -228,6 +231,33 @@ class DetailsActivity : BaseActivity<ActivityDetailsBinding>(),
|
|||||||
binding.pager.isUserInputEnabled = true
|
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 {
|
companion object {
|
||||||
|
|
||||||
const val ACTION_MANGA_VIEW = "${BuildConfig.APPLICATION_ID}.action.VIEW_MANGA"
|
const val ACTION_MANGA_VIEW = "${BuildConfig.APPLICATION_ID}.action.VIEW_MANGA"
|
||||||
|
|||||||
@@ -128,23 +128,32 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||||
binding.progressBar.isVisible = isLoading
|
if (isLoading) {
|
||||||
|
binding.progressBar.show()
|
||||||
|
} else {
|
||||||
|
binding.progressBar.hide()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
override fun onClick(v: View) {
|
||||||
val manga = viewModel.manga.value
|
val manga = viewModel.manga.value ?: return
|
||||||
when (v.id) {
|
when (v.id) {
|
||||||
R.id.button_favorite -> {
|
R.id.button_favorite -> {
|
||||||
FavouriteCategoriesDialog.show(childFragmentManager, manga ?: return)
|
FavouriteCategoriesDialog.show(childFragmentManager, manga)
|
||||||
}
|
}
|
||||||
R.id.button_read -> {
|
R.id.button_read -> {
|
||||||
startActivity(
|
val chapterId = viewModel.readingHistory.value?.chapterId
|
||||||
ReaderActivity.newIntent(
|
if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) {
|
||||||
context ?: return,
|
(activity as? DetailsActivity)?.showChapterMissingDialog(chapterId)
|
||||||
manga ?: return,
|
} else {
|
||||||
null
|
startActivity(
|
||||||
|
ReaderActivity.newIntent(
|
||||||
|
context ?: return,
|
||||||
|
manga,
|
||||||
|
null
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ import org.koitharu.kotatsu.base.domain.MangaIntent
|
|||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException
|
import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException
|
||||||
import org.koitharu.kotatsu.core.model.Manga
|
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.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||||
import org.koitharu.kotatsu.details.ui.model.toListItem
|
import org.koitharu.kotatsu.details.ui.model.toListItem
|
||||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||||
import org.koitharu.kotatsu.history.domain.ChapterExtra
|
import org.koitharu.kotatsu.history.domain.ChapterExtra
|
||||||
@@ -29,7 +33,7 @@ class DetailsViewModel(
|
|||||||
private val localMangaRepository: LocalMangaRepository,
|
private val localMangaRepository: LocalMangaRepository,
|
||||||
private val trackingRepository: TrackingRepository,
|
private val trackingRepository: TrackingRepository,
|
||||||
private val mangaDataRepository: MangaDataRepository,
|
private val mangaDataRepository: MangaDataRepository,
|
||||||
private val settings: AppSettings
|
private val settings: AppSettings,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
private val mangaData = MutableStateFlow<Manga?>(intent.manga)
|
private val mangaData = MutableStateFlow<Manga?>(intent.manga)
|
||||||
@@ -53,6 +57,18 @@ class DetailsViewModel(
|
|||||||
trackingRepository.getNewChaptersCount(mangaId)
|
trackingRepository.getNewChaptersCount(mangaId)
|
||||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
|
}.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()
|
private val chaptersReversed = settings.observe()
|
||||||
.filter { it == AppSettings.KEY_REVERSE_CHAPTERS }
|
.filter { it == AppSettings.KEY_REVERSE_CHAPTERS }
|
||||||
.map { settings.chaptersReverse }
|
.map { settings.chaptersReverse }
|
||||||
@@ -85,24 +101,19 @@ class DetailsViewModel(
|
|||||||
|
|
||||||
val chapters = combine(
|
val chapters = combine(
|
||||||
mangaData.map { it?.chapters.orEmpty() },
|
mangaData.map { it?.chapters.orEmpty() },
|
||||||
|
remoteManga,
|
||||||
history.map { it?.chapterId },
|
history.map { it?.chapterId },
|
||||||
newChapters,
|
newChapters,
|
||||||
chaptersReversed,
|
|
||||||
selectedBranch
|
selectedBranch
|
||||||
) { chapters, currentId, newCount, reversed, branch ->
|
) { chapters, sourceManga, currentId, newCount, branch ->
|
||||||
val currentIndex = chapters.indexOfFirst { it.id == currentId }
|
val sourceChapters = sourceManga?.chapters
|
||||||
val firstNewIndex = chapters.size - newCount
|
if (sourceChapters.isNullOrEmpty()) {
|
||||||
val res = chapters.mapIndexed { index, chapter ->
|
mapChapters(chapters, currentId, newCount, branch)
|
||||||
chapter.toListItem(
|
} else {
|
||||||
when {
|
mapChaptersWithSource(chapters, sourceChapters, currentId, newCount, branch)
|
||||||
index >= firstNewIndex -> ChapterExtra.NEW
|
}
|
||||||
index == currentIndex -> ChapterExtra.CURRENT
|
}.combine(chaptersReversed) { list, reversed ->
|
||||||
index < currentIndex -> ChapterExtra.READ
|
if (reversed) list.asReversed() else list
|
||||||
else -> ChapterExtra.UNREAD
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}.filter { it.chapter.branch == branch }
|
|
||||||
if (reversed) res.asReversed() else res
|
|
||||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -121,6 +132,12 @@ class DetailsViewModel(
|
|||||||
?.maxByOrNull { it.value.size }?.key
|
?.maxByOrNull { it.value.size }?.key
|
||||||
}
|
}
|
||||||
mangaData.value = manga
|
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?) {
|
fun setSelectedBranch(branch: String?) {
|
||||||
selectedBranch.value = branch
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,23 +3,22 @@ package org.koitharu.kotatsu.details.ui.adapter
|
|||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
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.databinding.ItemChapterBinding
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||||
import org.koitharu.kotatsu.history.domain.ChapterExtra
|
import org.koitharu.kotatsu.history.domain.ChapterExtra
|
||||||
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||||
|
|
||||||
fun chapterListItemAD(
|
fun chapterListItemAD(
|
||||||
clickListener: OnListItemClickListener<MangaChapter>
|
clickListener: OnListItemClickListener<ChapterListItem>,
|
||||||
) = adapterDelegateViewBinding<ChapterListItem, ChapterListItem, ItemChapterBinding>(
|
) = adapterDelegateViewBinding<ChapterListItem, ChapterListItem, ItemChapterBinding>(
|
||||||
{ inflater, parent -> ItemChapterBinding.inflate(inflater, parent, false) }
|
{ inflater, parent -> ItemChapterBinding.inflate(inflater, parent, false) }
|
||||||
) {
|
) {
|
||||||
|
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
clickListener.onItemClick(item.chapter, it)
|
clickListener.onItemClick(item, it)
|
||||||
}
|
}
|
||||||
itemView.setOnLongClickListener {
|
itemView.setOnLongClickListener {
|
||||||
clickListener.onItemLongClick(item.chapter, it)
|
clickListener.onItemLongClick(item, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
bind { payload ->
|
bind { payload ->
|
||||||
@@ -43,5 +42,7 @@ fun chapterListItemAD(
|
|||||||
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse))
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,12 +3,11 @@ package org.koitharu.kotatsu.details.ui.adapter
|
|||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||||
import kotlin.jvm.internal.Intrinsics
|
import kotlin.jvm.internal.Intrinsics
|
||||||
|
|
||||||
class ChaptersAdapter(
|
class ChaptersAdapter(
|
||||||
onItemClickListener: OnListItemClickListener<MangaChapter>
|
onItemClickListener: OnListItemClickListener<ChapterListItem>,
|
||||||
) : AsyncListDifferDelegationAdapter<ChapterListItem>(DiffCallback()) {
|
) : AsyncListDifferDelegationAdapter<ChapterListItem>(DiffCallback()) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -38,7 +37,7 @@ class ChaptersAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getChangePayload(oldItem: ChapterListItem, newItem: ChapterListItem): Any? {
|
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 newItem.extra
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ import org.koitharu.kotatsu.history.domain.ChapterExtra
|
|||||||
|
|
||||||
data class ChapterListItem(
|
data class ChapterListItem(
|
||||||
val chapter: MangaChapter,
|
val chapter: MangaChapter,
|
||||||
val extra: ChapterExtra
|
val extra: ChapterExtra,
|
||||||
|
val isMissing: Boolean,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ package org.koitharu.kotatsu.details.ui.model
|
|||||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.history.domain.ChapterExtra
|
import org.koitharu.kotatsu.history.domain.ChapterExtra
|
||||||
|
|
||||||
fun MangaChapter.toListItem(extra: ChapterExtra) = ChapterListItem(
|
fun MangaChapter.toListItem(
|
||||||
|
extra: ChapterExtra,
|
||||||
|
isMissing: Boolean,
|
||||||
|
) = ChapterListItem(
|
||||||
chapter = this,
|
chapter = this,
|
||||||
extra = extra
|
extra = extra,
|
||||||
|
isMissing = isMissing,
|
||||||
)
|
)
|
||||||
@@ -47,6 +47,7 @@ class DownloadService : BaseService() {
|
|||||||
private val jobCount = MutableStateFlow(0)
|
private val jobCount = MutableStateFlow(0)
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
private val controlReceiver = ControlReceiver()
|
private val controlReceiver = ControlReceiver()
|
||||||
|
private var binder: DownloadBinder? = null
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
@@ -75,11 +76,12 @@ class DownloadService : BaseService() {
|
|||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder {
|
override fun onBind(intent: Intent): IBinder {
|
||||||
super.onBind(intent)
|
super.onBind(intent)
|
||||||
return DownloadBinder()
|
return binder ?: DownloadBinder(this).also { binder = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
unregisterReceiver(controlReceiver)
|
unregisterReceiver(controlReceiver)
|
||||||
|
binder = null
|
||||||
super.onDestroy()
|
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>>>
|
val downloads: Flow<Collection<JobStateFlow<DownloadManager.State>>>
|
||||||
get() = jobCount.mapLatest { jobs.values }
|
get() = service.jobCount.mapLatest { service.jobs.values }
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -160,6 +162,9 @@ class DownloadService : BaseService() {
|
|||||||
private const val EXTRA_CANCEL_ID = "cancel_id"
|
private const val EXTRA_CANCEL_ID = "cancel_id"
|
||||||
|
|
||||||
fun start(context: Context, manga: Manga, chaptersIds: Collection<Long>? = null) {
|
fun start(context: Context, manga: Manga, chaptersIds: Collection<Long>? = null) {
|
||||||
|
if (chaptersIds?.isEmpty() == true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
confirmDataTransfer(context) {
|
confirmDataTransfer(context) {
|
||||||
val intent = Intent(context, DownloadService::class.java)
|
val intent = Intent(context, DownloadService::class.java)
|
||||||
intent.putExtra(EXTRA_MANGA, manga)
|
intent.putExtra(EXTRA_MANGA, manga)
|
||||||
|
|||||||
@@ -98,7 +98,10 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
|
|||||||
entryName = index.getCoverEntry()
|
entryName = index.getCoverEntry()
|
||||||
?: findFirstEntry(zip.entries(), isImage = true)?.name.orEmpty()
|
?: 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
|
// fallback
|
||||||
|
|||||||
@@ -15,16 +15,17 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|||||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.databinding.DialogChaptersBinding
|
import org.koitharu.kotatsu.databinding.DialogChaptersBinding
|
||||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
|
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.details.ui.model.toListItem
|
||||||
import org.koitharu.kotatsu.history.domain.ChapterExtra
|
import org.koitharu.kotatsu.history.domain.ChapterExtra
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||||
|
|
||||||
class ChaptersDialog : AlertDialogFragment<DialogChaptersBinding>(),
|
class ChaptersDialog : AlertDialogFragment<DialogChaptersBinding>(),
|
||||||
OnListItemClickListener<MangaChapter> {
|
OnListItemClickListener<ChapterListItem> {
|
||||||
|
|
||||||
override fun onInflateView(
|
override fun onInflateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?
|
container: ViewGroup?,
|
||||||
) = DialogChaptersBinding.inflate(inflater, container, false)
|
) = DialogChaptersBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
override fun onBuildDialog(builder: AlertDialog.Builder) {
|
override fun onBuildDialog(builder: AlertDialog.Builder) {
|
||||||
@@ -51,7 +52,8 @@ class ChaptersDialog : AlertDialogFragment<DialogChaptersBinding>(),
|
|||||||
index < currentPosition -> ChapterExtra.READ
|
index < currentPosition -> ChapterExtra.READ
|
||||||
index == currentPosition -> ChapterExtra.CURRENT
|
index == currentPosition -> ChapterExtra.CURRENT
|
||||||
else -> ChapterExtra.UNREAD
|
else -> ChapterExtra.UNREAD
|
||||||
}
|
},
|
||||||
|
isMissing = false
|
||||||
)
|
)
|
||||||
}) {
|
}) {
|
||||||
if (currentPosition >= 0) {
|
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)
|
((parentFragment as? OnChapterChangeListener)
|
||||||
?: (activity as? OnChapterChangeListener))?.let {
|
?: (activity as? OnChapterChangeListener))?.let {
|
||||||
dismiss()
|
dismiss()
|
||||||
it.onChapterChanged(item)
|
it.onChapterChanged(item.chapter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package org.koitharu.kotatsu.utils.ext
|
package org.koitharu.kotatsu.utils.ext
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.Network
|
import android.net.Network
|
||||||
import android.net.NetworkRequest
|
import android.net.NetworkRequest
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
@@ -19,4 +21,8 @@ suspend fun ConnectivityManager.waitForNetwork(): Network {
|
|||||||
unregisterNetworkCallback(callback)
|
unregisterNetworkCallback(callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun buildAlertDialog(context: Context, block: AlertDialog.Builder.() -> Unit): AlertDialog {
|
||||||
|
return AlertDialog.Builder(context).apply(block).create()
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.utils.ext
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.transform
|
||||||
|
|
||||||
fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
|
fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
|
||||||
var isFirstCall = true
|
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>> {
|
inline fun <T, R> Flow<List<T>>.mapItems(crossinline transform: (T) -> R): Flow<List<R>> {
|
||||||
return map { list -> list.map(transform) }
|
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)
|
||||||
}
|
}
|
||||||
@@ -263,13 +263,15 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<ProgressBar
|
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
android:id="@+id/progressBar"
|
android:id="@+id/progressBar"
|
||||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:indeterminate="true"
|
android:indeterminate="true"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
app:showAnimationBehavior="inward"
|
||||||
|
app:hideAnimationBehavior="outward"
|
||||||
app:layout_constraintBottom_toTopOf="parent"
|
app:layout_constraintBottom_toTopOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
|||||||
@@ -270,13 +270,15 @@
|
|||||||
tools:ignore="UnusedAttribute"
|
tools:ignore="UnusedAttribute"
|
||||||
tools:text="@tools:sample/lorem/random[25]" />
|
tools:text="@tools:sample/lorem/random[25]" />
|
||||||
|
|
||||||
<ProgressBar
|
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
android:id="@+id/progressBar"
|
android:id="@+id/progressBar"
|
||||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:indeterminate="true"
|
android:indeterminate="true"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
app:showAnimationBehavior="inward"
|
||||||
|
app:hideAnimationBehavior="outward"
|
||||||
app:layout_constraintBottom_toTopOf="parent"
|
app:layout_constraintBottom_toTopOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
|||||||
@@ -277,6 +277,8 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:indeterminate="true"
|
android:indeterminate="true"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
app:showAnimationBehavior="inward"
|
||||||
|
app:hideAnimationBehavior="outward"
|
||||||
app:layout_constraintBottom_toTopOf="parent"
|
app:layout_constraintBottom_toTopOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
|||||||
@@ -222,4 +222,6 @@
|
|||||||
<string name="read_more">Read more</string>
|
<string name="read_more">Read more</string>
|
||||||
<string name="queued">Queued</string>
|
<string name="queued">Queued</string>
|
||||||
<string name="text_downloads_holder">There are currently no active downloads</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>
|
</resources>
|
||||||
Reference in New Issue
Block a user