Migrate details to AdapterDelegates and mvvm
This commit is contained in:
@@ -32,6 +32,12 @@ class MangaDataRepository(private val db: MangaDatabase) {
|
||||
return db.mangaDao.find(mangaId)?.toManga()
|
||||
}
|
||||
|
||||
suspend fun resolveIntent(intent: MangaIntent): Manga? = when {
|
||||
intent.manga != null -> intent.manga
|
||||
intent.mangaId != MangaIntent.ID_NONE -> db.mangaDao.find(intent.mangaId)?.toManga()
|
||||
else -> null // TODO resolve uri
|
||||
}
|
||||
|
||||
suspend fun storeManga(manga: Manga) {
|
||||
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
|
||||
db.withTransaction {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.koitharu.kotatsu.base.domain
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
|
||||
data class MangaIntent(
|
||||
val manga: Manga?,
|
||||
val mangaId: Long,
|
||||
val uri: Uri?
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(intent: Intent?) = MangaIntent(
|
||||
manga = intent?.getParcelableExtra(KEY_MANGA),
|
||||
mangaId = intent?.getLongExtra(KEY_ID, ID_NONE) ?: ID_NONE,
|
||||
uri = intent?.data
|
||||
)
|
||||
|
||||
fun from(args: Bundle?) = MangaIntent(
|
||||
manga = args?.getParcelable(KEY_MANGA),
|
||||
mangaId = args?.getLong(KEY_ID, ID_NONE) ?: ID_NONE,
|
||||
uri = null
|
||||
)
|
||||
|
||||
const val ID_NONE = 0L
|
||||
|
||||
const val KEY_MANGA = "manga"
|
||||
const val KEY_ID = "id"
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.*
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
abstract class BaseViewModel : ViewModel() {
|
||||
|
||||
@@ -13,19 +15,21 @@ abstract class BaseViewModel : ViewModel() {
|
||||
val isLoading = MutableLiveData(false)
|
||||
|
||||
protected fun launchJob(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
): Job = viewModelScope.launch(createErrorHandler(), start, block)
|
||||
): Job = viewModelScope.launch(context + createErrorHandler(), start, block)
|
||||
|
||||
protected fun launchLoadingJob(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
): Job = viewModelScope.launch(createErrorHandler(), start) {
|
||||
isLoading.value = true
|
||||
): Job = viewModelScope.launch(context + createErrorHandler(), start) {
|
||||
isLoading.postValue(true)
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
isLoading.postValue(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +38,7 @@ abstract class BaseViewModel : ViewModel() {
|
||||
throwable.printStackTrace()
|
||||
}
|
||||
if (throwable !is CancellationException) {
|
||||
onError.call(throwable)
|
||||
onError.postCall(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koitharu.kotatsu.utils.ext.replaceWith
|
||||
|
||||
@Deprecated("", replaceWith = ReplaceWith("AsyncListDifferDelegationAdapter"))
|
||||
abstract class BaseRecyclerAdapter<T, E>(private val onItemClickListener: OnRecyclerItemClickListener<T>? = null) :
|
||||
RecyclerView.Adapter<BaseViewHolder<T, E>>(),
|
||||
KoinComponent {
|
||||
|
||||
@@ -2,10 +2,13 @@ package org.koitharu.kotatsu.details
|
||||
|
||||
import org.koin.android.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
||||
import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
||||
|
||||
val detailsModule
|
||||
get() = module {
|
||||
|
||||
viewModel { DetailsViewModel(get(), get(), get(), get(), get()) }
|
||||
viewModel { (intent: MangaIntent) ->
|
||||
DetailsViewModel(intent, get(), get(), get(), get(), get())
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.base.ui.list.BaseRecyclerAdapter
|
||||
import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.history.domain.ChapterExtra
|
||||
|
||||
class ChaptersAdapter(onItemClickListener: OnRecyclerItemClickListener<MangaChapter>) :
|
||||
BaseRecyclerAdapter<MangaChapter, ChapterExtra>(onItemClickListener) {
|
||||
|
||||
private val checkedIds = HashSet<Long>()
|
||||
|
||||
val checkedItemsCount: Int
|
||||
get() = checkedIds.size
|
||||
|
||||
val checkedItemsIds: Set<Long>
|
||||
get() = checkedIds
|
||||
|
||||
var currentChapterId: Long? = null
|
||||
set(value) {
|
||||
field = value
|
||||
updateCurrentPosition()
|
||||
}
|
||||
|
||||
var newChaptersCount: Int = 0
|
||||
set(value) {
|
||||
val updated = maxOf(field, value)
|
||||
field = value
|
||||
notifyItemRangeChanged(itemCount - updated, updated)
|
||||
}
|
||||
|
||||
var currentChapterPosition = RecyclerView.NO_POSITION
|
||||
private set
|
||||
|
||||
fun clearChecked() {
|
||||
checkedIds.clear()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun checkAll() {
|
||||
for (item in dataSet) {
|
||||
checkedIds.add(item.id)
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun setItemIsChecked(itemId: Long, isChecked: Boolean) {
|
||||
if ((isChecked && checkedIds.add(itemId)) || (!isChecked && checkedIds.remove(itemId))) {
|
||||
val pos = findItemPositionById(itemId)
|
||||
if (pos != RecyclerView.NO_POSITION) {
|
||||
notifyItemChanged(pos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleItemChecked(itemId: Long) {
|
||||
setItemIsChecked(itemId, itemId !in checkedIds)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup) = ChapterHolder(parent)
|
||||
|
||||
override fun onGetItemId(item: MangaChapter) = item.id
|
||||
|
||||
override fun getExtra(item: MangaChapter, position: Int): ChapterExtra = when {
|
||||
item.id in checkedIds -> ChapterExtra.CHECKED
|
||||
currentChapterPosition == RecyclerView.NO_POSITION
|
||||
|| currentChapterPosition < position -> if (position >= itemCount - newChaptersCount) {
|
||||
ChapterExtra.NEW
|
||||
} else {
|
||||
ChapterExtra.UNREAD
|
||||
}
|
||||
currentChapterPosition == position -> ChapterExtra.CURRENT
|
||||
currentChapterPosition > position -> ChapterExtra.READ
|
||||
else -> ChapterExtra.UNREAD
|
||||
}
|
||||
|
||||
override fun onDataSetChanged() {
|
||||
super.onDataSetChanged()
|
||||
updateCurrentPosition()
|
||||
}
|
||||
|
||||
private fun updateCurrentPosition() {
|
||||
val pos = currentChapterId?.let {
|
||||
dataSet.indexOfFirst { x -> x.id == it }
|
||||
} ?: RecyclerView.NO_POSITION
|
||||
if (pos != currentChapterPosition) {
|
||||
currentChapterPosition = pos
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,75 +9,66 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.fragment_chapters.*
|
||||
import org.koin.android.viewmodel.ext.android.sharedViewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
|
||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.download.DownloadService
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.utils.ext.resolveDp
|
||||
|
||||
class ChaptersFragment : BaseFragment(R.layout.fragment_chapters),
|
||||
OnRecyclerItemClickListener<MangaChapter>, ActionMode.Callback {
|
||||
OnListItemClickListener<MangaChapter>, ActionMode.Callback {
|
||||
|
||||
private val viewModel by sharedViewModel<DetailsViewModel>()
|
||||
|
||||
private var manga: Manga? = null
|
||||
|
||||
private lateinit var adapter: ChaptersAdapter
|
||||
private var chaptersAdapter: ChaptersAdapter? = null
|
||||
private var actionMode: ActionMode? = null
|
||||
private var selectionDecoration: ChaptersSelectionDecoration? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
adapter = ChaptersAdapter(this)
|
||||
recyclerView_chapters.addItemDecoration(
|
||||
DividerItemDecoration(
|
||||
view.context,
|
||||
RecyclerView.VERTICAL
|
||||
)
|
||||
)
|
||||
recyclerView_chapters.setHasFixedSize(true)
|
||||
recyclerView_chapters.adapter = adapter
|
||||
chaptersAdapter = ChaptersAdapter(this)
|
||||
selectionDecoration = ChaptersSelectionDecoration(view.context)
|
||||
with(recyclerView_chapters) {
|
||||
addItemDecoration(DividerItemDecoration(view.context, RecyclerView.VERTICAL))
|
||||
addItemDecoration(selectionDecoration!!)
|
||||
setHasFixedSize(true)
|
||||
adapter = chaptersAdapter
|
||||
}
|
||||
|
||||
viewModel.mangaData.observe(viewLifecycleOwner, this::onMangaUpdated)
|
||||
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
|
||||
viewModel.history.observe(viewLifecycleOwner, this::onHistoryChanged)
|
||||
viewModel.newChapters.observe(viewLifecycleOwner, this::onNewChaptersChanged)
|
||||
viewModel.chapters.observe(viewLifecycleOwner, this::onChaptersChanged)
|
||||
}
|
||||
|
||||
private fun onMangaUpdated(manga: Manga) {
|
||||
this.manga = manga
|
||||
adapter.replaceData(manga.chapters.orEmpty())
|
||||
scrollToCurrent()
|
||||
override fun onDestroyView() {
|
||||
chaptersAdapter = null
|
||||
selectionDecoration = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun onChaptersChanged(list: List<ChapterListItem>) {
|
||||
chaptersAdapter?.items = list
|
||||
}
|
||||
|
||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
progressBar.isVisible = isLoading
|
||||
}
|
||||
|
||||
private fun onHistoryChanged(history: MangaHistory?) {
|
||||
adapter.currentChapterId = history?.chapterId
|
||||
scrollToCurrent()
|
||||
}
|
||||
|
||||
private fun onNewChaptersChanged(newChapters: Int) {
|
||||
adapter.newChaptersCount = newChapters
|
||||
}
|
||||
|
||||
override fun onItemClick(item: MangaChapter, position: Int, view: View) {
|
||||
if (adapter.checkedItemsCount != 0) {
|
||||
adapter.toggleItemChecked(item.id)
|
||||
if (adapter.checkedItemsCount == 0) {
|
||||
override fun onItemClick(item: MangaChapter, view: View) {
|
||||
if (selectionDecoration?.checkedItemsCount != 0) {
|
||||
selectionDecoration?.toggleItemChecked(item.id)
|
||||
if (selectionDecoration?.checkedItemsCount == 0) {
|
||||
actionMode?.finish()
|
||||
} else {
|
||||
actionMode?.invalidate()
|
||||
recyclerView_chapters.invalidateItemDecorations()
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -91,44 +82,38 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters),
|
||||
startActivity(
|
||||
ReaderActivity.newIntent(
|
||||
context ?: return,
|
||||
manga ?: return,
|
||||
viewModel.manga.value ?: return,
|
||||
item.id
|
||||
), options.toBundle()
|
||||
)
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: MangaChapter, position: Int, view: View): Boolean {
|
||||
override fun onItemLongClick(item: MangaChapter, view: View): Boolean {
|
||||
if (actionMode == null) {
|
||||
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
|
||||
}
|
||||
return actionMode?.also {
|
||||
adapter.setItemIsChecked(item.id, true)
|
||||
selectionDecoration?.setItemIsChecked(item.id, true)
|
||||
recyclerView_chapters.invalidateItemDecorations()
|
||||
it.invalidate()
|
||||
} != null
|
||||
}
|
||||
|
||||
private fun scrollToCurrent() {
|
||||
val pos = (recyclerView_chapters.adapter as? ChaptersAdapter)?.currentChapterPosition
|
||||
?: RecyclerView.NO_POSITION
|
||||
if (pos != RecyclerView.NO_POSITION) {
|
||||
(recyclerView_chapters.layoutManager as? LinearLayoutManager)
|
||||
?.scrollToPositionWithOffset(pos, resources.resolveDp(40))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_save -> {
|
||||
DownloadService.start(
|
||||
context ?: return false,
|
||||
manga ?: return false,
|
||||
adapter.checkedItemsIds
|
||||
viewModel.manga.value ?: return false,
|
||||
selectionDecoration?.checkedItemsIds
|
||||
)
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
R.id.action_select_all -> {
|
||||
adapter.checkAll()
|
||||
val ids = chaptersAdapter?.items?.map { it.chapter.id } ?: return false
|
||||
selectionDecoration?.checkAll(ids)
|
||||
recyclerView_chapters.invalidateItemDecorations()
|
||||
mode.invalidate()
|
||||
true
|
||||
}
|
||||
@@ -137,6 +122,7 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters),
|
||||
}
|
||||
|
||||
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
|
||||
@@ -144,18 +130,19 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters),
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
val count = adapter.checkedItemsCount
|
||||
val count = selectionDecoration?.checkedItemsCount ?: return false
|
||||
mode.subtitle = resources.getQuantityString(
|
||||
R.plurals.chapters_from_x,
|
||||
count,
|
||||
count,
|
||||
adapter.itemCount
|
||||
chaptersAdapter?.itemCount ?: 0
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
adapter.clearChecked()
|
||||
selectionDecoration?.clearSelection()
|
||||
recyclerView_chapters.invalidateItemDecorations()
|
||||
actionMode = null
|
||||
}
|
||||
}
|
||||
@@ -7,20 +7,22 @@ import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.net.toFile
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import kotlinx.android.synthetic.main.activity_details.*
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.viewmodel.ext.android.viewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.browser.BrowserActivity
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
@@ -34,9 +36,9 @@ import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||
|
||||
class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrategy {
|
||||
|
||||
private val viewModel by viewModel<DetailsViewModel>()
|
||||
|
||||
private var manga: Manga? = null
|
||||
private val viewModel by viewModel<DetailsViewModel> {
|
||||
parametersOf(MangaIntent.from(intent))
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -44,22 +46,14 @@ class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrate
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
pager.adapter = MangaDetailsAdapter(this)
|
||||
TabLayoutMediator(tabs, pager, this).attach()
|
||||
if (savedInstanceState == null) {
|
||||
intent?.getParcelableExtra<Manga>(EXTRA_MANGA)?.let {
|
||||
viewModel.loadDetails(it, true)
|
||||
} ?: intent?.getLongExtra(EXTRA_MANGA_ID, 0)?.takeUnless { it == 0L }?.let {
|
||||
viewModel.findMangaById(it)
|
||||
} ?: finishAfterTransition()
|
||||
}
|
||||
|
||||
viewModel.mangaData.observe(this, ::onMangaUpdated)
|
||||
viewModel.manga.observe(this, ::onMangaUpdated)
|
||||
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
|
||||
viewModel.onMangaRemoved.observe(this, ::onMangaRemoved)
|
||||
viewModel.onError.observe(this, ::onError)
|
||||
viewModel.newChapters.observe(this, ::onNewChaptersChanged)
|
||||
}
|
||||
|
||||
private fun onMangaUpdated(manga: Manga) {
|
||||
this.manga = manga
|
||||
title = manga.title
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
@@ -73,7 +67,7 @@ class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrate
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
if (manga == null) {
|
||||
if (viewModel.manga.value == null) {
|
||||
Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
|
||||
finishAfterTransition()
|
||||
} else {
|
||||
@@ -98,8 +92,9 @@ class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrate
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
val manga = viewModel.manga.value
|
||||
menu.findItem(R.id.action_save).isVisible =
|
||||
manga?.source != null && manga?.source != MangaSource.LOCAL
|
||||
manga?.source != null && manga.source != MangaSource.LOCAL
|
||||
menu.findItem(R.id.action_delete).isVisible =
|
||||
manga?.source == MangaSource.LOCAL
|
||||
menu.findItem(R.id.action_browser).isVisible =
|
||||
@@ -111,7 +106,7 @@ class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrate
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.action_share -> {
|
||||
manga?.let {
|
||||
viewModel.manga.value?.let {
|
||||
if (it.source == MangaSource.LOCAL) {
|
||||
ShareHelper.shareCbz(this, Uri.parse(it.url).toFile())
|
||||
} else {
|
||||
@@ -121,8 +116,8 @@ class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrate
|
||||
true
|
||||
}
|
||||
R.id.action_delete -> {
|
||||
manga?.let { m ->
|
||||
MaterialAlertDialogBuilder(this)
|
||||
viewModel.manga.value?.let { m ->
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.delete_manga)
|
||||
.setMessage(getString(R.string.text_delete_local_manga, m.title))
|
||||
.setPositiveButton(R.string.delete) { _, _ ->
|
||||
@@ -134,10 +129,10 @@ class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrate
|
||||
true
|
||||
}
|
||||
R.id.action_save -> {
|
||||
manga?.let {
|
||||
viewModel.manga.value?.let {
|
||||
val chaptersCount = it.chapters?.size ?: 0
|
||||
if (chaptersCount > 5) {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.save_manga)
|
||||
.setMessage(
|
||||
getString(
|
||||
@@ -160,19 +155,19 @@ class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrate
|
||||
true
|
||||
}
|
||||
R.id.action_browser -> {
|
||||
manga?.let {
|
||||
viewModel.manga.value?.let {
|
||||
startActivity(BrowserActivity.newIntent(this, it.url))
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.action_related -> {
|
||||
manga?.let {
|
||||
viewModel.manga.value?.let {
|
||||
startActivity(GlobalSearchActivity.newIntent(this, it.title))
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.action_shortcut -> {
|
||||
manga?.let {
|
||||
viewModel.manga.value?.let {
|
||||
lifecycleScope.launch {
|
||||
if (!MangaShortcut(it).requestPinShortcut(this@DetailsActivity)) {
|
||||
Snackbar.make(
|
||||
@@ -192,7 +187,6 @@ class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrate
|
||||
tab.text = when (position) {
|
||||
0 -> getString(R.string.details)
|
||||
1 -> getString(R.string.chapters)
|
||||
2 -> getString(R.string.related)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -211,17 +205,14 @@ class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrate
|
||||
|
||||
companion object {
|
||||
|
||||
private const val EXTRA_MANGA = "manga"
|
||||
const val EXTRA_MANGA_ID = "manga_id"
|
||||
|
||||
const val ACTION_MANGA_VIEW = "${BuildConfig.APPLICATION_ID}.action.VIEW_MANGA"
|
||||
|
||||
fun newIntent(context: Context, manga: Manga) =
|
||||
Intent(context, DetailsActivity::class.java)
|
||||
.putExtra(EXTRA_MANGA, manga)
|
||||
.putExtra(MangaIntent.KEY_MANGA, manga)
|
||||
|
||||
fun newIntent(context: Context, mangaId: Long) =
|
||||
Intent(context, DetailsActivity::class.java)
|
||||
.putExtra(EXTRA_MANGA_ID, mangaId)
|
||||
.putExtra(MangaIntent.KEY_ID, mangaId)
|
||||
}
|
||||
}
|
||||
@@ -29,23 +29,19 @@ class DetailsFragment : BaseFragment(R.layout.fragment_details), View.OnClickLis
|
||||
|
||||
private val viewModel by sharedViewModel<DetailsViewModel>()
|
||||
|
||||
private var manga: Manga? = null
|
||||
private var history: MangaHistory? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.mangaData.observe(viewLifecycleOwner, ::onMangaUpdated)
|
||||
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
|
||||
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
||||
viewModel.favouriteCategories.observe(viewLifecycleOwner, ::onFavouriteChanged)
|
||||
viewModel.history.observe(viewLifecycleOwner, ::onHistoryChanged)
|
||||
viewModel.readingHistory.observe(viewLifecycleOwner, ::onHistoryChanged)
|
||||
}
|
||||
|
||||
private fun onMangaUpdated(manga: Manga) {
|
||||
this.manga = manga
|
||||
imageView_cover.newImageRequest(manga.largeCoverUrl ?: manga.coverUrl)
|
||||
.fallback(R.drawable.ic_placeholder)
|
||||
.crossfade(true)
|
||||
.lifecycle(this)
|
||||
.lifecycle(viewLifecycleOwner)
|
||||
.enqueueWith(coil)
|
||||
textView_title.text = manga.title
|
||||
textView_subtitle.textAndVisible = manga.altTitle
|
||||
@@ -94,12 +90,19 @@ class DetailsFragment : BaseFragment(R.layout.fragment_details), View.OnClickLis
|
||||
imageView_favourite.setOnClickListener(this)
|
||||
button_read.setOnClickListener(this)
|
||||
button_read.setOnLongClickListener(this)
|
||||
updateReadButton()
|
||||
button_read.isEnabled = !manga.chapters.isNullOrEmpty()
|
||||
}
|
||||
|
||||
private fun onHistoryChanged(history: MangaHistory?) {
|
||||
this.history = history
|
||||
updateReadButton()
|
||||
with(button_read) {
|
||||
if (history == null) {
|
||||
setText(R.string.read)
|
||||
setIconResource(R.drawable.ic_read)
|
||||
} else {
|
||||
setText(R.string._continue)
|
||||
setIconResource(R.drawable.ic_play)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFavouriteChanged(categories: List<FavouriteCategory>) {
|
||||
@@ -117,6 +120,7 @@ class DetailsFragment : BaseFragment(R.layout.fragment_details), View.OnClickLis
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
val manga = viewModel.manga.value
|
||||
when {
|
||||
v.id == R.id.imageView_favourite -> {
|
||||
FavouriteCategoriesDialog.show(childFragmentManager, manga ?: return)
|
||||
@@ -126,7 +130,7 @@ class DetailsFragment : BaseFragment(R.layout.fragment_details), View.OnClickLis
|
||||
ReaderActivity.newIntent(
|
||||
context ?: return,
|
||||
manga ?: return,
|
||||
history
|
||||
viewModel.readingHistory.value
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -145,7 +149,7 @@ class DetailsFragment : BaseFragment(R.layout.fragment_details), View.OnClickLis
|
||||
override fun onLongClick(v: View): Boolean {
|
||||
when (v.id) {
|
||||
R.id.button_read -> {
|
||||
if (history == null) {
|
||||
if (viewModel.readingHistory.value == null) {
|
||||
return false
|
||||
}
|
||||
v.showPopupMenu(R.menu.popup_read) {
|
||||
@@ -154,7 +158,7 @@ class DetailsFragment : BaseFragment(R.layout.fragment_details), View.OnClickLis
|
||||
startActivity(
|
||||
ReaderActivity.newIntent(
|
||||
context ?: return@showPopupMenu false,
|
||||
manga ?: return@showPopupMenu false
|
||||
viewModel.manga.value ?: return@showPopupMenu false
|
||||
)
|
||||
)
|
||||
true
|
||||
@@ -167,19 +171,4 @@ class DetailsFragment : BaseFragment(R.layout.fragment_details), View.OnClickLis
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateReadButton() {
|
||||
if (manga?.chapters.isNullOrEmpty()) {
|
||||
button_read.isEnabled = false
|
||||
} else {
|
||||
button_read.isEnabled = true
|
||||
if (history == null) {
|
||||
button_read.setText(R.string.read)
|
||||
button_read.setIconResource(R.drawable.ic_read)
|
||||
} else {
|
||||
button_read.setText(R.string._continue)
|
||||
button_read.setIconResource(R.drawable.ic_play)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.flow.*
|
||||
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.core.exceptions.MangaNotFoundException
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.details.ui.model.toListItem
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.favourites.domain.OnFavouritesChangeListener
|
||||
import org.koitharu.kotatsu.history.domain.ChapterExtra
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.history.domain.OnHistoryChangeListener
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
@@ -20,88 +20,82 @@ import org.koitharu.kotatsu.utils.ext.safe
|
||||
import java.io.IOException
|
||||
|
||||
class DetailsViewModel(
|
||||
intent: MangaIntent,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val favouritesRepository: FavouritesRepository,
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
private val mangaDataRepository: MangaDataRepository
|
||||
) : BaseViewModel(), OnHistoryChangeListener, OnFavouritesChangeListener {
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val mangaData = MutableStateFlow<Manga?>(intent.manga)
|
||||
|
||||
private val history = mangaData.mapNotNull { it?.id }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { mangaId ->
|
||||
historyRepository.observeOne(mangaId)
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
|
||||
private val favourite = mangaData.mapNotNull { it?.id }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { mangaId ->
|
||||
favouritesRepository.observeCategories(mangaId)
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
private val newChapters = mangaData.mapNotNull { it?.id }
|
||||
.distinctUntilChanged()
|
||||
.mapLatest { mangaId ->
|
||||
trackingRepository.getNewChaptersCount(mangaId)
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, 0)
|
||||
|
||||
val manga = mangaData.filterNotNull()
|
||||
.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
val favouriteCategories = favourite
|
||||
.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
val newChaptersCount = newChapters
|
||||
.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
val readingHistory = history
|
||||
.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
val mangaData = MutableLiveData<Manga>()
|
||||
val newChapters = MutableLiveData<Int>(0)
|
||||
val onMangaRemoved = SingleLiveEvent<Manga>()
|
||||
val history = MutableLiveData<MangaHistory?>()
|
||||
val favouriteCategories = MutableLiveData<List<FavouriteCategory>>()
|
||||
|
||||
val chapters = combine(
|
||||
mangaData.map { it?.chapters.orEmpty() },
|
||||
history.map { it?.chapterId },
|
||||
newChapters
|
||||
) { chapters, currentId, newCount ->
|
||||
val currentIndex = chapters.indexOfFirst { it.id == currentId }
|
||||
val firstNewIndex = chapters.size - newCount
|
||||
chapters.mapIndexed { index, chapter ->
|
||||
chapter.toListItem(
|
||||
when {
|
||||
index >= firstNewIndex -> ChapterExtra.NEW
|
||||
index == currentIndex -> ChapterExtra.CURRENT
|
||||
index < currentIndex -> ChapterExtra.READ
|
||||
else -> ChapterExtra.UNREAD
|
||||
}
|
||||
)
|
||||
}
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
init {
|
||||
HistoryRepository.subscribe(this)
|
||||
FavouritesRepository.subscribe(this)
|
||||
}
|
||||
|
||||
fun findMangaById(id: Long) {
|
||||
launchLoadingJob {
|
||||
val manga = mangaDataRepository.findMangaById(id)
|
||||
?: throw MangaNotFoundException("Cannot find manga by id")
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
var manga = mangaDataRepository.resolveIntent(intent)
|
||||
?: throw MangaNotFoundException("Cannot find manga")
|
||||
mangaData.value = manga
|
||||
manga = manga.source.repository.getDetails(manga)
|
||||
mangaData.value = manga
|
||||
loadDetails(manga, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadDetails(manga: Manga, force: Boolean = false) {
|
||||
if (!force && mangaData.value == manga) {
|
||||
return
|
||||
}
|
||||
loadHistory(manga)
|
||||
mangaData.value = manga
|
||||
loadFavourite(manga)
|
||||
launchLoadingJob {
|
||||
val data = withContext(Dispatchers.Default) {
|
||||
manga.source.repository.getDetails(manga)
|
||||
}
|
||||
mangaData.value = data
|
||||
newChapters.value = trackingRepository.getNewChaptersCount(manga.id)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteLocal(manga: Manga) {
|
||||
launchLoadingJob {
|
||||
withContext(Dispatchers.Default) {
|
||||
val original = localMangaRepository.getRemoteManga(manga)
|
||||
localMangaRepository.delete(manga) || throw IOException("Unable to delete file")
|
||||
safe {
|
||||
historyRepository.deleteOrSwap(manga, original)
|
||||
}
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
val original = localMangaRepository.getRemoteManga(manga)
|
||||
localMangaRepository.delete(manga) || throw IOException("Unable to delete file")
|
||||
safe {
|
||||
historyRepository.deleteOrSwap(manga, original)
|
||||
}
|
||||
onMangaRemoved.call(manga)
|
||||
onMangaRemoved.postCall(manga)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadHistory(manga: Manga) {
|
||||
launchJob {
|
||||
history.value = historyRepository.getOne(manga)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadFavourite(manga: Manga) {
|
||||
launchJob {
|
||||
favouriteCategories.value = favouritesRepository.getCategories(manga.id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onHistoryChanged() {
|
||||
loadHistory(mangaData.value ?: return)
|
||||
}
|
||||
|
||||
override fun onFavouritesChanged(mangaId: Long) {
|
||||
val manga = mangaData.value ?: return
|
||||
if (mangaId == manga.id) {
|
||||
loadFavourite(manga)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
HistoryRepository.unsubscribe(this)
|
||||
FavouritesRepository.unsubscribe(this)
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,29 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
package org.koitharu.kotatsu.details.ui.adapter
|
||||
|
||||
import android.graphics.Color
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer
|
||||
import kotlinx.android.synthetic.main.item_chapter.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
|
||||
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.history.domain.ChapterExtra
|
||||
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||
|
||||
class ChapterHolder(parent: ViewGroup) :
|
||||
BaseViewHolder<MangaChapter, ChapterExtra>(parent, R.layout.item_chapter) {
|
||||
fun chapterListItemAD(
|
||||
clickListener: OnListItemClickListener<MangaChapter>
|
||||
) = adapterDelegateLayoutContainer<ChapterListItem, ChapterListItem>(R.layout.item_chapter) {
|
||||
|
||||
override fun onBind(data: MangaChapter, extra: ChapterExtra) {
|
||||
textView_title.text = data.name
|
||||
textView_number.text = data.number.toString()
|
||||
imageView_check.isVisible = extra == ChapterExtra.CHECKED
|
||||
when (extra) {
|
||||
itemView.setOnClickListener {
|
||||
clickListener.onItemClick(item.chapter, it)
|
||||
}
|
||||
itemView.setOnLongClickListener {
|
||||
clickListener.onItemLongClick(item.chapter, it)
|
||||
}
|
||||
|
||||
bind { payload ->
|
||||
textView_title.text = item.chapter.name
|
||||
textView_number.text = item.chapter.number.toString()
|
||||
when (item.extra) {
|
||||
ChapterExtra.UNREAD -> {
|
||||
textView_number.setBackgroundResource(R.drawable.bg_badge_default)
|
||||
textView_number.setTextColor(context.getThemeColor(android.R.attr.textColorSecondaryInverse))
|
||||
@@ -34,10 +40,6 @@ class ChapterHolder(parent: ViewGroup) :
|
||||
textView_number.setBackgroundResource(R.drawable.bg_badge_accent)
|
||||
textView_number.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse))
|
||||
}
|
||||
ChapterExtra.CHECKED -> {
|
||||
textView_number.background = null
|
||||
textView_number.setTextColor(Color.TRANSPARENT)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
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>
|
||||
) : AsyncListDifferDelegationAdapter<ChapterListItem>(DiffCallback()) {
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
delegatesManager.addDelegate(chapterListItemAD(onItemClickListener))
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return items[position].chapter.id
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<ChapterListItem>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: ChapterListItem, newItem: ChapterListItem): Boolean {
|
||||
return oldItem.chapter.id == newItem.chapter.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: ChapterListItem, newItem: ChapterListItem): Boolean {
|
||||
return Intrinsics.areEqual(oldItem, newItem)
|
||||
}
|
||||
|
||||
override fun getChangePayload(oldItem: ChapterListItem, newItem: ChapterListItem): Any? {
|
||||
if (oldItem.extra != newItem.extra) {
|
||||
return newItem.extra
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.koitharu.kotatsu.details.ui.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import androidx.collection.ArraySet
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.children
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.utils.ext.resolveDp
|
||||
|
||||
class ChaptersSelectionDecoration(context: Context) : RecyclerView.ItemDecoration() {
|
||||
|
||||
private val icon = ContextCompat.getDrawable(context, R.drawable.ic_check)
|
||||
private val padding = context.resources.resolveDp(16)
|
||||
private val bounds = Rect()
|
||||
private val selection = ArraySet<Long>()
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
|
||||
init {
|
||||
paint.color = context.getThemeColor(android.R.attr.colorControlActivated)
|
||||
paint.style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
val checkedItemsCount: Int
|
||||
get() = selection.size
|
||||
|
||||
val checkedItemsIds: Set<Long>
|
||||
get() = selection
|
||||
|
||||
fun toggleItemChecked(id: Long) {
|
||||
if (!selection.remove(id)) {
|
||||
selection.add(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun setItemIsChecked(id: Long, isChecked: Boolean) {
|
||||
if (isChecked) {
|
||||
selection.add(id)
|
||||
} else {
|
||||
selection.remove(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun checkAll(ids: Collection<Long>) {
|
||||
selection.addAll(ids)
|
||||
}
|
||||
|
||||
fun clearSelection() {
|
||||
selection.clear()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
icon ?: return
|
||||
canvas.save()
|
||||
if (parent.clipToPadding) {
|
||||
canvas.clipRect(
|
||||
parent.paddingLeft, parent.paddingTop, parent.width - parent.paddingRight,
|
||||
parent.height - parent.paddingBottom
|
||||
)
|
||||
}
|
||||
|
||||
for (child in parent.children) {
|
||||
val itemId = parent.getChildItemId(child)
|
||||
if (itemId in selection) {
|
||||
parent.getDecoratedBoundsWithMargins(child, bounds)
|
||||
bounds.offset(child.translationX.toInt(), child.translationY.toInt())
|
||||
canvas.drawRect(bounds, paint)
|
||||
}
|
||||
}
|
||||
canvas.restore()
|
||||
}
|
||||
|
||||
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
icon ?: return
|
||||
canvas.save()
|
||||
val left: Int
|
||||
val right: Int
|
||||
if (parent.clipToPadding) {
|
||||
left = parent.paddingLeft
|
||||
right = parent.width - parent.paddingRight
|
||||
canvas.clipRect(
|
||||
left, parent.paddingTop, right,
|
||||
parent.height - parent.paddingBottom
|
||||
)
|
||||
} else {
|
||||
left = 0
|
||||
right = parent.width
|
||||
}
|
||||
|
||||
for (child in parent.children) {
|
||||
val itemId = parent.getChildItemId(child)
|
||||
if (itemId in selection) {
|
||||
parent.getDecoratedBoundsWithMargins(child, bounds)
|
||||
bounds.offset(child.translationX.toInt(), child.translationY.toInt())
|
||||
val hh = (bounds.height() - icon.intrinsicHeight) / 2
|
||||
val top: Int = bounds.top + hh
|
||||
val bottom: Int = bounds.bottom - hh
|
||||
icon.setBounds(right - icon.intrinsicWidth - padding, top, right - padding, bottom)
|
||||
icon.draw(canvas)
|
||||
}
|
||||
}
|
||||
canvas.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.koitharu.kotatsu.details.ui.model
|
||||
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.history.domain.ChapterExtra
|
||||
|
||||
data class ChapterListItem(
|
||||
val chapter: MangaChapter,
|
||||
val extra: ChapterExtra
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
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(
|
||||
chapter = this,
|
||||
extra = extra
|
||||
)
|
||||
@@ -38,6 +38,10 @@ abstract class FavouritesDao {
|
||||
@Query("SELECT * FROM favourites WHERE manga_id = :id GROUP BY manga_id")
|
||||
abstract suspend fun find(id: Long): FavouriteManga?
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM favourites WHERE manga_id = :id GROUP BY manga_id")
|
||||
abstract fun observe(id: Long): Flow<FavouriteManga?>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract suspend fun insert(favourite: FavouriteEntity)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.collection.ArraySet
|
||||
import androidx.room.withTransaction
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
@@ -57,6 +58,12 @@ class FavouritesRepository(private val db: MangaDatabase) {
|
||||
return entities?.map { it.toFavouriteCategory() }.orEmpty()
|
||||
}
|
||||
|
||||
fun observeCategories(mangaId: Long): Flow<List<FavouriteCategory>> {
|
||||
return db.favouritesDao.observe(mangaId).map { entity ->
|
||||
entity?.categories?.map { it.toFavouriteCategory() }.orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addCategory(title: String): FavouriteCategory {
|
||||
val entity = FavouriteCategoryEntity(
|
||||
title = title,
|
||||
|
||||
@@ -25,6 +25,9 @@ abstract class HistoryDao {
|
||||
@Query("SELECT * FROM history WHERE manga_id = :id")
|
||||
abstract suspend fun find(id: Long): HistoryEntity?
|
||||
|
||||
@Query("SELECT * FROM history WHERE manga_id = :id")
|
||||
abstract fun observe(id: Long): Flow<HistoryEntity?>
|
||||
|
||||
@Query("DELETE FROM history")
|
||||
abstract suspend fun clear()
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@ package org.koitharu.kotatsu.history.domain
|
||||
|
||||
enum class ChapterExtra {
|
||||
|
||||
READ, CURRENT, UNREAD, NEW, CHECKED
|
||||
READ, CURRENT, UNREAD, NEW
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import androidx.collection.ArraySet
|
||||
import androidx.room.withTransaction
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
@@ -32,6 +33,12 @@ class HistoryRepository(private val db: MangaDatabase) : KoinComponent {
|
||||
}
|
||||
}
|
||||
|
||||
fun observeOne(id: Long): Flow<MangaHistory?> {
|
||||
return db.historyDao.observe(id).map {
|
||||
it?.toMangaHistory()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int, scroll: Int) {
|
||||
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
|
||||
db.withTransaction {
|
||||
|
||||
@@ -5,18 +5,17 @@ import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.dialog_chapters.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.details.ui.ChaptersAdapter
|
||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
class ChaptersDialog : AlertDialogFragment(R.layout.dialog_chapters),
|
||||
OnRecyclerItemClickListener<MangaChapter> {
|
||||
OnListItemClickListener<MangaChapter> {
|
||||
|
||||
override fun onBuildDialog(builder: AlertDialog.Builder) {
|
||||
builder.setTitle(R.string.chapters)
|
||||
@@ -32,22 +31,12 @@ class ChaptersDialog : AlertDialogFragment(R.layout.dialog_chapters),
|
||||
)
|
||||
)
|
||||
recyclerView_chapters.adapter = ChaptersAdapter(this).apply {
|
||||
arguments?.getParcelableArrayList<MangaChapter>(ARG_CHAPTERS)?.let(this::replaceData)
|
||||
currentChapterId = arguments?.getLong(ARG_CURRENT_ID, 0L)?.takeUnless { it == 0L }
|
||||
// arguments?.getParcelableArrayList<MangaChapter>(ARG_CHAPTERS)?.let(this::setItems)
|
||||
// currentChapterId = arguments?.getLong(ARG_CURRENT_ID, 0L)?.takeUnless { it == 0L }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val pos = (recyclerView_chapters.adapter as? ChaptersAdapter)?.currentChapterPosition
|
||||
?: RecyclerView.NO_POSITION
|
||||
if (pos != RecyclerView.NO_POSITION) {
|
||||
(recyclerView_chapters.layoutManager as? LinearLayoutManager)
|
||||
?.scrollToPositionWithOffset(pos, 100)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(item: MangaChapter, position: Int, view: View) {
|
||||
override fun onItemClick(item: MangaChapter, view: View) {
|
||||
((parentFragment as? OnChapterChangeListener)
|
||||
?: (activity as? OnChapterChangeListener))?.let {
|
||||
dismiss()
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class SelectionController {
|
||||
|
||||
private val state = MutableStateFlow(emptySet<Int>())
|
||||
|
||||
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import coil.executeBlocking
|
||||
import coil.request.ImageRequest
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.utils.ext.requireBitmap
|
||||
import java.io.IOException
|
||||
@@ -52,7 +52,7 @@ class RecentListFactory(
|
||||
views.setImageViewResource(R.id.imageView_cover, R.drawable.ic_placeholder)
|
||||
}
|
||||
val intent = Intent()
|
||||
intent.putExtra(DetailsActivity.EXTRA_MANGA_ID, item.id)
|
||||
intent.putExtra(MangaIntent.KEY_ID, item.id)
|
||||
views.setOnClickFillInIntent(R.id.imageView_cover, intent)
|
||||
return views
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ import coil.executeBlocking
|
||||
import coil.request.ImageRequest
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.prefs.AppWidgetConfig
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.utils.ext.requireBitmap
|
||||
import java.io.IOException
|
||||
@@ -63,7 +63,7 @@ class ShelfListFactory(
|
||||
views.setImageViewResource(R.id.imageView_cover, R.drawable.ic_placeholder)
|
||||
}
|
||||
val intent = Intent()
|
||||
intent.putExtra(DetailsActivity.EXTRA_MANGA_ID, item.id)
|
||||
intent.putExtra(MangaIntent.KEY_ID, item.id)
|
||||
views.setOnClickFillInIntent(R.id.rootLayout, intent)
|
||||
return views
|
||||
}
|
||||
|
||||
@@ -21,20 +21,6 @@
|
||||
android:textColor="?android:textColorSecondaryInverse"
|
||||
tools:text="13" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView_check"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_toStartOf="@id/textView_title"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="centerInside"
|
||||
android:src="@drawable/ic_check"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
Reference in New Issue
Block a user