Migrate details to AdapterDelegates and mvvm

This commit is contained in:
Koitharu
2020-11-24 06:58:16 +02:00
parent 1b1540b35b
commit fa02cfd7e8
25 changed files with 427 additions and 338 deletions

View File

@@ -32,6 +32,12 @@ class MangaDataRepository(private val db: MangaDatabase) {
return db.mangaDao.find(mangaId)?.toManga() 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) { suspend fun storeManga(manga: Manga) {
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag) val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
db.withTransaction { db.withTransaction {

View File

@@ -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"
}
}

View File

@@ -6,6 +6,8 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
abstract class BaseViewModel : ViewModel() { abstract class BaseViewModel : ViewModel() {
@@ -13,19 +15,21 @@ abstract class BaseViewModel : ViewModel() {
val isLoading = MutableLiveData(false) val isLoading = MutableLiveData(false)
protected fun launchJob( protected fun launchJob(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT, start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit block: suspend CoroutineScope.() -> Unit
): Job = viewModelScope.launch(createErrorHandler(), start, block) ): Job = viewModelScope.launch(context + createErrorHandler(), start, block)
protected fun launchLoadingJob( protected fun launchLoadingJob(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT, start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit block: suspend CoroutineScope.() -> Unit
): Job = viewModelScope.launch(createErrorHandler(), start) { ): Job = viewModelScope.launch(context + createErrorHandler(), start) {
isLoading.value = true isLoading.postValue(true)
try { try {
block() block()
} finally { } finally {
isLoading.value = false isLoading.postValue(false)
} }
} }
@@ -34,7 +38,7 @@ abstract class BaseViewModel : ViewModel() {
throwable.printStackTrace() throwable.printStackTrace()
} }
if (throwable !is CancellationException) { if (throwable !is CancellationException) {
onError.call(throwable) onError.postCall(throwable)
} }
} }
} }

View File

@@ -5,6 +5,7 @@ import androidx.recyclerview.widget.RecyclerView
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koitharu.kotatsu.utils.ext.replaceWith import org.koitharu.kotatsu.utils.ext.replaceWith
@Deprecated("", replaceWith = ReplaceWith("AsyncListDifferDelegationAdapter"))
abstract class BaseRecyclerAdapter<T, E>(private val onItemClickListener: OnRecyclerItemClickListener<T>? = null) : abstract class BaseRecyclerAdapter<T, E>(private val onItemClickListener: OnRecyclerItemClickListener<T>? = null) :
RecyclerView.Adapter<BaseViewHolder<T, E>>(), RecyclerView.Adapter<BaseViewHolder<T, E>>(),
KoinComponent { KoinComponent {

View File

@@ -2,10 +2,13 @@ package org.koitharu.kotatsu.details
import org.koin.android.viewmodel.dsl.viewModel import org.koin.android.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.details.ui.DetailsViewModel import org.koitharu.kotatsu.details.ui.DetailsViewModel
val detailsModule val detailsModule
get() = module { get() = module {
viewModel { DetailsViewModel(get(), get(), get(), get(), get()) } viewModel { (intent: MangaIntent) ->
DetailsViewModel(intent, get(), get(), get(), get(), get())
}
} }

View File

@@ -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()
}
}
}

View File

