Update history repository

This commit is contained in:
Koitharu
2020-02-02 16:31:50 +02:00
parent 7d677833fa
commit aa65f9b02c
33 changed files with 339 additions and 200 deletions

View File

@@ -1,6 +0,0 @@
package org.koitharu.kotatsu.core.model
data class MangaInfo <E>(
val manga: Manga,
val extra: E
)

View File

@@ -0,0 +1,6 @@
package org.koitharu.kotatsu.domain
enum class ChapterExtra {
READ, CURRENT, UNREAD, NEW
}

View File

@@ -11,44 +11,15 @@ import org.koitharu.kotatsu.core.parser.MangaRepository
import java.io.Closeable
import java.util.*
class HistoryRepository() : KoinComponent,
MangaRepository, Closeable {
class HistoryRepository() : KoinComponent {
private val db: MangaDatabase by inject()
override val sortOrders: Set<SortOrder> = setOf(SortOrder.NEWEST, SortOrder.POPULARITY)
override val isSearchAvailable = false
override suspend fun getList(
offset: Int,
query: String?,
sortOrder: SortOrder?,
tag: MangaTag?
): List<Manga> = getHistory(offset, query, sortOrder, tag).map { x -> x.manga }
suspend fun getHistory(
offset: Int,
query: String? = null,
sortOrder: SortOrder? = null,
tag: MangaTag? = null
): List<MangaInfo<MangaHistory>> {
suspend fun getList(offset: Int) : List<Manga> {
val entities = db.historyDao().getAll(offset, 20, "updated_by")
return entities.map { x -> MangaInfo(x.manga.toManga(), x.history.toMangaHistory()) }
return entities.map { it.manga.toManga() }
}
override suspend fun getDetails(manga: Manga): Manga {
throw UnsupportedOperationException("History repository does not support getDetails() method")
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
throw UnsupportedOperationException("History repository does not support getPages() method")
}
override suspend fun getPageFullUrl(page: MangaPage) = page.url
override suspend fun getTags() = emptySet<MangaTag>()
suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int) {
val dao = db.historyDao()
val entity = HistoryEntity(
@@ -66,7 +37,7 @@ class HistoryRepository() : KoinComponent,
)
}
suspend fun getHistory(manga: Manga): MangaHistory? {
suspend fun getOne(manga: Manga): MangaHistory? {
return db.historyDao().getOneOrNull(manga.id)?.let {
MangaHistory(
createdAt = Date(it.createdAt),
@@ -80,8 +51,4 @@ class HistoryRepository() : KoinComponent,
suspend fun clear() {
db.historyDao().clear()
}
override fun close() {
db.close()
}
}

View File

@@ -5,8 +5,8 @@ import androidx.recyclerview.widget.RecyclerView
import org.koin.core.KoinComponent
import org.koitharu.kotatsu.utils.ext.replaceWith
abstract class BaseRecyclerAdapter<T>(private val onItemClickListener: OnRecyclerItemClickListener<T>? = null) :
RecyclerView.Adapter<BaseViewHolder<T>>(),
abstract class BaseRecyclerAdapter<T, E>(private val onItemClickListener: OnRecyclerItemClickListener<T>? = null) :
RecyclerView.Adapter<BaseViewHolder<T, E>>(),
KoinComponent {
private val dataSet = ArrayList<T>()
@@ -16,8 +16,9 @@ abstract class BaseRecyclerAdapter<T>(private val onItemClickListener: OnRecycle
setHasStableIds(true)
}
override fun onBindViewHolder(holder: BaseViewHolder<T>, position: Int) {
holder.bind(dataSet[position])
override fun onBindViewHolder(holder: BaseViewHolder<T, E>, position: Int) {
val item = dataSet[position]
holder.bind(item, getExtra(item, position))
}
fun getItem(position: Int) = dataSet[position]
@@ -64,13 +65,16 @@ abstract class BaseRecyclerAdapter<T>(private val onItemClickListener: OnRecycle
final override fun getItemCount() = dataSet.size
final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<T> {
return onCreateViewHolder(parent).setOnItemClickListener(onItemClickListener).also(this::onViewHolderCreated)
final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<T, E> {
return onCreateViewHolder(parent).setOnItemClickListener(onItemClickListener)
.also(this::onViewHolderCreated)
}
protected open fun onViewHolderCreated(holder: BaseViewHolder<T>) = Unit
protected abstract fun getExtra(item: T, position: Int): E
protected abstract fun onCreateViewHolder(parent: ViewGroup): BaseViewHolder<T>
protected open fun onViewHolderCreated(holder: BaseViewHolder<T, E>) = Unit
protected abstract fun onCreateViewHolder(parent: ViewGroup): BaseViewHolder<T, E>
protected abstract fun onGetItemId(item: T): Long
}

View File

@@ -8,7 +8,7 @@ import kotlinx.android.extensions.LayoutContainer
import org.koin.core.KoinComponent
import org.koitharu.kotatsu.utils.ext.inflate
abstract class BaseViewHolder<T> protected constructor(view: View) :
abstract class BaseViewHolder<T, E> protected constructor(view: View) :
RecyclerView.ViewHolder(view), LayoutContainer, KoinComponent {
constructor(parent: ViewGroup, @LayoutRes resId: Int) : this(parent.inflate(resId))
@@ -21,14 +21,14 @@ abstract class BaseViewHolder<T> protected constructor(view: View) :
val context get() = itemView.context!!
fun bind(data: T) {
fun bind(data: T, extra: E) {
boundData = data
onBind(data)
onBind(data, extra)
}
fun requireData() = boundData ?: throw IllegalStateException("Calling requireData() before bind()")
fun setOnItemClickListener(listener: OnRecyclerItemClickListener<T>?): BaseViewHolder<T> {
fun setOnItemClickListener(listener: OnRecyclerItemClickListener<T>?): BaseViewHolder<T, E> {
if (listener != null) {
itemView.setOnClickListener {
listener.onItemClick(boundData ?: return@setOnClickListener, adapterPosition, it)
@@ -40,5 +40,5 @@ abstract class BaseViewHolder<T> protected constructor(view: View) :
return this
}
abstract fun onBind(data: T)
abstract fun onBind(data: T, extra: E)
}

View File

@@ -4,12 +4,33 @@ import android.view.ViewGroup
import kotlinx.android.synthetic.main.item_chapter.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.domain.ChapterExtra
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
import org.koitharu.kotatsu.utils.ext.getThemeColor
class ChapterHolder(parent: ViewGroup) : BaseViewHolder<MangaChapter>(parent, R.layout.item_chapter) {
class ChapterHolder(parent: ViewGroup) :
BaseViewHolder<MangaChapter, ChapterExtra>(parent, R.layout.item_chapter) {
override fun onBind(data: MangaChapter) {
override fun onBind(data: MangaChapter, extra: ChapterExtra) {
textView_title.text = data.name
textView_number.text = data.number.toString()
when (extra) {
ChapterExtra.UNREAD -> {
textView_number.setBackgroundResource(R.drawable.bg_badge_default)
textView_number.setTextColor(context.getThemeColor(android.R.attr.textColorSecondaryInverse))
}
ChapterExtra.READ -> {
textView_number.setBackgroundResource(R.drawable.bg_badge_outline)
textView_number.setTextColor(context.getThemeColor(android.R.attr.textColorTertiary))
}
ChapterExtra.CURRENT -> {
textView_number.setBackgroundResource(R.drawable.bg_badge_outline_accent)
textView_number.setTextColor(context.getThemeColor(R.attr.colorAccent))
}
ChapterExtra.NEW -> {
textView_number.setBackgroundResource(R.drawable.bg_badge_accent)
textView_number.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse))
}
}
}
}

View File

@@ -1,15 +1,32 @@
package org.koitharu.kotatsu.ui.details
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.domain.ChapterExtra
import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
class ChaptersAdapter(onItemClickListener: OnRecyclerItemClickListener<MangaChapter>) :
BaseRecyclerAdapter<MangaChapter>(onItemClickListener) {
BaseRecyclerAdapter<MangaChapter, ChapterExtra>(onItemClickListener) {
var currentChapterPosition = RecyclerView.NO_POSITION
set(value) {
field = value
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup) = ChapterHolder(parent)
override fun onGetItemId(item: MangaChapter) = item.id
override fun getExtra(item: MangaChapter, position: Int): ChapterExtra = when {
currentChapterPosition == RecyclerView.NO_POSITION -> ChapterExtra.UNREAD
currentChapterPosition == position -> ChapterExtra.CURRENT
currentChapterPosition < position -> ChapterExtra.UNREAD
currentChapterPosition > position -> ChapterExtra.READ
else -> ChapterExtra.UNREAD
}
}

View File

@@ -8,9 +8,9 @@ import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.fragment_chapters.*
import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.model.MangaInfo
import org.koitharu.kotatsu.ui.common.BaseFragment
import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.ui.reader.ReaderActivity
@@ -21,7 +21,7 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsV
@Suppress("unused")
private val presenter by moxyPresenter { (activity as MangaDetailsActivity).presenter }
private var data: MangaInfo<MangaHistory?>? = null
private var manga: Manga? = null
private lateinit var adapter: ChaptersAdapter
@@ -37,9 +37,9 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsV
recyclerView_chapters.adapter = adapter
}
override fun onMangaUpdated(data: MangaInfo<MangaHistory?>) {
this.data = data
adapter.replaceData(data.manga.chapters.orEmpty())
override fun onMangaUpdated(manga: Manga) {
this.manga = manga
adapter.replaceData(manga.chapters.orEmpty())
}
override fun onLoadingStateChanged(isLoading: Boolean) {
@@ -50,11 +50,17 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsV
}
override fun onHistoryChanged(history: MangaHistory?) {
adapter.currentChapterPosition = history?.let {
manga?.chapters?.indexOfFirst { x -> x.id == it.chapterId }
} ?: RecyclerView.NO_POSITION
}
override fun onItemClick(item: MangaChapter, position: Int, view: View) {
startActivity(
ReaderActivity.newIntent(
context ?: return,
data?.manga ?: return,
manga ?: return,
item.id
)
)

View File

@@ -9,7 +9,6 @@ import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.model.MangaInfo
import org.koitharu.kotatsu.ui.common.BaseActivity
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
@@ -25,13 +24,23 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView {
tabs.setupWithViewPager(pager)
intent?.getParcelableExtra<Manga>(EXTRA_MANGA)?.let {
presenter.loadDetails(it)
presenter.loadHistory(it)
} ?: finish()
}
override fun onMangaUpdated(data: MangaInfo<MangaHistory?>) {
title = data.manga.title
override fun onResume() {
super.onResume()
intent?.getParcelableExtra<Manga>(EXTRA_MANGA)?.let {
presenter.loadHistory(it)
}
}
override fun onMangaUpdated(manga: Manga) {
title = manga.title
}
override fun onHistoryChanged(history: MangaHistory?) = Unit
override fun onLoadingStateChanged(isLoading: Boolean) = Unit
override fun onError(e: Exception) {

View File

@@ -6,7 +6,7 @@ import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import org.koitharu.kotatsu.R
class MangaDetailsAdapter(private val resources: Resources, fm: FragmentManager) : FragmentPagerAdapter(fm, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
class MangaDetailsAdapter(private val resources: Resources, fm: FragmentManager) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
override fun getCount() = 2

View File

@@ -8,7 +8,6 @@ import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.model.MangaInfo
import org.koitharu.kotatsu.ui.common.BaseFragment
import org.koitharu.kotatsu.ui.reader.ReaderActivity
import org.koitharu.kotatsu.utils.ext.setChips
@@ -19,46 +18,34 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
@Suppress("unused")
private val presenter by moxyPresenter { (activity as MangaDetailsActivity).presenter }
override fun onMangaUpdated(data: MangaInfo<MangaHistory?>) {
imageView_cover.load(data.manga.largeCoverUrl ?: data.manga.coverUrl)
textView_title.text = data.manga.title
textView_subtitle.text = data.manga.localizedTitle
textView_description.text = data.manga.description?.parseAsHtml()
if (data.manga.rating == Manga.NO_RATING) {
private var manga: Manga? = null
private var history: MangaHistory? = null
override fun onMangaUpdated(manga: Manga) {
this.manga = manga
imageView_cover.load(manga.largeCoverUrl ?: manga.coverUrl)
textView_title.text = manga.title
textView_subtitle.text = manga.localizedTitle
textView_description.text = manga.description?.parseAsHtml()
if (manga.rating == Manga.NO_RATING) {
ratingBar.isVisible = false
} else {
ratingBar.progress = (ratingBar.max * data.manga.rating).roundToInt()
ratingBar.progress = (ratingBar.max * manga.rating).roundToInt()
ratingBar.isVisible = true
}
chips_tags.setChips(data.manga.tags) {
chips_tags.setChips(manga.tags) {
create(
text = it.title,
iconRes = R.drawable.ic_chip_tag,
tag = it
)
}
if (data.manga.chapters.isNullOrEmpty()) {
button_read.isEnabled = false
} else {
button_read.isEnabled = true
if (data.extra == 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)
}
val chapterId = data.extra?.chapterId ?: data.manga.chapters.first().id
button_read.setOnClickListener {
startActivity(
ReaderActivity.newIntent(
context ?: return@setOnClickListener,
data.manga,
chapterId
)
)
}
}
updateReadButton()
}
override fun onHistoryChanged(history: MangaHistory?) {
this.history = history
updateReadButton()
}
override fun onLoadingStateChanged(isLoading: Boolean) {
@@ -68,4 +55,28 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
override fun onError(e: Exception) {
}
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)
}
button_read.setOnClickListener {
startActivity(
ReaderActivity.newIntent(
context ?: return@setOnClickListener,
manga ?: return@setOnClickListener,
history
)
)
}
}
}
}

View File

@@ -1,13 +1,11 @@
package org.koitharu.kotatsu.ui.details
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import moxy.InjectViewState
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaInfo
import org.koitharu.kotatsu.domain.HistoryRepository
import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.ui.common.BasePresenter
@@ -15,24 +13,25 @@ import org.koitharu.kotatsu.ui.common.BasePresenter
@InjectViewState
class MangaDetailsPresenter : BasePresenter<MangaDetailsView>() {
private lateinit var historyRepository: HistoryRepository
private var isLoaded = false
fun loadDetails(manga: Manga) {
if (isLoaded) {
override fun onFirstViewAttach() {
historyRepository = HistoryRepository()
super.onFirstViewAttach()
}
fun loadDetails(manga: Manga, force: Boolean = false) {
if (!force && isLoaded) {
return
}
viewState.onMangaUpdated(MangaInfo(manga, null))
viewState.onMangaUpdated(manga)
launch {
try {
viewState.onLoadingStateChanged(true)
val data = withContext(Dispatchers.IO) {
val details = async {
MangaProviderFactory.create(manga.source).getDetails(manga)
}
val history = async {
HistoryRepository().use { it.getHistory(manga) }
}
MangaInfo(details.await(), history.await())
MangaProviderFactory.create(manga.source).getDetails(manga)
}
viewState.onMangaUpdated(data)
isLoaded = true
@@ -46,4 +45,19 @@ class MangaDetailsPresenter : BasePresenter<MangaDetailsView>() {
}
}
}
fun loadHistory(manga: Manga) {
launch {
try {
val history = withContext(Dispatchers.IO) {
historyRepository.getOne(manga)
}
viewState.onHistoryChanged(history)
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
}
}
}
}

View File

@@ -4,17 +4,20 @@ import moxy.MvpView
import moxy.viewstate.strategy.AddToEndSingleStrategy
import moxy.viewstate.strategy.OneExecutionStateStrategy
import moxy.viewstate.strategy.StateStrategyType
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.model.MangaInfo
interface MangaDetailsView : MvpView {
@StateStrategyType(AddToEndSingleStrategy::class)
fun onMangaUpdated(data: MangaInfo<MangaHistory?>)
fun onMangaUpdated(manga: Manga)
@StateStrategyType(AddToEndSingleStrategy::class)
fun onLoadingStateChanged(isLoading: Boolean)
@StateStrategyType(OneExecutionStateStrategy::class)
fun onError(e: Exception)
@StateStrategyType(AddToEndSingleStrategy::class)
fun onHistoryChanged(history: MangaHistory?)
}

View File

@@ -5,17 +5,18 @@ import coil.api.load
import coil.request.RequestDisposable
import kotlinx.android.synthetic.main.item_manga_grid.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaInfo
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
class MangaGridHolder<E>(parent: ViewGroup) : BaseViewHolder<MangaInfo<E>>(parent, R.layout.item_manga_grid) {
class MangaGridHolder(parent: ViewGroup) : BaseViewHolder<Manga, MangaHistory?>(parent, R.layout.item_manga_grid) {
private var coverRequest: RequestDisposable? = null
override fun onBind(data: MangaInfo<E>) {
override fun onBind(data: Manga, extra: MangaHistory?) {
coverRequest?.dispose()
textView_title.text = data.manga.title
coverRequest = imageView_cover.load(data.manga.coverUrl) {
textView_title.text = data.title
coverRequest = imageView_cover.load(data.coverUrl) {
crossfade(true)
}
}

View File

@@ -1,21 +1,24 @@
package org.koitharu.kotatsu.ui.main.list
import android.view.ViewGroup
import org.koitharu.kotatsu.core.model.MangaInfo
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter
import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
class MangaListAdapter<E>(onItemClickListener: OnRecyclerItemClickListener<MangaInfo<E>>) :
BaseRecyclerAdapter<MangaInfo<E>>(onItemClickListener) {
class MangaListAdapter(onItemClickListener: OnRecyclerItemClickListener<Manga>) :
BaseRecyclerAdapter<Manga, MangaHistory?>(onItemClickListener) {
var listMode: ListMode = ListMode.LIST
override fun onCreateViewHolder(parent: ViewGroup) = when(listMode) {
ListMode.LIST -> MangaListHolder<E>(parent)
ListMode.DETAILED_LIST -> MangaListDetailsHolder<E>(parent)
ListMode.LIST -> MangaListHolder(parent)
ListMode.DETAILED_LIST -> MangaListDetailsHolder(parent)
ListMode.GRID -> MangaGridHolder(parent)
}
override fun onGetItemId(item: MangaInfo<E>) = item.manga.id
override fun onGetItemId(item: Manga) = item.id
override fun getExtra(item: Manga, position: Int): MangaHistory? = null
}

View File

@@ -8,30 +8,30 @@ import coil.request.RequestDisposable
import kotlinx.android.synthetic.main.item_manga_list_details.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaInfo
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
import org.koitharu.kotatsu.utils.ext.textAndVisible
import kotlin.math.roundToInt
class MangaListDetailsHolder<E>(parent: ViewGroup) : BaseViewHolder<MangaInfo<E>>(parent, R.layout.item_manga_list_details) {
class MangaListDetailsHolder(parent: ViewGroup) : BaseViewHolder<Manga, MangaHistory?>(parent, R.layout.item_manga_list_details) {
private var coverRequest: RequestDisposable? = null
@SuppressLint("SetTextI18n")
override fun onBind(data: MangaInfo<E>) {
override fun onBind(data: Manga, extra: MangaHistory?) {
coverRequest?.dispose()
textView_title.text = data.manga.title
textView_subtitle.textAndVisible = data.manga.localizedTitle
coverRequest = imageView_cover.load(data.manga.coverUrl) {
textView_title.text = data.title
textView_subtitle.textAndVisible = data.localizedTitle
coverRequest = imageView_cover.load(data.coverUrl) {
crossfade(true)
}
if(data.manga.rating == Manga.NO_RATING) {
if(data.rating == Manga.NO_RATING) {
textView_rating.isVisible = false
} else {
textView_rating.text = "${(data.manga.rating * 10).roundToInt()}/10"
textView_rating.text = "${(data.rating * 10).roundToInt()}/10"
textView_rating.isVisible = true
}
textView_tags.text = data.manga.tags.joinToString(", ") {
textView_tags.text = data.tags.joinToString(", ") {
it.title
}
}

View File

@@ -14,7 +14,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_list.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaInfo
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.ui.common.BaseFragment
import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
@@ -27,9 +27,9 @@ import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.hasItems
abstract class MangaListFragment <E> : BaseFragment(R.layout.fragment_list), MangaListView<E>,
PaginationScrollListener.Callback, OnRecyclerItemClickListener<MangaInfo<E>> {
PaginationScrollListener.Callback, OnRecyclerItemClickListener<Manga> {
private lateinit var adapter: MangaListAdapter<E>
private lateinit var adapter: MangaListAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -73,16 +73,16 @@ abstract class MangaListFragment <E> : BaseFragment(R.layout.fragment_list), Man
else -> super.onOptionsItemSelected(item)
}
override fun onItemClick(item: MangaInfo<E>, position: Int, view: View) {
startActivity(MangaDetailsActivity.newIntent(context ?: return, item.manga))
override fun onItemClick(item: Manga, position: Int, view: View) {
startActivity(MangaDetailsActivity.newIntent(context ?: return, item))
}
override fun onListChanged(list: List<MangaInfo<E>>) {
override fun onListChanged(list: List<Manga>) {
adapter.replaceData(list)
layout_holder.isVisible = list.isEmpty()
}
override fun onListAppended(list: List<MangaInfo<E>>) {
override fun onListAppended(list: List<Manga>) {
adapter.appendData(list)
}

View File

@@ -5,19 +5,20 @@ import coil.api.load
import coil.request.RequestDisposable
import kotlinx.android.synthetic.main.item_manga_list.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaInfo
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
import org.koitharu.kotatsu.utils.ext.textAndVisible
class MangaListHolder<E>(parent: ViewGroup) : BaseViewHolder<MangaInfo<E>>(parent, R.layout.item_manga_list) {
class MangaListHolder(parent: ViewGroup) : BaseViewHolder<Manga, MangaHistory?>(parent, R.layout.item_manga_list) {
private var coverRequest: RequestDisposable? = null
override fun onBind(data: MangaInfo<E>) {
override fun onBind(data: Manga, extra: MangaHistory?) {
coverRequest?.dispose()
textView_title.text = data.manga.title
textView_subtitle.textAndVisible = data.manga.localizedTitle
coverRequest = imageView_cover.load(data.manga.coverUrl) {
textView_title.text = data.title
textView_subtitle.textAndVisible = data.localizedTitle
coverRequest = imageView_cover.load(data.coverUrl) {
crossfade(true)
}
}

View File

@@ -2,15 +2,15 @@ package org.koitharu.kotatsu.ui.main.list
import moxy.MvpView
import moxy.viewstate.strategy.*
import org.koitharu.kotatsu.core.model.MangaInfo
import org.koitharu.kotatsu.core.model.Manga
interface MangaListView<E> : MvpView {
@StateStrategyType(AddToEndSingleTagStrategy::class, tag = "content")
fun onListChanged(list: List<MangaInfo<E>>)
fun onListChanged(list: List<Manga>)
@StateStrategyType(AddToEndStrategy::class, tag = "content")
fun onListAppended(list: List<MangaInfo<E>>)
fun onListAppended(list: List<Manga>)
@StateStrategyType(AddToEndSingleStrategy::class)
fun onLoadingChanged(isLoading: Boolean)

View File

@@ -4,7 +4,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import moxy.InjectViewState
import okhttp3.internal.closeQuietly
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.domain.HistoryRepository
@@ -26,7 +25,7 @@ class HistoryListPresenter : BasePresenter<MangaListView<MangaHistory>>() {
viewState.onLoadingChanged(true)
try {
val list = withContext(Dispatchers.IO) {
repository.getHistory(offset = offset)
repository.getList(offset = offset)
}
if (offset == 0) {
viewState.onListChanged(list)
@@ -62,9 +61,4 @@ class HistoryListPresenter : BasePresenter<MangaListView<MangaHistory>>() {
}
}
}
override fun onDestroy() {
repository.closeQuietly()
super.onDestroy()
}
}

View File

@@ -5,7 +5,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import moxy.InjectViewState
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.MangaInfo
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.ui.common.BasePresenter
@@ -21,7 +20,6 @@ class RemoteListPresenter : BasePresenter<MangaListView<Unit>>() {
val list = withContext(Dispatchers.IO) {
MangaProviderFactory.create(source)
.getList(offset)
.map { MangaInfo(it, Unit) }
}
if (offset == 0) {
viewState.onListChanged(list)

View File

@@ -11,17 +11,17 @@ import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class PageHolder(parent: ViewGroup, private val loader: PageLoader) : BaseViewHolder<MangaPage>(parent, R.layout.item_page),
class PageHolder(parent: ViewGroup, private val loader: PageLoader) : BaseViewHolder<MangaPage, Unit>(parent, R.layout.item_page),
SubsamplingScaleImageView.OnImageEventListener {
init {
ssiv.setOnImageEventListener(this)
button_retry.setOnClickListener {
onBind(boundData ?: return@setOnClickListener)
onBind(boundData ?: return@setOnClickListener, Unit)
}
}
override fun onBind(data: MangaPage) {
override fun onBind(data: MangaPage, extra: Unit) {
layout_error.isVisible = false
progressBar.show()
ssiv.recycle()

View File

@@ -5,9 +5,11 @@ import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
class PagesAdapter(private val loader: PageLoader) : BaseRecyclerAdapter<MangaPage>() {
class PagesAdapter(private val loader: PageLoader) : BaseRecyclerAdapter<MangaPage, Unit>() {
override fun onCreateViewHolder(parent: ViewGroup) = PageHolder(parent, loader)
override fun onGetItemId(item: MangaPage) = item.id
override fun getExtra(item: MangaPage, position: Int) = Unit
}

View File

@@ -5,11 +5,13 @@ import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.activity_reader.*
import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.ui.common.BaseActivity
import org.koitharu.kotatsu.utils.ext.showDialog
@@ -18,12 +20,7 @@ class ReaderActivity : BaseActivity(), ReaderView {
private val presenter by moxyPresenter { ReaderPresenter() }
private val manga by lazy(LazyThreadSafetyMode.NONE) {
intent.getParcelableExtra<Manga>(EXTRA_MANGA)!!
}
private val chapterId by lazy(LazyThreadSafetyMode.NONE) {
intent.getLongExtra(EXTRA_CHAPTER_ID, 0L)
}
private lateinit var state: ReaderState
private lateinit var loader: PageLoader
private lateinit var adapter: PagesAdapter
@@ -34,21 +31,24 @@ class ReaderActivity : BaseActivity(), ReaderView {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
bottomBar.inflateMenu(R.menu.opt_reader_bottom)
val chapter = manga.chapters?.find { x -> x.id == chapterId }
if (chapter == null) {
// TODO
finish()
return
}
title = chapter.name
manga.chapters?.run {
supportActionBar?.subtitle = getString(R.string.chapter_d_of_d, chapter.number, size)
state = savedInstanceState?.getParcelable<ReaderState>(EXTRA_STATE)
?: intent.getParcelableExtra<ReaderState>(EXTRA_STATE)
?: let {
Toast.makeText(this, R.string.error_occurred, Toast.LENGTH_SHORT).show()
finish()
return
}
title = state.chapter?.name ?: state.manga.title
state.manga.chapters?.run {
supportActionBar?.subtitle =
getString(R.string.chapter_d_of_d, state.chapter?.number ?: 0, size)
}
loader = PageLoader(this)
adapter = PagesAdapter(loader)
pager.adapter = adapter
presenter.loadChapter(chapter)
presenter.loadChapter(state)
}
override fun onDestroy() {
@@ -57,7 +57,8 @@ class ReaderActivity : BaseActivity(), ReaderView {
}
override fun onPause() {
presenter.addToHistory(manga, chapterId, pager.currentItem)
state = state.copy(page = pager.currentItem)
presenter.saveState(state)
super.onPause()
}
@@ -66,16 +67,17 @@ class ReaderActivity : BaseActivity(), ReaderView {
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem) = when(item.itemId) {
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_chapters -> {
ChaptersDialog.show(supportFragmentManager, manga.chapters.orEmpty())
ChaptersDialog.show(supportFragmentManager, state.manga.chapters.orEmpty())
true
}
else -> super.onOptionsItemSelected(item)
}
override fun onPagesReady(pages: List<MangaPage>) {
override fun onPagesReady(pages: List<MangaPage>, index: Int) {
adapter.replaceData(pages)
pager.setCurrentItem(index, false)
}
override fun onLoadingStateChanged(isLoading: Boolean) {
@@ -92,11 +94,32 @@ class ReaderActivity : BaseActivity(), ReaderView {
companion object {
private const val EXTRA_MANGA = "manga"
private const val EXTRA_CHAPTER_ID = "chapter_id"
private const val EXTRA_STATE = "state"
fun newIntent(context: Context, manga: Manga, chapterId: Long) = Intent(context, ReaderActivity::class.java)
.putExtra(EXTRA_MANGA, manga)
.putExtra(EXTRA_CHAPTER_ID, chapterId)
fun newIntent(context: Context, state: ReaderState) =
Intent(context, ReaderActivity::class.java)
.putExtra(EXTRA_STATE, state)
fun newIntent(context: Context, manga: Manga, chapterId: Long = -1) = newIntent(
context, ReaderState(
manga = manga,
chapterId = if (chapterId == -1L) manga.chapters?.firstOrNull()?.id
?: -1 else chapterId,
page = 0
)
)
fun newIntent(context: Context, manga: Manga, history: MangaHistory?) =
if (history == null) {
newIntent(context, manga)
} else {
newIntent(
context, ReaderState(
manga = manga,
chapterId = history.chapterId,
page = history.page
)
)
}
}
}

View File

@@ -14,14 +14,18 @@ import org.koitharu.kotatsu.ui.common.BasePresenter
@InjectViewState
class ReaderPresenter : BasePresenter<ReaderView>() {
fun loadChapter(chapter: MangaChapter) {
fun loadChapter(state: ReaderState) {
launch {
viewState.onLoadingStateChanged(isLoading = true)
try {
val pages = withContext(Dispatchers.IO) {
MangaProviderFactory.create(chapter.source).getPages(chapter)
val repo = MangaProviderFactory.create(state.manga.source)
val chapter = state.chapter ?: repo.getDetails(state.manga).chapters
?.first { it.id == state.chapterId }
?: throw RuntimeException("Chapter ${state.chapterId} not found")
repo.getPages(chapter)
}
viewState.onPagesReady(pages)
viewState.onPagesReady(pages, state.page)
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
@@ -33,11 +37,13 @@ class ReaderPresenter : BasePresenter<ReaderView>() {
}
}
fun addToHistory(manga: Manga, chapterId: Long, page: Int) {
fun saveState(state: ReaderState) {
launch(Dispatchers.IO) {
HistoryRepository().use {
it.addOrUpdate(manga, chapterId, page)
}
HistoryRepository().addOrUpdate(
manga = state.manga,
chapterId = state.chapterId,
page = state.page
)
}
}

View File

@@ -0,0 +1,20 @@
package org.koitharu.kotatsu.ui.reader
import android.os.Parcelable
import kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter
@Parcelize
data class ReaderState(
val manga: Manga,
val chapterId: Long,
val page: Int
) : Parcelable {
@IgnoredOnParcel
val chapter: MangaChapter? by lazy {
manga.chapters?.find { it.id == chapterId }
}
}

View File

@@ -9,7 +9,7 @@ import org.koitharu.kotatsu.core.model.MangaPage
interface ReaderView : MvpView {
@StateStrategyType(AddToEndSingleStrategy::class)
fun onPagesReady(pages: List<MangaPage>)
fun onPagesReady(pages: List<MangaPage>, index: Int)
@StateStrategyType(AddToEndSingleStrategy::class)
fun onLoadingStateChanged(isLoading: Boolean)

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="5dp" />
<solid android:color="?colorAccent" />
<padding
android:bottom="2dp"
android:left="2dp"
android:right="2dp"
android:top="2dp" />
</shape>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="5dp" />
<stroke
android:width="2dp"
android:color="?android:textColorTertiary" />
<padding
android:bottom="2dp"
android:left="2dp"
android:right="2dp"
android:top="2dp" />
</shape>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="5dp" />
<stroke
android:width="2dp"
android:color="?android:colorAccent" />
<padding
android:bottom="2dp"
android:left="2dp"
android:right="2dp"
android:top="2dp" />
</shape>

View File

@@ -27,25 +27,27 @@
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:maxLines="3"
android:layout_marginEnd="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="?android:textColorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem" />
tools:text="@tools:sample/lorem[20]" />
<TextView
android:id="@+id/textView_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginTop="4dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textColor="?android:textColorSecondary"
android:maxLines="2"
app:layout_constraintEnd_toEndOf="@id/textView_title"
app:layout_constraintStart_toStartOf="@id/textView_title"
app:layout_constraintTop_toBottomOf="@id/textView_title"
tools:text="@tools:sample/lorem" />
tools:text="@tools:sample/lorem[20]" />
<RatingBar
android:id="@+id/ratingBar"

View File

@@ -13,7 +13,7 @@
android:id="@+id/textView_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_round_rect"
android:background="@drawable/bg_badge_default"
android:gravity="center"
android:minWidth="26dp"
android:textColor="?android:textColorSecondaryInverse"