mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-14 13:08:56 +01:00
Migrate History screen to Compose (#6922)
* Migrate History screen to Compose - Migrate screen - Strip logic from presenter into use cases and repository - Setup for other screen being able to migrate to Compose with Theme * Changes from review comments
This commit is contained in:
@@ -24,6 +24,7 @@ import coil.decode.GifDecoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.disk.DiskCache
|
||||
import coil.util.DebugLogger
|
||||
import eu.kanade.domain.DomainModule
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
|
||||
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
||||
@@ -74,6 +75,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||
}
|
||||
|
||||
Injekt.importModule(AppModule(this))
|
||||
Injekt.importModule(DomainModule())
|
||||
|
||||
setupAcra()
|
||||
setupNotificationChannels()
|
||||
|
||||
@@ -294,7 +294,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
||||
databaseHelper.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -168,7 +168,7 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
||||
}
|
||||
}
|
||||
}
|
||||
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
||||
databaseHelper.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@ import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.HistoryUpsertResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||
import java.util.Date
|
||||
@@ -64,9 +64,9 @@ interface HistoryQueries : DbProvider {
|
||||
* Inserts history object if not yet in database
|
||||
* @param history history object
|
||||
*/
|
||||
fun updateHistoryLastRead(history: History) = db.put()
|
||||
fun upsertHistoryLastRead(history: History) = db.put()
|
||||
.`object`(history)
|
||||
.withPutResolver(HistoryLastReadPutResolver())
|
||||
.withPutResolver(HistoryUpsertResolver())
|
||||
.prepare()
|
||||
|
||||
/**
|
||||
@@ -74,12 +74,40 @@ interface HistoryQueries : DbProvider {
|
||||
* Inserts history object if not yet in database
|
||||
* @param historyList history object list
|
||||
*/
|
||||
fun updateHistoryLastRead(historyList: List<History>) = db.put()
|
||||
fun upsertHistoryLastRead(historyList: List<History>) = db.put()
|
||||
.objects(historyList)
|
||||
.withPutResolver(HistoryLastReadPutResolver())
|
||||
.withPutResolver(HistoryUpsertResolver())
|
||||
.prepare()
|
||||
|
||||
fun deleteHistory() = db.delete()
|
||||
fun resetHistoryLastRead(historyId: Long) = db.executeSQL()
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(
|
||||
"""
|
||||
UPDATE ${HistoryTable.TABLE}
|
||||
SET history_last_read = 0
|
||||
WHERE ${HistoryTable.COL_ID} = $historyId
|
||||
""".trimIndent()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun resetHistoryLastRead(historyIds: List<Long>) = db.executeSQL()
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(
|
||||
"""
|
||||
UPDATE ${HistoryTable.TABLE}
|
||||
SET history_last_read = 0
|
||||
WHERE ${HistoryTable.COL_ID} in ${historyIds.joinToString(",", "(", ")")}
|
||||
""".trimIndent()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun dropHistoryTable() = db.delete()
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(HistoryTable.TABLE)
|
||||
|
||||
@@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.data.database.mappers.HistoryPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||
|
||||
class HistoryLastReadPutResolver : HistoryPutResolver() {
|
||||
class HistoryUpsertResolver : HistoryPutResolver() {
|
||||
|
||||
/**
|
||||
* Updates last_read time of chapter
|
||||
@@ -97,14 +97,19 @@ import kotlin.math.max
|
||||
class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
|
||||
companion object {
|
||||
fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent {
|
||||
|
||||
fun newIntent(context: Context, mangaId: Long?, chapterId: Long?): Intent {
|
||||
return Intent(context, ReaderActivity::class.java).apply {
|
||||
putExtra("manga", manga.id)
|
||||
putExtra("chapter", chapter.id)
|
||||
putExtra("manga", mangaId)
|
||||
putExtra("chapter", chapterId)
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
}
|
||||
}
|
||||
|
||||
fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent {
|
||||
return newIntent(context, manga.id, chapter.id)
|
||||
}
|
||||
|
||||
private const val ENABLED_BUTTON_IMAGE_ALPHA = 255
|
||||
private const val DISABLED_BUTTON_IMAGE_ALPHA = 64
|
||||
|
||||
|
||||
@@ -449,7 +449,7 @@ class ReaderPresenter(
|
||||
private fun saveChapterHistory(chapter: ReaderChapter) {
|
||||
if (!incognitoMode) {
|
||||
val history = History.create(chapter.chapter).apply { last_read = Date().time }
|
||||
db.updateHistoryLastRead(history).asRxCompletable()
|
||||
db.upsertHistoryLastRead(history).asRxCompletable()
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.history
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
class ClearHistoryDialogController : DialogController() {
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialAlertDialogBuilder(activity!!)
|
||||
.setMessage(R.string.clear_history_confirmation)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
(targetController as? HistoryController)
|
||||
?.presenter
|
||||
?.deleteAllHistory()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.history
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
|
||||
/**
|
||||
* Adapter of HistoryHolder.
|
||||
* Connection between Fragment and Holder
|
||||
* Holder updates should be called from here.
|
||||
*
|
||||
* @param controller a HistoryController object
|
||||
* @constructor creates an instance of the adapter.
|
||||
*/
|
||||
class HistoryAdapter(controller: HistoryController) :
|
||||
FlexibleAdapter<IFlexible<*>>(null, controller, true) {
|
||||
|
||||
val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
val resumeClickListener: OnResumeClickListener = controller
|
||||
val removeClickListener: OnRemoveClickListener = controller
|
||||
val itemClickListener: OnItemClickListener = controller
|
||||
|
||||
/**
|
||||
* DecimalFormat used to display correct chapter number
|
||||
*/
|
||||
val decimalFormat = DecimalFormat(
|
||||
"#.###",
|
||||
DecimalFormatSymbols()
|
||||
.apply { decimalSeparator = '.' },
|
||||
)
|
||||
|
||||
init {
|
||||
setDisplayHeadersAtStartUp(true)
|
||||
}
|
||||
|
||||
interface OnResumeClickListener {
|
||||
fun onResumeClick(position: Int)
|
||||
}
|
||||
|
||||
interface OnRemoveClickListener {
|
||||
fun onRemoveClick(position: Int)
|
||||
}
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun onItemClick(position: Int)
|
||||
}
|
||||
}
|
||||
@@ -1,192 +1,65 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.history
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.presentation.history.HistoryScreen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.databinding.HistoryControllerBinding
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.databinding.ComposeControllerBinding
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.ProgressItem
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import logcat.LogPriority
|
||||
import reactivecircus.flowbinding.appcompat.queryTextChanges
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Fragment that shows recently read manga.
|
||||
*/
|
||||
class HistoryController :
|
||||
NucleusController<HistoryControllerBinding, HistoryPresenter>(),
|
||||
RootController,
|
||||
FlexibleAdapter.OnUpdateListener,
|
||||
FlexibleAdapter.EndlessScrollListener,
|
||||
HistoryAdapter.OnRemoveClickListener,
|
||||
HistoryAdapter.OnResumeClickListener,
|
||||
HistoryAdapter.OnItemClickListener,
|
||||
RemoveHistoryDialog.Listener {
|
||||
NucleusController<ComposeControllerBinding, HistoryPresenter>(),
|
||||
RootController {
|
||||
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Adapter containing the recent manga.
|
||||
*/
|
||||
var adapter: HistoryAdapter? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* Endless loading item.
|
||||
*/
|
||||
private var progressItem: ProgressItem? = null
|
||||
|
||||
/**
|
||||
* Search query.
|
||||
*/
|
||||
private var query = ""
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return resources?.getString(R.string.label_recent_manga)
|
||||
}
|
||||
override fun getTitle(): String? = resources?.getString(R.string.label_recent_manga)
|
||||
|
||||
override fun createPresenter(): HistoryPresenter {
|
||||
return HistoryPresenter()
|
||||
}
|
||||
override fun createPresenter(): HistoryPresenter = HistoryPresenter()
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater) = HistoryControllerBinding.inflate(inflater)
|
||||
override fun createBinding(inflater: LayoutInflater): ComposeControllerBinding =
|
||||
ComposeControllerBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
binding.recycler.applyInsetter {
|
||||
type(navigationBars = true) {
|
||||
padding()
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize adapter
|
||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
adapter = HistoryAdapter(this@HistoryController)
|
||||
binding.recycler.setHasFixedSize(true)
|
||||
binding.recycler.adapter = adapter
|
||||
adapter?.fastScroller = binding.fastScroller
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
adapter = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate adapter with chapters
|
||||
*
|
||||
* @param mangaHistory list of manga history
|
||||
*/
|
||||
fun onNextManga(mangaHistory: List<HistoryItem>, cleanBatch: Boolean = false) {
|
||||
if (adapter?.itemCount ?: 0 == 0) {
|
||||
resetProgressItem()
|
||||
}
|
||||
if (cleanBatch) {
|
||||
adapter?.updateDataSet(mangaHistory)
|
||||
} else {
|
||||
adapter?.onLoadMoreComplete(mangaHistory)
|
||||
}
|
||||
binding.recycler.onAnimationsFinished {
|
||||
(activity as? MainActivity)?.ready = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely error if next page load fails
|
||||
*/
|
||||
fun onAddPageError(error: Throwable) {
|
||||
adapter?.onLoadMoreComplete(null)
|
||||
adapter?.endlessTargetCount = 1
|
||||
logcat(LogPriority.ERROR, error)
|
||||
}
|
||||
|
||||
override fun onUpdateEmptyView(size: Int) {
|
||||
if (size > 0) {
|
||||
binding.emptyView.hide()
|
||||
} else {
|
||||
binding.emptyView.show(R.string.information_no_recent_manga)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new progress item and reenables the scroll listener.
|
||||
*/
|
||||
private fun resetProgressItem() {
|
||||
progressItem = ProgressItem()
|
||||
adapter?.endlessTargetCount = 0
|
||||
adapter?.setEndlessScrollListener(this, progressItem!!)
|
||||
}
|
||||
|
||||
override fun onLoadMore(lastPosition: Int, currentPage: Int) {
|
||||
val view = view ?: return
|
||||
if (BackupRestoreService.isRunning(view.context.applicationContext)) {
|
||||
onAddPageError(Throwable())
|
||||
return
|
||||
}
|
||||
val adapter = adapter ?: return
|
||||
presenter.requestNext(adapter.itemCount - adapter.headerItems.size, query)
|
||||
}
|
||||
|
||||
override fun noMoreLoad(newItemsSize: Int) {}
|
||||
|
||||
override fun onResumeClick(position: Int) {
|
||||
val activity = activity ?: return
|
||||
val (manga, chapter, _) = (adapter?.getItem(position) as? HistoryItem)?.mch ?: return
|
||||
|
||||
val nextChapter = presenter.getNextChapter(chapter, manga)
|
||||
if (nextChapter != null) {
|
||||
val intent = ReaderActivity.newIntent(activity, manga, nextChapter)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
activity.toast(R.string.no_next_chapter)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRemoveClick(position: Int) {
|
||||
val (manga, _, history) = (adapter?.getItem(position) as? HistoryItem)?.mch ?: return
|
||||
RemoveHistoryDialog(this, manga, history).showDialog(router)
|
||||
}
|
||||
|
||||
override fun onItemClick(position: Int) {
|
||||
val manga = (adapter?.getItem(position) as? HistoryItem)?.mch?.manga ?: return
|
||||
router.pushController(MangaController(manga).withFadeTransaction())
|
||||
}
|
||||
|
||||
override fun removeHistory(manga: Manga, history: History, all: Boolean) {
|
||||
if (all) {
|
||||
// Reset last read of chapter to 0L
|
||||
presenter.removeAllFromHistory(manga.id!!)
|
||||
} else {
|
||||
// Remove all chapters belonging to manga from library
|
||||
presenter.removeFromHistory(history)
|
||||
binding.root.setContent {
|
||||
HistoryScreen(
|
||||
composeView = binding.root,
|
||||
presenter = presenter,
|
||||
onClickItem = { (manga, _, _) ->
|
||||
router.pushController(MangaController(manga).withFadeTransaction())
|
||||
},
|
||||
onClickResume = { (manga, chapter, _) ->
|
||||
presenter.getNextChapterForManga(manga, chapter)
|
||||
},
|
||||
onClickDelete = { (manga, _, history), all ->
|
||||
if (all) {
|
||||
// Reset last read of chapter to 0L
|
||||
presenter.removeAllFromHistory(manga.id!!)
|
||||
} else {
|
||||
// Remove all chapters belonging to manga from library
|
||||
presenter.removeFromHistory(history)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,46 +74,33 @@ class HistoryController :
|
||||
searchView.clearFocus()
|
||||
}
|
||||
searchView.queryTextChanges()
|
||||
.drop(1) // Drop first event after subscribed
|
||||
.filter { router.backstack.lastOrNull()?.controller == this }
|
||||
.onEach {
|
||||
query = it.toString()
|
||||
presenter.updateList(query)
|
||||
presenter.search(query)
|
||||
}
|
||||
.launchIn(viewScope)
|
||||
|
||||
// Fixes problem with the overflow icon showing up in lieu of search
|
||||
searchItem.fixExpand(
|
||||
onExpand = { invalidateMenuOnExpand() },
|
||||
)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
return when (item.itemId) {
|
||||
R.id.action_clear_history -> {
|
||||
val ctrl = ClearHistoryDialogController()
|
||||
ctrl.targetController = this@HistoryController
|
||||
ctrl.showDialog(router)
|
||||
val dialog = ClearHistoryDialogController()
|
||||
dialog.targetController = this@HistoryController
|
||||
dialog.showDialog(router)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
class ClearHistoryDialogController : DialogController() {
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialAlertDialogBuilder(activity!!)
|
||||
.setMessage(R.string.clear_history_confirmation)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
(targetController as? HistoryController)?.clearHistory()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearHistory() {
|
||||
db.deleteHistory().executeAsBlocking()
|
||||
activity?.toast(R.string.clear_history_completed)
|
||||
fun openChapter(chapter: Chapter?) {
|
||||
val activity = activity ?: return
|
||||
if (chapter != null) {
|
||||
val intent = ReaderActivity.newIntent(activity, chapter.manga_id, chapter.id)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
activity.toast(R.string.no_next_chapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.history
|
||||
|
||||
import android.view.View
|
||||
import coil.dispose
|
||||
import coil.load
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||
import eu.kanade.tachiyomi.databinding.HistoryItemBinding
|
||||
import eu.kanade.tachiyomi.util.lang.toTimestampString
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Holder that contains recent manga item
|
||||
* Uses R.layout.item_recently_read.
|
||||
* UI related actions should be called from here.
|
||||
*
|
||||
* @param view the inflated view for this holder.
|
||||
* @param adapter the adapter handling this holder.
|
||||
* @constructor creates a new recent chapter holder.
|
||||
*/
|
||||
class HistoryHolder(
|
||||
view: View,
|
||||
val adapter: HistoryAdapter,
|
||||
) : FlexibleViewHolder(view, adapter) {
|
||||
|
||||
private val binding = HistoryItemBinding.bind(view)
|
||||
|
||||
init {
|
||||
binding.holder.setOnClickListener {
|
||||
adapter.itemClickListener.onItemClick(bindingAdapterPosition)
|
||||
}
|
||||
|
||||
binding.remove.setOnClickListener {
|
||||
adapter.removeClickListener.onRemoveClick(bindingAdapterPosition)
|
||||
}
|
||||
|
||||
binding.resume.setOnClickListener {
|
||||
adapter.resumeClickListener.onResumeClick(bindingAdapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set values of view
|
||||
*
|
||||
* @param item item containing history information
|
||||
*/
|
||||
fun bind(item: MangaChapterHistory) {
|
||||
// Retrieve objects
|
||||
val (manga, chapter, history) = item
|
||||
|
||||
// Set manga title
|
||||
binding.mangaTitle.text = manga.title
|
||||
|
||||
// Set chapter number + timestamp
|
||||
if (chapter.chapter_number > -1f) {
|
||||
val formattedNumber = adapter.decimalFormat.format(chapter.chapter_number.toDouble())
|
||||
binding.mangaSubtitle.text = itemView.context.getString(
|
||||
R.string.recent_manga_time,
|
||||
formattedNumber,
|
||||
Date(history.last_read).toTimestampString(),
|
||||
)
|
||||
} else {
|
||||
binding.mangaSubtitle.text = Date(history.last_read).toTimestampString()
|
||||
}
|
||||
|
||||
// Set cover
|
||||
binding.cover.dispose()
|
||||
binding.cover.load(item.manga)
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.history
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||
import eu.kanade.tachiyomi.ui.recent.DateSectionItem
|
||||
|
||||
class HistoryItem(val mch: MangaChapterHistory, header: DateSectionItem) :
|
||||
AbstractSectionableItem<HistoryHolder, DateSectionItem>(header) {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.history_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): HistoryHolder {
|
||||
return HistoryHolder(view, adapter as HistoryAdapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: HistoryHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?,
|
||||
) {
|
||||
holder.bind(mch)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is HistoryItem) {
|
||||
return mch.manga.id == other.mch.manga.id
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return mch.manga.id!!.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -1,157 +1,135 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.history
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import androidx.paging.insertSeparators
|
||||
import androidx.paging.map
|
||||
import eu.kanade.domain.history.interactor.DeleteHistoryTable
|
||||
import eu.kanade.domain.history.interactor.GetHistory
|
||||
import eu.kanade.domain.history.interactor.GetNextChapterForManga
|
||||
import eu.kanade.domain.history.interactor.RemoveHistoryById
|
||||
import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.recent.DateSectionItem
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.toDateKey
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.DateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.TreeMap
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Presenter of HistoryFragment.
|
||||
* Contains information and data for fragment.
|
||||
* Observable updates should be called from here.
|
||||
*/
|
||||
class HistoryPresenter : BasePresenter<HistoryController>() {
|
||||
class HistoryPresenter(
|
||||
private val getHistory: GetHistory = Injekt.get(),
|
||||
private val getNextChapterForManga: GetNextChapterForManga = Injekt.get(),
|
||||
private val deleteHistoryTable: DeleteHistoryTable = Injekt.get(),
|
||||
private val removeHistoryById: RemoveHistoryById = Injekt.get(),
|
||||
private val removeHistoryByMangaId: RemoveHistoryByMangaId = Injekt.get(),
|
||||
) : BasePresenter<HistoryController>() {
|
||||
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private val relativeTime: Int = preferences.relativeTime().get()
|
||||
private val dateFormat: DateFormat = preferences.dateFormat()
|
||||
|
||||
private var recentMangaSubscription: Subscription? = null
|
||||
private var _query: MutableStateFlow<String> = MutableStateFlow("")
|
||||
private var _state: MutableStateFlow<HistoryState> = MutableStateFlow(HistoryState.EMPTY)
|
||||
val state: StateFlow<HistoryState> = _state
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
// Used to get a list of recently read manga
|
||||
updateList()
|
||||
}
|
||||
|
||||
fun requestNext(offset: Int, search: String = "") {
|
||||
getRecentMangaObservable(offset = offset, search = search)
|
||||
.subscribeLatestCache(
|
||||
{ view, mangas ->
|
||||
view.onNextManga(mangas)
|
||||
},
|
||||
HistoryController::onAddPageError,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent manga observable
|
||||
* @return list of history
|
||||
*/
|
||||
private fun getRecentMangaObservable(limit: Int = 25, offset: Int = 0, search: String = ""): Observable<List<HistoryItem>> {
|
||||
// Set date limit for recent manga
|
||||
val cal = Calendar.getInstance().apply {
|
||||
time = Date()
|
||||
add(Calendar.YEAR, -50)
|
||||
}
|
||||
|
||||
return db.getRecentManga(cal.time, limit, offset, search).asRxObservable()
|
||||
.map { recents ->
|
||||
val map = TreeMap<Date, MutableList<MangaChapterHistory>> { d1, d2 -> d2.compareTo(d1) }
|
||||
val byDay = recents
|
||||
.groupByTo(map) { it.history.last_read.toDateKey() }
|
||||
byDay.flatMap { entry ->
|
||||
val dateItem = DateSectionItem(entry.key, relativeTime, dateFormat)
|
||||
entry.value.map { HistoryItem(it, dateItem) }
|
||||
}
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset last read of chapter to 0L
|
||||
* @param history history belonging to chapter
|
||||
*/
|
||||
fun removeFromHistory(history: History) {
|
||||
history.last_read = 0L
|
||||
db.updateHistoryLastRead(history).asRxObservable()
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull a list of history from the db
|
||||
* @param search a search query to use for filtering
|
||||
*/
|
||||
fun updateList(search: String = "") {
|
||||
recentMangaSubscription?.unsubscribe()
|
||||
recentMangaSubscription = getRecentMangaObservable(search = search)
|
||||
.subscribeLatestCache(
|
||||
{ view, mangas ->
|
||||
view.onNextManga(mangas, true)
|
||||
},
|
||||
HistoryController::onAddPageError,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all chapters belonging to manga from history.
|
||||
* @param mangaId id of manga
|
||||
*/
|
||||
fun removeAllFromHistory(mangaId: Long) {
|
||||
db.getHistoryByMangaId(mangaId).asRxSingle()
|
||||
.map { list ->
|
||||
list.forEach { it.last_read = 0L }
|
||||
db.updateHistoryLastRead(list).executeAsBlocking()
|
||||
}
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the next chapter of the given one.
|
||||
*
|
||||
* @param chapter the chapter of the history object.
|
||||
* @param manga the manga of the chapter.
|
||||
*/
|
||||
fun getNextChapter(chapter: Chapter, manga: Manga): Chapter? {
|
||||
if (!chapter.read) {
|
||||
return chapter
|
||||
}
|
||||
|
||||
val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
|
||||
Manga.CHAPTER_SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
|
||||
Manga.CHAPTER_SORTING_NUMBER -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
|
||||
Manga.CHAPTER_SORTING_UPLOAD_DATE -> { c1, c2 -> c1.date_upload.compareTo(c2.date_upload) }
|
||||
else -> throw NotImplementedError("Unknown sorting method")
|
||||
}
|
||||
|
||||
val chapters = db.getChapters(manga).executeAsBlocking()
|
||||
.sortedWith { c1, c2 -> sortFunction(c1, c2) }
|
||||
|
||||
val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id }
|
||||
return when (manga.sorting) {
|
||||
Manga.CHAPTER_SORTING_SOURCE -> chapters.getOrNull(currChapterIndex + 1)
|
||||
Manga.CHAPTER_SORTING_NUMBER -> {
|
||||
val chapterNumber = chapter.chapter_number
|
||||
|
||||
((currChapterIndex + 1) until chapters.size)
|
||||
.map { chapters[it] }
|
||||
.firstOrNull {
|
||||
it.chapter_number > chapterNumber &&
|
||||
it.chapter_number <= chapterNumber + 1
|
||||
presenterScope.launchIO {
|
||||
_state.update { state ->
|
||||
state.copy(
|
||||
list = _query.flatMapLatest { query ->
|
||||
getHistory.subscribe(query)
|
||||
.map { pagingData ->
|
||||
pagingData
|
||||
.map {
|
||||
UiModel.History(it)
|
||||
}
|
||||
.insertSeparators { before, after ->
|
||||
val beforeDate =
|
||||
before?.item?.history?.last_read?.toDateKey()
|
||||
val afterDate =
|
||||
after?.item?.history?.last_read?.toDateKey()
|
||||
when {
|
||||
beforeDate == null && afterDate != null -> UiModel.Header(
|
||||
afterDate,
|
||||
)
|
||||
beforeDate != null && afterDate != null -> UiModel.Header(
|
||||
afterDate,
|
||||
)
|
||||
// Return null to avoid adding a separator between two items.
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.cachedIn(presenterScope),
|
||||
)
|
||||
}
|
||||
Manga.CHAPTER_SORTING_UPLOAD_DATE -> {
|
||||
chapters.drop(currChapterIndex + 1)
|
||||
.firstOrNull { it.date_upload >= chapter.date_upload }
|
||||
}
|
||||
}
|
||||
|
||||
fun search(query: String) {
|
||||
presenterScope.launchIO {
|
||||
_query.emit(query)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFromHistory(history: History) {
|
||||
presenterScope.launchIO {
|
||||
removeHistoryById.await(history)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAllFromHistory(mangaId: Long) {
|
||||
presenterScope.launchIO {
|
||||
removeHistoryByMangaId.await(mangaId)
|
||||
}
|
||||
}
|
||||
|
||||
fun getNextChapterForManga(manga: Manga, chapter: Chapter) {
|
||||
presenterScope.launchIO {
|
||||
val chapter = getNextChapterForManga.await(manga, chapter)
|
||||
view?.openChapter(chapter)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAllHistory() {
|
||||
presenterScope.launchIO {
|
||||
val result = deleteHistoryTable.await()
|
||||
if (!result) return@launchIO
|
||||
launchUI {
|
||||
view?.activity?.toast(R.string.clear_history_completed)
|
||||
}
|
||||
else -> throw NotImplementedError("Unknown sorting method")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class UiModel {
|
||||
data class History(val item: MangaChapterHistory) : UiModel()
|
||||
data class Header(val date: Date) : UiModel()
|
||||
}
|
||||
|
||||
data class HistoryState(
|
||||
val list: Flow<PagingData<UiModel>>? = null,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY = HistoryState(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.history
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.widget.DialogCheckboxView
|
||||
|
||||
class RemoveHistoryDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
where T : Controller, T : RemoveHistoryDialog.Listener {
|
||||
|
||||
private var manga: Manga? = null
|
||||
|
||||
private var history: History? = null
|
||||
|
||||
constructor(target: T, manga: Manga, history: History) : this() {
|
||||
this.manga = manga
|
||||
this.history = history
|
||||
targetController = target
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val activity = activity!!
|
||||
|
||||
// Create custom view
|
||||
val dialogCheckboxView = DialogCheckboxView(activity).apply {
|
||||
setDescription(R.string.dialog_with_checkbox_remove_description)
|
||||
setOptionDescription(R.string.dialog_with_checkbox_reset)
|
||||
}
|
||||
|
||||
return MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.action_remove)
|
||||
.setView(dialogCheckboxView)
|
||||
.setPositiveButton(R.string.action_remove) { _, _ -> onPositive(dialogCheckboxView.isChecked()) }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
}
|
||||
|
||||
private fun onPositive(checked: Boolean) {
|
||||
val target = targetController as? Listener ?: return
|
||||
val manga = manga ?: return
|
||||
val history = history ?: return
|
||||
|
||||
target.removeHistory(manga, history, checked)
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun removeHistory(manga: Manga, history: History, all: Boolean)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user