@@ -9,75 +9,66 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.fragment_chapters.* import kotlinx.android.synthetic.main.fragment_chapters.*
import org.koin.android.viewmodel.ext.android.sharedViewModel import org.koin.android.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.OnRecyclerItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter 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.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.download.DownloadService
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.utils.ext.resolveDp
class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), class ChaptersFragment : BaseFragment(R.layout.fragment_chapters),
OnRecyclerItemClickListener<MangaChapter>, ActionMode.Callback { OnListItemClickListener<MangaChapter>, ActionMode.Callback {
private val viewModel by sharedViewModel<DetailsViewModel>() private val viewModel by sharedViewModel<DetailsViewModel>()
private var manga: Manga? = null private var chaptersAdapter: ChaptersAdapter? = null
private lateinit var adapter: ChaptersAdapter
private var actionMode: ActionMode? = null private var actionMode: ActionMode? = null
private var selectionDecoration: ChaptersSelectionDecoration? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
adapter = ChaptersAdapter(this) chaptersAdapter = ChaptersAdapter(this)
recyclerView_chapters.addItemDecoration( selectionDecoration = ChaptersSelectionDecoration(view.context)
DividerItemDecoration( with(recyclerView_chapters) {
view.context, addItemDecoration(DividerItemDecoration(view.context, RecyclerView.VERTICAL))
RecyclerView.VERTICAL addItemDecoration(selectionDecoration!!)
) setHasFixedSize(true)
) adapter = chaptersAdapter
recyclerView_chapters.setHasFixedSize(true) }
recyclerView_chapters.adapter = adapter
viewModel.mangaData.observe(viewLifecycleOwner, this::onMangaUpdated)
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged) viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
viewModel.history.observe(viewLifecycleOwner, this::onHistoryChanged) viewModel.chapters.observe(viewLifecycleOwner, this::onChaptersChanged)
viewModel.newChapters.observe(viewLifecycleOwner, this::onNewChaptersChanged)
} }
private fun onMangaUpdated(manga: Manga) { override fun onDestroyView() {
this.manga = manga chaptersAdapter = null
adapter.replaceData(manga.chapters.orEmpty()) selectionDecoration = null
scrollToCurrent() super.onDestroyView()
}
private fun onChaptersChanged(list: List<ChapterListItem>) {
chaptersAdapter?.items = list
} }
private fun onLoadingStateChanged(isLoading: Boolean) { private fun onLoadingStateChanged(isLoading: Boolean) {
progressBar.isVisible = isLoading progressBar.isVisible = isLoading
} }
private fun onHistoryChanged(history: MangaHistory?) { override fun onItemClick(item: MangaChapter, view: View) {
adapter.currentChapterId = history?.chapterId if (selectionDecoration?.checkedItemsCount != 0) {
scrollToCurrent() selectionDecoration?.toggleItemChecked(item.id)
} if (selectionDecoration?.checkedItemsCount == 0) {
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) {
actionMode?.finish() actionMode?.finish()
} else { } else {
actionMode?.invalidate() actionMode?.invalidate()
recyclerView_chapters.invalidateItemDecorations()
} }
return return
} }
@@ -91,44 +82,38 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters),
startActivity( startActivity(
ReaderActivity.newIntent( ReaderActivity.newIntent(
context ?: return, context ?: return,
manga ?: return, viewModel.manga.value ?: return,
item.id item.id
), options.toBundle() ), options.toBundle()
) )
} }
override fun onItemLongClick(item: MangaChapter, position: Int, view: View): Boolean { override fun onItemLongClick(item: MangaChapter, 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 {
adapter.setItemIsChecked(item.id, true) selectionDecoration?.setItemIsChecked(item.id, true)
recyclerView_chapters.invalidateItemDecorations()
it.invalidate() it.invalidate()
} != null } != 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 { override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.action_save -> { R.id.action_save -> {
DownloadService.start( DownloadService.start(
context ?: return false, context ?: return false,
manga ?: return false, viewModel.manga.value ?: return false,
adapter.checkedItemsIds selectionDecoration?.checkedItemsIds
) )
mode.finish() mode.finish()
true true
} }
R.id.action_select_all -> { 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() mode.invalidate()
true true
} }
@@ -137,6 +122,7 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters),
} }
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
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 menu.findItem(R.id.action_save).isVisible = manga?.source != MangaSource.LOCAL
mode.title = manga?.title mode.title = manga?.title
@@ -144,18 +130,19 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters),
} }
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
val count = adapter.checkedItemsCount val count = selectionDecoration?.checkedItemsCount ?: return false
mode.subtitle = resources.getQuantityString( mode.subtitle = resources.getQuantityString(
R.plurals.chapters_from_x, R.plurals.chapters_from_x,
count, count,
count, count,
adapter.itemCount chaptersAdapter?.itemCount ?: 0
) )
return true return true
} }
override fun onDestroyActionMode(mode: ActionMode?) { override fun onDestroyActionMode(mode: ActionMode?) {
adapter.clearChecked() selectionDecoration?.clearSelection()
recyclerView_chapters.invalidateItemDecorations()
actionMode = null actionMode = null
} }
} }

