Migrate details to AdapterDelegates and mvvm

pull/26/head
Koitharu 6 years ago
parent 1b1540b35b
commit fa02cfd7e8

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

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

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

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

@ -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.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 onLoadingStateChanged(isLoading: Boolean) {
progressBar.isVisible = isLoading
} }
private fun onHistoryChanged(history: MangaHistory?) { private fun onChaptersChanged(list: List<ChapterListItem>) {
adapter.currentChapterId = history?.chapterId chaptersAdapter?.items = list
scrollToCurrent()
} }
private fun onNewChaptersChanged(newChapters: Int) { private fun onLoadingStateChanged(isLoading: Boolean) {
adapter.newChaptersCount = newChapters progressBar.isVisible = isLoading
} }
override fun onItemClick(item: MangaChapter, position: Int, view: View) { override fun onItemClick(item: MangaChapter, view: View) {
if (adapter.checkedItemsCount != 0) { if (selectionDecoration?.checkedItemsCount != 0) {
adapter.toggleItemChecked(item.id) selectionDecoration?.toggleItemChecked(item.id)
if (adapter.checkedItemsCount == 0) { if (selectionDecoration?.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
} }
} }

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

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

@ -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() {
val mangaData = MutableLiveData<Manga>() private val mangaData = MutableStateFlow<Manga?>(intent.manga)
val newChapters = MutableLiveData<Int>(0)
val onMangaRemoved = SingleLiveEvent<Manga>()
val history = MutableLiveData<MangaHistory?>()
val favouriteCategories = MutableLiveData<List<FavouriteCategory>>()
init { private val history = mangaData.mapNotNull { it?.id }
HistoryRepository.subscribe(this) .distinctUntilChanged()
FavouritesRepository.subscribe(this) .flatMapLatest { mangaId ->
} historyRepository.observeOne(mangaId)
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
fun findMangaById(id: Long) { private val favourite = mangaData.mapNotNull { it?.id }
launchLoadingJob { .distinctUntilChanged()
val manga = mangaDataRepository.findMangaById(id) .flatMapLatest { mangaId ->
?: throw MangaNotFoundException("Cannot find manga by id") favouritesRepository.observeCategories(mangaId)
mangaData.value = manga }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
loadDetails(manga, true)
}
}
fun loadDetails(manga: Manga, force: Boolean = false) { private val newChapters = mangaData.mapNotNull { it?.id }
if (!force && mangaData.value == manga) { .distinctUntilChanged()
return .mapLatest { mangaId ->
} trackingRepository.getNewChaptersCount(mangaId)
loadHistory(manga) }.stateIn(viewModelScope, SharingStarted.Eagerly, 0)
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) { val manga = mangaData.filterNotNull()
launchLoadingJob { .asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
withContext(Dispatchers.Default) { val favouriteCategories = favourite
val original = localMangaRepository.getRemoteManga(manga) .asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
localMangaRepository.delete(manga) || throw IOException("Unable to delete file") val newChaptersCount = newChapters
safe { .asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
historyRepository.deleteOrSwap(manga, original) val readingHistory = history
} .asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
}
onMangaRemoved.call(manga)
}
}
private fun loadHistory(manga: Manga) { val onMangaRemoved = SingleLiveEvent<Manga>()
launchJob {
history.value = historyRepository.getOne(manga)
}
}
private fun loadFavourite(manga: Manga) { val chapters = combine(
launchJob { mangaData.map { it?.chapters.orEmpty() },
favouriteCategories.value = favouritesRepository.getCategories(manga.id) 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)
override fun onHistoryChanged() {
loadHistory(mangaData.value ?: return)
}
override fun onFavouritesChanged(mangaId: Long) { init {
val manga = mangaData.value ?: return launchLoadingJob(Dispatchers.Default) {
if (mangaId == manga.id) { var manga = mangaDataRepository.resolveIntent(intent)
loadFavourite(manga) ?: throw MangaNotFoundException("Cannot find manga")
mangaData.value = manga
manga = manga.source.repository.getDetails(manga)
mangaData.value = manga
} }
} }
override fun onCleared() { fun deleteLocal(manga: Manga) {
HistoryRepository.unsubscribe(this) launchLoadingJob(Dispatchers.Default) {
FavouritesRepository.unsubscribe(this) val original = localMangaRepository.getRemoteManga(manga)
super.onCleared() localMangaRepository.delete(manga) || throw IOException("Unable to delete file")
safe {
historyRepository.deleteOrSwap(manga, original)
}
onMangaRemoved.postCall(manga)
}
} }
} }

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save