View File

@@ -7,20 +7,22 @@ import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.android.synthetic.main.activity_details.* import kotlinx.android.synthetic.main.activity_details.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.viewmodel.ext.android.viewModel import org.koin.android.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.browser.BrowserActivity import org.koitharu.kotatsu.browser.BrowserActivity
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
@@ -34,9 +36,9 @@ import org.koitharu.kotatsu.utils.ext.getThemeColor
class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrategy { class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrategy {
private val viewModel by viewModel<DetailsViewModel>() private val viewModel by viewModel<DetailsViewModel> {
parametersOf(MangaIntent.from(intent))
private var manga: Manga? = null }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -44,22 +46,14 @@ class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrate
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
pager.adapter = MangaDetailsAdapter(this) pager.adapter = MangaDetailsAdapter(this)
TabLayoutMediator(tabs, pager, this).attach() 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.onMangaRemoved.observe(this, ::onMangaRemoved)
viewModel.onError.observe(this, ::onError) viewModel.onError.observe(this, ::onError)
viewModel.newChapters.observe(this, ::onNewChaptersChanged)
} }
private fun onMangaUpdated(manga: Manga) { private fun onMangaUpdated(manga: Manga) {
this.manga = manga
title = manga.title title = manga.title
invalidateOptionsMenu() invalidateOptionsMenu()
} }
@@ -73,7 +67,7 @@ class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrate
} }
private fun onError(e: Throwable) { private fun onError(e: Throwable) {
if (manga == null) { if (viewModel.manga.value == null) {
Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show() Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
finishAfterTransition() finishAfterTransition()
} else { } else {
@@ -98,8 +92,9 @@ class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrate
} }
override fun onPrepareOptionsMenu(menu: Menu): Boolean { override fun onPrepareOptionsMenu(menu: Menu): Boolean {
val manga = viewModel.manga.value
menu.findItem(R.id.action_save).isVisible = 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 = menu.findItem(R.id.action_delete).isVisible =
manga?.source == MangaSource.LOCAL manga?.source == MangaSource.LOCAL
menu.findItem(R.id.action_browser).isVisible = menu.findItem(R.id.action_browser).isVisible =
@@ -111,7 +106,7 @@ class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrate
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_share -> { R.id.action_share -> {
manga?.let { viewModel.manga.value?.let {
if (it.source == MangaSource.LOCAL) { if (it.source == MangaSource.LOCAL) {
ShareHelper.shareCbz(this, Uri.parse(it.url).toFile()) ShareHelper.shareCbz(this, Uri.parse(it.url).toFile())
} else { } else {
@@ -121,8 +116,8 @@ class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrate
true true
} }
R.id.action_delete -> { R.id.action_delete -> {
manga?.let { m -> viewModel.manga.value?.let { m ->
MaterialAlertDialogBuilder(this) AlertDialog.Builder(this)
.setTitle(R.string.delete_manga) .setTitle(R.string.delete_manga)
.setMessage(getString(R.string.text_delete_local_manga, m.title)) .setMessage(getString(R.string.text_delete_local_manga, m.title))
.setPositiveButton(R.string.delete) { _, _ -> .setPositiveButton(R.string.delete) { _, _ ->
@@ -134,10 +129,10 @@ class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrate
true true
} }
R.id.action_save -> { R.id.action_save -> {
manga?.let { viewModel.manga.value?.let {
val chaptersCount = it.chapters?.size ?: 0 val chaptersCount = it.chapters?.size ?: 0
if (chaptersCount > 5) { if (chaptersCount > 5) {
MaterialAlertDialogBuilder(this) AlertDialog.Builder(this)
.setTitle(R.string.save_manga) .setTitle(R.string.save_manga)
.setMessage( .setMessage(
getString( getString(
@@ -160,19 +155,19 @@ class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrate
true true
} }
R.id.action_browser -> { R.id.action_browser -> {
manga?.let { viewModel.manga.value?.let {
startActivity(BrowserActivity.newIntent(this, it.url)) startActivity(BrowserActivity.newIntent(this, it.url))
} }
true true
} }
R.id.action_related -> { R.id.action_related -> {
manga?.let { viewModel.manga.value?.let {
startActivity(GlobalSearchActivity.newIntent(this, it.title)) startActivity(GlobalSearchActivity.newIntent(this, it.title))
} }
true true
} }
R.id.action_shortcut -> { R.id.action_shortcut -> {
manga?.let { viewModel.manga.value?.let {
lifecycleScope.launch { lifecycleScope.launch {
if (!MangaShortcut(it).requestPinShortcut(this@DetailsActivity)) { if (!MangaShortcut(it).requestPinShortcut(this@DetailsActivity)) {
Snackbar.make( Snackbar.make(
@@ -192,7 +187,6 @@ class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrate
tab.text = when (position) { tab.text = when (position) {
0 -> getString(R.string.details) 0 -> getString(R.string.details)
1 -> getString(R.string.chapters) 1 -> getString(R.string.chapters)
2 -> getString(R.string.related)
else -> null else -> null
} }
} }
@@ -211,17 +205,14 @@ class DetailsActivity : BaseActivity(), TabLayoutMediator.TabConfigurationStrate
companion object { 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" const val ACTION_MANGA_VIEW = "${BuildConfig.APPLICATION_ID}.action.VIEW_MANGA"
fun newIntent(context: Context, manga: Manga) = fun newIntent(context: Context, manga: Manga) =
Intent(context, DetailsActivity::class.java) Intent(context, DetailsActivity::class.java)
.putExtra(EXTRA_MANGA, manga) .putExtra(MangaIntent.KEY_MANGA, manga)
fun newIntent(context: Context, mangaId: Long) = fun newIntent(context: Context, mangaId: Long) =
Intent(context, DetailsActivity::class.java) Intent(context, DetailsActivity::class.java)
.putExtra(EXTRA_MANGA_ID, mangaId) .putExtra(MangaIntent.KEY_ID, mangaId)
} }
} }

View File

@@ -29,23 +29,19 @@ class DetailsFragment : BaseFragment(R.layout.fragment_details), View.OnClickLis
private val viewModel by sharedViewModel<DetailsViewModel>() private val viewModel by sharedViewModel<DetailsViewModel>()
private var manga: Manga? = null
private var history: MangaHistory? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel.mangaData.observe(viewLifecycleOwner, ::onMangaUpdated) viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged) viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
viewModel.favouriteCategories.observe(viewLifecycleOwner, ::onFavouriteChanged) viewModel.favouriteCategories.observe(viewLifecycleOwner, ::onFavouriteChanged)
viewModel.history.observe(viewLifecycleOwner, ::onHistoryChanged) viewModel.readingHistory.observe(viewLifecycleOwner, ::onHistoryChanged)
} }
private fun onMangaUpdated(manga: Manga) { private fun onMangaUpdated(manga: Manga) {
this.manga = manga
imageView_cover.newImageRequest(manga.largeCoverUrl ?: manga.coverUrl) imageView_cover.newImageRequest(manga.largeCoverUrl ?: manga.coverUrl)
.fallback(R.drawable.ic_placeholder) .fallback(R.drawable.ic_placeholder)
.crossfade(true) .crossfade(true)
.lifecycle(this) .lifecycle(viewLifecycleOwner)
.enqueueWith(coil) .enqueueWith(coil)
textView_title.text = manga.title textView_title.text = manga.title
textView_subtitle.textAndVisible = manga.altTitle textView_subtitle.textAndVisible = manga.altTitle
@@ -94,12 +90,19 @@ class DetailsFragment : BaseFragment(R.layout.fragment_details), View.OnClickLis
imageView_favourite.setOnClickListener(this) imageView_favourite.setOnClickListener(this)
button_read.setOnClickListener(this) button_read.setOnClickListener(this)
button_read.setOnLongClickListener(this) button_read.setOnLongClickListener(this)
updateReadButton() button_read.isEnabled = !manga.chapters.isNullOrEmpty()
} }
private fun onHistoryChanged(history: MangaHistory?) { private fun onHistoryChanged(history: MangaHistory?) {
this.history = history with(button_read) {
updateReadButton() 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>) { private fun onFavouriteChanged(categories: List<FavouriteCategory>) {
@@ -117,6 +120,7 @@ class DetailsFragment : BaseFragment(R.layout.fragment_details), View.OnClickLis
} }
override fun onClick(v: View) { override fun onClick(v: View) {
val manga = viewModel.manga.value
when { when {
v.id == R.id.imageView_favourite -> { v.id == R.id.imageView_favourite -> {
FavouriteCategoriesDialog.show(childFragmentManager, manga ?: return) FavouriteCategoriesDialog.show(childFragmentManager, manga ?: return)
@@ -126,7 +130,7 @@ class DetailsFragment : BaseFragment(R.layout.fragment_details), View.OnClickLis
ReaderActivity.newIntent( ReaderActivity.newIntent(
context ?: return, context ?: return,
manga ?: 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 { override fun onLongClick(v: View): Boolean {
when (v.id) { when (v.id) {
R.id.button_read -> { R.id.button_read -> {
if (history == null) { if (viewModel.readingHistory.value == null) {
return false return false
} }
v.showPopupMenu(R.menu.popup_read) { v.showPopupMenu(R.menu.popup_read) {
@@ -154,7 +158,7 @@ class DetailsFragment : BaseFragment(R.layout.fragment_details), View.OnClickLis
startActivity( startActivity(
ReaderActivity.newIntent( ReaderActivity.newIntent(
context ?: return@showPopupMenu false, context ?: return@showPopupMenu false,
manga ?: return@showPopupMenu false viewModel.manga.value ?: return@showPopupMenu false
) )
) )
true true
@@ -167,19 +171,4 @@ class DetailsFragment : BaseFragment(R.layout.fragment_details), View.OnClickLis
else -> return false 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)
}
}
}
} }

View File

@@ -1,18 +1,18 @@
package org.koitharu.kotatsu.details.ui 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.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.flow.*
import org.koitharu.kotatsu.base.domain.MangaDataRepository 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.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException 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.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.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.HistoryRepository
import org.koitharu.kotatsu.history.domain.OnHistoryChangeListener
import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
@@ -20,88 +20,82 @@ import org.koitharu.kotatsu.utils.ext.safe
import java.io.IOException import java.io.IOException
class DetailsViewModel( class DetailsViewModel(
intent: MangaIntent,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val favouritesRepository: FavouritesRepository, private val favouritesRepository: FavouritesRepository,
private val localMangaRepository: LocalMangaRepository, private val localMangaRepository: LocalMangaRepository,
private val trackingRepository: TrackingRepository, private val trackingRepository: TrackingRepository,
private val mangaDataRepository: MangaDataRepository 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 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 { init {
HistoryRepository.subscribe(this) launchLoadingJob(Dispatchers.Default) {
FavouritesRepository.subscribe(this) var manga = mangaDataRepository.resolveIntent(intent)
} ?: throw MangaNotFoundException("Cannot find manga")
mangaData.value = manga
fun findMangaById(id: Long) { manga = manga.source.repository.getDetails(manga)
launchLoadingJob {
val manga = mangaDataRepository.findMangaById(id)
?: throw MangaNotFoundException("Cannot find manga by id")
mangaData.value = 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) { fun deleteLocal(manga: Manga) {
launchLoadingJob { launchLoadingJob(Dispatchers.Default) {
withContext(Dispatchers.Default) { val original = localMangaRepository.getRemoteManga(manga)
val original = localMangaRepository.getRemoteManga(manga) localMangaRepository.delete(manga) || throw IOException("Unable to delete file")
localMangaRepository.delete(manga) || throw IOException("Unable to delete file") safe {
safe { historyRepository.deleteOrSwap(manga, original)
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()
}
} }

View File

@@ -1,23 +1,29 @@
package org.koitharu.kotatsu.details.ui package org.koitharu.kotatsu.details.ui.adapter
import android.graphics.Color import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer
import android.view.ViewGroup
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.item_chapter.* import kotlinx.android.synthetic.main.item_chapter.*
import org.koitharu.kotatsu.R 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.core.model.MangaChapter
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
class ChapterHolder(parent: ViewGroup) : fun chapterListItemAD(
BaseViewHolder<MangaChapter, ChapterExtra>(parent, R.layout.item_chapter) { clickListener: OnListItemClickListener<MangaChapter>
) = adapterDelegateLayoutContainer<ChapterListItem, ChapterListItem>(R.layout.item_chapter) {
override fun onBind(data: MangaChapter, extra: ChapterExtra) { itemView.setOnClickListener {
textView_title.text = data.name clickListener.onItemClick(item.chapter, it)
textView_number.text = data.number.toString() }
imageView_check.isVisible = extra == ChapterExtra.CHECKED itemView.setOnLongClickListener {
when (extra) { 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 -> { ChapterExtra.UNREAD -> {
textView_number.setBackgroundResource(R.drawable.bg_badge_default) textView_number.setBackgroundResource(R.drawable.bg_badge_default)
textView_number.setTextColor(context.getThemeColor(android.R.attr.textColorSecondaryInverse)) 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.setBackgroundResource(R.drawable.bg_badge_accent)
textView_number.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse)) textView_number.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse))
} }
ChapterExtra.CHECKED -> {
textView_number.background = null
textView_number.setTextColor(Color.TRANSPARENT)
}
} }
} }
} }

View File

@@ -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
}
}
}

View File

@@ -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()
}
}

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -38,6 +38,10 @@ abstract class FavouritesDao {
@Query("SELECT * FROM favourites WHERE manga_id = :id GROUP BY manga_id") @Query("SELECT * FROM favourites WHERE manga_id = :id GROUP BY manga_id")
abstract suspend fun find(id: Long): FavouriteManga? 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) @Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(favourite: FavouriteEntity) abstract suspend fun insert(favourite: FavouriteEntity)

View File

@@ -4,6 +4,7 @@ import androidx.collection.ArraySet
import androidx.room.withTransaction import androidx.room.withTransaction
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.MangaEntity
@@ -57,6 +58,12 @@ class FavouritesRepository(private val db: MangaDatabase) {
return entities?.map { it.toFavouriteCategory() }.orEmpty() 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 { suspend fun addCategory(title: String): FavouriteCategory {
val entity = FavouriteCategoryEntity( val entity = FavouriteCategoryEntity(
title = title, title = title,

View File

@@ -25,6 +25,9 @@ abstract class HistoryDao {
@Query("SELECT * FROM history WHERE manga_id = :id") @Query("SELECT * FROM history WHERE manga_id = :id")
abstract suspend fun find(id: Long): HistoryEntity? 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") @Query("DELETE FROM history")
abstract suspend fun clear() abstract suspend fun clear()

View File

@@ -2,5 +2,5 @@ package org.koitharu.kotatsu.history.domain
enum class ChapterExtra { enum class ChapterExtra {
READ, CURRENT, UNREAD, NEW, CHECKED READ, CURRENT, UNREAD, NEW
} }

View File

@@ -4,6 +4,7 @@ import androidx.collection.ArraySet
import androidx.room.withTransaction import androidx.room.withTransaction
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject 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) { suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int, scroll: Int) {
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag) val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
db.withTransaction { db.withTransaction {

View File

@@ -5,18 +5,17 @@ import android.view.View
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.dialog_chapters.* import kotlinx.android.synthetic.main.dialog_chapters.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.AlertDialogFragment 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.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 import org.koitharu.kotatsu.utils.ext.withArgs
class ChaptersDialog : AlertDialogFragment(R.layout.dialog_chapters), class ChaptersDialog : AlertDialogFragment(R.layout.dialog_chapters),
OnRecyclerItemClickListener<MangaChapter> { OnListItemClickListener<MangaChapter> {
override fun onBuildDialog(builder: AlertDialog.Builder) { override fun onBuildDialog(builder: AlertDialog.Builder) {
builder.setTitle(R.string.chapters) builder.setTitle(R.string.chapters)
@@ -32,22 +31,12 @@ class ChaptersDialog : AlertDialogFragment(R.layout.dialog_chapters),
) )
) )
recyclerView_chapters.adapter = ChaptersAdapter(this).apply { recyclerView_chapters.adapter = ChaptersAdapter(this).apply {
arguments?.getParcelableArrayList<MangaChapter>(ARG_CHAPTERS)?.let(this::replaceData) // arguments?.getParcelableArrayList<MangaChapter>(ARG_CHAPTERS)?.let(this::setItems)
currentChapterId = arguments?.getLong(ARG_CURRENT_ID, 0L)?.takeUnless { it == 0L } // currentChapterId = arguments?.getLong(ARG_CURRENT_ID, 0L)?.takeUnless { it == 0L }
} }
} }
override fun onResume() { override fun onItemClick(item: MangaChapter, view: View) {
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) {
((parentFragment as? OnChapterChangeListener) ((parentFragment as? OnChapterChangeListener)
?: (activity as? OnChapterChangeListener))?.let { ?: (activity as? OnChapterChangeListener))?.let {
dismiss() dismiss()

View File

@@ -0,0 +1,10 @@
package org.koitharu.kotatsu.utils
import kotlinx.coroutines.flow.MutableStateFlow
class SelectionController {
private val state = MutableStateFlow(emptySet<Int>())
}

View File

@@ -9,8 +9,8 @@ import coil.executeBlocking
import coil.request.ImageRequest import coil.request.ImageRequest
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.core.model.Manga 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.history.domain.HistoryRepository
import org.koitharu.kotatsu.utils.ext.requireBitmap import org.koitharu.kotatsu.utils.ext.requireBitmap
import java.io.IOException import java.io.IOException
@@ -52,7 +52,7 @@ class RecentListFactory(
views.setImageViewResource(R.id.imageView_cover, R.drawable.ic_placeholder) views.setImageViewResource(R.id.imageView_cover, R.drawable.ic_placeholder)
} }
val intent = Intent() 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) views.setOnClickFillInIntent(R.id.imageView_cover, intent)
return views return views
} }

View File

@@ -9,9 +9,9 @@ import coil.executeBlocking
import coil.request.ImageRequest import coil.request.ImageRequest
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.prefs.AppWidgetConfig 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.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.utils.ext.requireBitmap import org.koitharu.kotatsu.utils.ext.requireBitmap
import java.io.IOException import java.io.IOException
@@ -63,7 +63,7 @@ class ShelfListFactory(
views.setImageViewResource(R.id.imageView_cover, R.drawable.ic_placeholder) views.setImageViewResource(R.id.imageView_cover, R.drawable.ic_placeholder)
} }
val intent = Intent() val intent = Intent()
intent.putExtra(DetailsActivity.EXTRA_MANGA_ID, item.id) intent.putExtra(MangaIntent.KEY_ID, item.id)
views.setOnClickFillInIntent(R.id.rootLayout, intent) views.setOnClickFillInIntent(R.id.rootLayout, intent)
return views return views
} }

View File

@@ -21,20 +21,6 @@
android:textColor="?android:textColorSecondaryInverse" android:textColor="?android:textColorSecondaryInverse"
tools:text="13" /> 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 <TextView
android:id="@+id/textView_title" android:id="@+id/textView_title"
android:layout_width="match_parent" android:layout_width="match_parent"