mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-29 01:29:02 +03:00
history changes
-history is now divided in an anime and manga section -TODO: fix saving anime to the history, doesn't work currently
This commit is contained in:
parent
1c78880221
commit
6f4d793813
16 changed files with 895 additions and 6 deletions
|
@ -5,6 +5,6 @@ package eu.kanade.tachiyomi.data.database.models
|
|||
*
|
||||
* @param manga object containing manga
|
||||
* @param chapter object containing chater
|
||||
* @param history object containing history
|
||||
* @param animehistory object containing history
|
||||
*/
|
||||
data class AnimeEpisodeHistory(val anime: Anime, val episode: Episode, val history: AnimeHistory)
|
||||
data class AnimeEpisodeHistory(val anime: Anime, val episode: Episode, val animehistory: AnimeHistory)
|
||||
|
|
|
@ -16,7 +16,7 @@ import eu.kanade.tachiyomi.ui.base.controller.RootController
|
|||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryController
|
||||
import eu.kanade.tachiyomi.ui.download.DownloadController
|
||||
import eu.kanade.tachiyomi.ui.recent.history.HistoryController
|
||||
import eu.kanade.tachiyomi.ui.recent.HistoryTabsController
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsController
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsMainController
|
||||
import eu.kanade.tachiyomi.util.preference.add
|
||||
|
@ -81,7 +81,7 @@ class MoreController :
|
|||
iconRes = R.drawable.ic_history_24dp
|
||||
iconTint = tintColor
|
||||
onClick {
|
||||
router.pushController(HistoryController().withFadeTransaction())
|
||||
router.pushController(HistoryTabsController().withFadeTransaction())
|
||||
}
|
||||
}
|
||||
preference {
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
package eu.kanade.tachiyomi.ui.recent
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
import com.bluelinelabs.conductor.Router
|
||||
import com.bluelinelabs.conductor.RouterTransaction
|
||||
import com.bluelinelabs.conductor.support.RouterPagerAdapter
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.PagerControllerBinding
|
||||
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.RxController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.recent.animehistory.AnimeHistoryController
|
||||
import eu.kanade.tachiyomi.ui.recent.history.HistoryController
|
||||
|
||||
class HistoryTabsController() :
|
||||
RxController<PagerControllerBinding>(),
|
||||
RootController,
|
||||
TabbedController {
|
||||
|
||||
private var adapter: HistroyTabsAdapter? = null
|
||||
|
||||
override fun getTitle(): String {
|
||||
return resources!!.getString(R.string.history)
|
||||
}
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater) = PagerControllerBinding.inflate(inflater)
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
adapter = HistroyTabsAdapter()
|
||||
binding.pager.adapter = adapter
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
super.onDestroyView(view)
|
||||
adapter = null
|
||||
}
|
||||
|
||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||
super.onChangeStarted(handler, type)
|
||||
if (type.isEnter) {
|
||||
(activity as? MainActivity)?.binding?.tabs?.apply {
|
||||
setupWithViewPager(binding.pager)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun configureTabs(tabs: TabLayout) {
|
||||
with(tabs) {
|
||||
tabGravity = TabLayout.GRAVITY_FILL
|
||||
tabMode = TabLayout.MODE_FIXED
|
||||
}
|
||||
}
|
||||
|
||||
private inner class HistroyTabsAdapter : RouterPagerAdapter(this@HistoryTabsController) {
|
||||
|
||||
private val tabTitles = listOf(
|
||||
R.string.label_history,
|
||||
R.string.label_animehistory
|
||||
)
|
||||
.map { resources!!.getString(it) }
|
||||
|
||||
override fun getCount(): Int {
|
||||
return tabTitles.size
|
||||
}
|
||||
|
||||
override fun configureRouter(router: Router, position: Int) {
|
||||
if (!router.hasRootController()) {
|
||||
val controller: Controller = when (position) {
|
||||
HISTORY_CONTROLLER -> HistoryController()
|
||||
ANIME_HISTORY_CONTROLLER -> AnimeHistoryController()
|
||||
else -> error("Wrong position $position")
|
||||
}
|
||||
router.setRoot(RouterTransaction.with(controller))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPageTitle(position: Int): CharSequence {
|
||||
return tabTitles[position]
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val HISTORY_CONTROLLER = 0
|
||||
const val ANIME_HISTORY_CONTROLLER = 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package eu.kanade.tachiyomi.ui.recent.animehistory
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.source.AnimeSourceManager
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
|
||||
/**
|
||||
* Adapter of AnimeHistoryHolder.
|
||||
* Connection between Fragment and Holder
|
||||
* Holder updates should be called from here.
|
||||
*
|
||||
* @param controller a AnimeHistoryController object
|
||||
* @constructor creates an instance of the adapter.
|
||||
*/
|
||||
class AnimeHistoryAdapter(controller: AnimeHistoryController) :
|
||||
FlexibleAdapter<IFlexible<*>>(null, controller, true) {
|
||||
|
||||
val sourceManager: AnimeSourceManager 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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,261 @@
|
|||
package eu.kanade.tachiyomi.ui.recent.animehistory
|
||||
|
||||
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.afollestad.materialdialogs.MaterialDialog
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
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.Anime
|
||||
import eu.kanade.tachiyomi.data.database.models.AnimeHistory
|
||||
import eu.kanade.tachiyomi.databinding.AnimeHistoryControllerBinding
|
||||
import eu.kanade.tachiyomi.source.AnimeSourceManager
|
||||
import eu.kanade.tachiyomi.source.model.toEpisodeInfo
|
||||
import eu.kanade.tachiyomi.ui.anime.AnimeController
|
||||
import eu.kanade.tachiyomi.ui.anime.episode.EpisodeItem
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
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.watcher.WatcherActivity
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import reactivecircus.flowbinding.appcompat.queryTextChanges
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.*
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
/**
|
||||
* Fragment that shows recently read anime.
|
||||
*/
|
||||
class AnimeHistoryController :
|
||||
NucleusController<AnimeHistoryControllerBinding, AnimeHistoryPresenter>(),
|
||||
RootController,
|
||||
FlexibleAdapter.OnUpdateListener,
|
||||
FlexibleAdapter.EndlessScrollListener,
|
||||
AnimeHistoryAdapter.OnRemoveClickListener,
|
||||
AnimeHistoryAdapter.OnResumeClickListener,
|
||||
AnimeHistoryAdapter.OnItemClickListener,
|
||||
RemoveAnimeHistoryDialog.Listener {
|
||||
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Adapter containing the recent anime.
|
||||
*/
|
||||
var adapter: AnimeHistoryAdapter? = 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 createPresenter(): AnimeHistoryPresenter {
|
||||
return AnimeHistoryPresenter()
|
||||
}
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater) = AnimeHistoryControllerBinding.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 = AnimeHistoryAdapter(this@AnimeHistoryController)
|
||||
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 animeAnimeHistory list of anime history
|
||||
*/
|
||||
fun onNextAnime(animeAnimeHistory: List<AnimeHistoryItem>, cleanBatch: Boolean = false) {
|
||||
if (adapter?.itemCount ?: 0 == 0 || cleanBatch) {
|
||||
resetProgressItem()
|
||||
}
|
||||
if (cleanBatch) {
|
||||
adapter?.updateDataSet(animeAnimeHistory)
|
||||
} else {
|
||||
adapter?.onLoadMoreComplete(animeAnimeHistory)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely error if next page load fails
|
||||
*/
|
||||
fun onAddPageError(error: Throwable) {
|
||||
adapter?.onLoadMoreComplete(null)
|
||||
adapter?.endlessTargetCount = 1
|
||||
}
|
||||
|
||||
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, query)
|
||||
}
|
||||
|
||||
override fun noMoreLoad(newItemsSize: Int) {}
|
||||
|
||||
override fun onResumeClick(position: Int) {
|
||||
val activity = activity ?: return
|
||||
val (anime, chapter, _) = (adapter?.getItem(position) as? AnimeHistoryItem)?.mch ?: return
|
||||
|
||||
val nextEpisode = presenter.getNextEpisode(chapter, anime)
|
||||
if (nextEpisode != null) {
|
||||
val source = Injekt.get<AnimeSourceManager>().getOrStub(anime.source)
|
||||
val link = runBlocking {
|
||||
return@runBlocking suspendCoroutine<String> { continuation ->
|
||||
var link: String
|
||||
launchIO {
|
||||
try {
|
||||
link = source.getEpisodeLink(nextEpisode.toEpisodeInfo())
|
||||
continuation.resume(link)
|
||||
} catch (e: Throwable) {
|
||||
withUIContext { throw e }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val episodeList: List<EpisodeItem> = Collections.emptyList()
|
||||
val newIntent = WatcherActivity.newIntent(activity, anime, nextEpisode, episodeList, link)
|
||||
startActivity(newIntent)
|
||||
} else {
|
||||
activity.toast(R.string.no_next_chapter)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRemoveClick(position: Int) {
|
||||
val (anime, _, animehistory) = (adapter?.getItem(position) as? AnimeHistoryItem)?.mch ?: return
|
||||
RemoveAnimeHistoryDialog(this, anime, animehistory).showDialog(router)
|
||||
}
|
||||
|
||||
override fun onItemClick(position: Int) {
|
||||
val anime = (adapter?.getItem(position) as? AnimeHistoryItem)?.mch?.anime ?: return
|
||||
router.pushController(AnimeController(anime).withFadeTransaction())
|
||||
}
|
||||
|
||||
override fun removeAnimeHistory(anime: Anime, animehistory: AnimeHistory, all: Boolean) {
|
||||
if (all) {
|
||||
// Reset last read of chapter to 0L
|
||||
presenter.removeAllFromAnimeHistory(anime.id!!)
|
||||
} else {
|
||||
// Remove all chapters belonging to anime from library
|
||||
presenter.removeFromAnimeHistory(animehistory)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.anime_history, menu)
|
||||
val searchItem = menu.findItem(R.id.action_search)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
searchView.maxWidth = Int.MAX_VALUE
|
||||
if (query.isNotEmpty()) {
|
||||
searchItem.expandActionView()
|
||||
searchView.setQuery(query, true)
|
||||
searchView.clearFocus()
|
||||
}
|
||||
searchView.queryTextChanges()
|
||||
.filter { router.backstack.lastOrNull()?.controller() == this }
|
||||
.onEach {
|
||||
query = it.toString()
|
||||
presenter.updateList(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) {
|
||||
R.id.action_clear_anime_history -> {
|
||||
val ctrl = ClearAnimeHistoryDialogController()
|
||||
ctrl.targetController = this@AnimeHistoryController
|
||||
ctrl.showDialog(router)
|
||||
}
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
class ClearAnimeHistoryDialogController : DialogController() {
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialDialog(activity!!)
|
||||
.message(R.string.clear_history_confirmation)
|
||||
.positiveButton(android.R.string.ok) {
|
||||
(targetController as? AnimeHistoryController)?.clearAnimeHistory()
|
||||
}
|
||||
.negativeButton(android.R.string.cancel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearAnimeHistory() {
|
||||
db.deleteHistory().executeAsBlocking()
|
||||
activity?.toast(R.string.clear_history_completed)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package eu.kanade.tachiyomi.ui.recent.animehistory
|
||||
|
||||
import android.view.View
|
||||
import coil.clear
|
||||
import coil.loadAny
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.AnimeEpisodeHistory
|
||||
import eu.kanade.tachiyomi.databinding.AnimeHistoryItemBinding
|
||||
import eu.kanade.tachiyomi.util.lang.toTimestampString
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Holder that contains recent anime 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 AnimeHistoryHolder(
|
||||
view: View,
|
||||
val adapter: AnimeHistoryAdapter
|
||||
) : FlexibleViewHolder(view, adapter) {
|
||||
|
||||
private val binding = AnimeHistoryItemBinding.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 animehistory information
|
||||
*/
|
||||
fun bind(item: AnimeEpisodeHistory) {
|
||||
// Retrieve objects
|
||||
val (anime, chapter, animehistory) = item
|
||||
|
||||
// Set anime title
|
||||
binding.animeTitle.text = anime.title
|
||||
|
||||
// Set chapter number + timestamp
|
||||
if (chapter.episode_number > -1f) {
|
||||
val formattedNumber = adapter.decimalFormat.format(chapter.episode_number.toDouble())
|
||||
binding.animeSubtitle.text = itemView.context.getString(
|
||||
R.string.recent_manga_time,
|
||||
formattedNumber,
|
||||
Date(animehistory.episode_id).toTimestampString()
|
||||
)
|
||||
} else {
|
||||
binding.animeSubtitle.text = Date(animehistory.last_seen).toTimestampString()
|
||||
}
|
||||
|
||||
// Set cover
|
||||
val radius = itemView.context.resources.getDimension(R.dimen.card_radius)
|
||||
binding.cover.clear()
|
||||
binding.cover.loadAny(item.anime) {
|
||||
transformations(RoundedCornersTransformation(radius))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package eu.kanade.tachiyomi.ui.recent.animehistory
|
||||
|
||||
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.AnimeEpisodeHistory
|
||||
import eu.kanade.tachiyomi.ui.recent.DateSectionItem
|
||||
|
||||
class AnimeHistoryItem(val mch: AnimeEpisodeHistory, header: DateSectionItem) :
|
||||
AbstractSectionableItem<AnimeHistoryHolder, DateSectionItem>(header) {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.anime_history_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): AnimeHistoryHolder {
|
||||
return AnimeHistoryHolder(view, adapter as AnimeHistoryAdapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: AnimeHistoryHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?
|
||||
) {
|
||||
holder.bind(mch)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is AnimeHistoryItem) {
|
||||
return mch.anime.id == other.mch.anime.id
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return mch.anime.id!!.hashCode()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
package eu.kanade.tachiyomi.ui.recent.animehistory
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.data.database.AnimeDatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Anime
|
||||
import eu.kanade.tachiyomi.data.database.models.AnimeEpisodeHistory
|
||||
import eu.kanade.tachiyomi.data.database.models.AnimeHistory
|
||||
import eu.kanade.tachiyomi.data.database.models.Episode
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.recent.DateSectionItem
|
||||
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.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.TreeMap
|
||||
|
||||
/**
|
||||
* Presenter of AnimeHistoryFragment.
|
||||
* Contains information and data for fragment.
|
||||
* Observable updates should be called from here.
|
||||
*/
|
||||
class AnimeHistoryPresenter : BasePresenter<AnimeHistoryController>() {
|
||||
|
||||
/**
|
||||
* Used to connect to database
|
||||
*/
|
||||
val db: AnimeDatabaseHelper by injectLazy()
|
||||
|
||||
private var recentAnimeSubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
// Used to get a list of recently read anime
|
||||
updateList()
|
||||
}
|
||||
|
||||
fun requestNext(offset: Int, search: String = "") {
|
||||
getRecentAnimeObservable(offset = offset, search = search)
|
||||
.subscribeLatestCache(
|
||||
{ view, animes ->
|
||||
view.onNextAnime(animes)
|
||||
},
|
||||
AnimeHistoryController::onAddPageError
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent anime observable
|
||||
* @return list of animehistory
|
||||
*/
|
||||
private fun getRecentAnimeObservable(limit: Int = 25, offset: Int = 0, search: String = ""): Observable<List<AnimeHistoryItem>> {
|
||||
// Set date limit for recent anime
|
||||
val cal = Calendar.getInstance().apply {
|
||||
time = Date()
|
||||
add(Calendar.YEAR, -50)
|
||||
}
|
||||
|
||||
return db.getRecentAnime(cal.time, limit, offset, search).asRxObservable()
|
||||
.map { recents ->
|
||||
val map = TreeMap<Date, MutableList<AnimeEpisodeHistory>> { d1, d2 -> d2.compareTo(d1) }
|
||||
val byDay = recents
|
||||
.groupByTo(map, { it.animehistory.last_seen.toDateKey() })
|
||||
byDay.flatMap { entry ->
|
||||
val dateItem = DateSectionItem(entry.key)
|
||||
entry.value.map { AnimeHistoryItem(it, dateItem) }
|
||||
}
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset last read of chapter to 0L
|
||||
* @param animehistory animehistory belonging to chapter
|
||||
*/
|
||||
fun removeFromAnimeHistory(animehistory: AnimeHistory) {
|
||||
animehistory.last_seen = 0L
|
||||
db.updateAnimeHistoryLastSeen(animehistory).asRxObservable()
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull a list of animehistory from the db
|
||||
* @param search a search query to use for filtering
|
||||
*/
|
||||
fun updateList(search: String = "") {
|
||||
recentAnimeSubscription?.unsubscribe()
|
||||
recentAnimeSubscription = getRecentAnimeObservable(search = search)
|
||||
.subscribeLatestCache(
|
||||
{ view, animes ->
|
||||
view.onNextAnime(animes, true)
|
||||
},
|
||||
AnimeHistoryController::onAddPageError
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all chapters belonging to anime from animehistory.
|
||||
* @param animeId id of anime
|
||||
*/
|
||||
fun removeAllFromAnimeHistory(animeId: Long) {
|
||||
db.getHistoryByAnimeId(animeId).asRxSingle()
|
||||
.map { list ->
|
||||
list.forEach { it.last_seen = 0L }
|
||||
db.updateAnimeHistoryLastSeen(list).executeAsBlocking()
|
||||
}
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the next chapter of the given one.
|
||||
*
|
||||
* @param chapter the chapter of the animehistory object.
|
||||
* @param anime the anime of the chapter.
|
||||
*/
|
||||
fun getNextEpisode(chapter: Episode, anime: Anime): Episode? {
|
||||
if (!chapter.seen) {
|
||||
return chapter
|
||||
}
|
||||
|
||||
val sortFunction: (Episode, Episode) -> Int = when (anime.sorting) {
|
||||
Anime.EPISODE_SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
|
||||
Anime.EPISODE_SORTING_NUMBER -> { c1, c2 -> c1.episode_number.compareTo(c2.episode_number) }
|
||||
Anime.EPISODE_SORTING_UPLOAD_DATE -> { c1, c2 -> c1.date_upload.compareTo(c2.date_upload) }
|
||||
else -> throw NotImplementedError("Unknown sorting method")
|
||||
}
|
||||
|
||||
val chapters = db.getEpisodes(anime).executeAsBlocking()
|
||||
.sortedWith { c1, c2 -> sortFunction(c1, c2) }
|
||||
|
||||
val currEpisodeIndex = chapters.indexOfFirst { chapter.id == it.id }
|
||||
return when (anime.sorting) {
|
||||
Anime.EPISODE_SORTING_SOURCE -> chapters.getOrNull(currEpisodeIndex + 1)
|
||||
Anime.EPISODE_SORTING_NUMBER -> {
|
||||
val chapterNumber = chapter.episode_number
|
||||
|
||||
((currEpisodeIndex + 1) until chapters.size)
|
||||
.map { chapters[it] }
|
||||
.firstOrNull {
|
||||
it.episode_number > chapterNumber &&
|
||||
it.episode_number <= chapterNumber + 1
|
||||
}
|
||||
}
|
||||
Anime.EPISODE_SORTING_UPLOAD_DATE -> {
|
||||
chapters.drop(currEpisodeIndex + 1)
|
||||
.firstOrNull { it.date_upload >= chapter.date_upload }
|
||||
}
|
||||
else -> throw NotImplementedError("Unknown sorting method")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package eu.kanade.tachiyomi.ui.recent.animehistory
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.customview.customView
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Anime
|
||||
import eu.kanade.tachiyomi.data.database.models.AnimeHistory
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.widget.DialogCheckboxView
|
||||
|
||||
class RemoveAnimeHistoryDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
where T : Controller, T : RemoveAnimeHistoryDialog.Listener {
|
||||
|
||||
private var anime: Anime? = null
|
||||
|
||||
private var animehistory: AnimeHistory? = null
|
||||
|
||||
constructor(target: T, anime: Anime, animehistory: AnimeHistory) : this() {
|
||||
this.anime = anime
|
||||
this.animehistory = animehistory
|
||||
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 MaterialDialog(activity)
|
||||
.title(R.string.action_remove)
|
||||
.customView(view = dialogCheckboxView, horizontalPadding = true)
|
||||
.positiveButton(R.string.action_remove) { onPositive(dialogCheckboxView.isChecked()) }
|
||||
.negativeButton(android.R.string.cancel)
|
||||
}
|
||||
|
||||
private fun onPositive(checked: Boolean) {
|
||||
val target = targetController as? Listener ?: return
|
||||
val anime = anime ?: return
|
||||
val animehistory = animehistory ?: return
|
||||
|
||||
target.removeAnimeHistory(anime, animehistory, checked)
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun removeAnimeHistory(anime: Anime, animehistory: AnimeHistory, all: Boolean)
|
||||
}
|
||||
}
|
|
@ -17,10 +17,17 @@ import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
|
|||
import com.google.android.exoplayer2.util.MimeTypes
|
||||
import com.google.android.exoplayer2.util.Util
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.AnimeDatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Anime
|
||||
import eu.kanade.tachiyomi.data.database.models.AnimeHistory
|
||||
import eu.kanade.tachiyomi.data.database.models.Episode
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.ui.anime.episode.EpisodeItem
|
||||
import eu.kanade.tachiyomi.util.view.hideBar
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.*
|
||||
|
||||
const val STATE_RESUME_WINDOW = "resumeWindow"
|
||||
const val STATE_RESUME_POSITION = "resumePosition"
|
||||
|
@ -29,6 +36,9 @@ const val STATE_PLAYER_PLAYING = "playerOnPlay"
|
|||
|
||||
class WatcherActivity : AppCompatActivity() {
|
||||
|
||||
private val preferences: PreferencesHelper = Injekt.get()
|
||||
private val incognitoMode = preferences.incognitoMode().get()
|
||||
private val db: AnimeDatabaseHelper = Injekt.get()
|
||||
private lateinit var exoPlayer: SimpleExoPlayer
|
||||
private lateinit var dataSourceFactory: DataSource.Factory
|
||||
private lateinit var playerView: DoubleTapPlayerView
|
||||
|
@ -134,6 +144,8 @@ class WatcherActivity : AppCompatActivity() {
|
|||
playbackPosition = exoPlayer.currentPosition
|
||||
currentWindow = exoPlayer.currentWindowIndex
|
||||
val episode = intent.getSerializableExtra("episode") as Episode
|
||||
val anime = intent.getSerializableExtra("anime_anime") as Anime
|
||||
saveEpisodeHistory(EpisodeItem(episode, anime))
|
||||
val returnIntent = intent
|
||||
returnIntent.putExtra("seconds_result", playbackPosition)
|
||||
returnIntent.putExtra("total_seconds_result", exoPlayer.duration)
|
||||
|
@ -215,10 +227,21 @@ class WatcherActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun saveEpisodeHistory(episode: EpisodeItem) {
|
||||
if (!incognitoMode) {
|
||||
val history = AnimeHistory.create(episode.episode).apply { last_seen = Date().time }
|
||||
db.updateAnimeHistoryLastSeen(history).asRxCompletable()
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newIntent(context: Context, anime: Anime, episode: Episode, episodeList: List<EpisodeItem>, url: String): Intent {
|
||||
return Intent(context, WatcherActivity::class.java).apply {
|
||||
putExtra("anime", anime.id)
|
||||
putExtra("anime_anime", anime)
|
||||
putExtra("episode", episode)
|
||||
putExtra("second", episode.last_second_seen)
|
||||
putExtra("uri", url)
|
||||
|
|
32
app/src/main/res/layout/anime_history_controller.xml
Normal file
32
app/src/main/res/layout/anime_history_controller.xml
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="@dimen/action_toolbar_list_padding"
|
||||
tools:listitem="@layout/history_item" />
|
||||
|
||||
<eu.kanade.tachiyomi.widget.MaterialFastScroll
|
||||
android:id="@+id/fast_scroller"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
app:fastScrollerBubbleEnabled="false"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<eu.kanade.tachiyomi.widget.EmptyView
|
||||
android:id="@+id/empty_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
81
app/src/main/res/layout/anime_history_item.xml
Normal file
81
app/src/main/res/layout/anime_history_item.xml
Normal file
|
@ -0,0 +1,81 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/cover"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/anime_description_cover"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="h,3:2"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@mipmap/ic_launcher" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/remove"
|
||||
app:layout_constraintStart_toEndOf="@+id/cover"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/anime_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:textAppearance="@style/TextAppearance.Medium"
|
||||
tools:text="Title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/anime_subtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
tools:text="Subtitle" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/remove"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/action_remove"
|
||||
android:padding="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/resume"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_delete_24dp"
|
||||
app:tint="?android:attr/textColorPrimary" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/resume"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/action_resume"
|
||||
android:padding="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_play_arrow_24dp"
|
||||
app:tint="?android:attr/textColorPrimary" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -57,7 +57,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/action_resume"
|
||||
android:contentDescription="@string/action_remove"
|
||||
android:padding="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/resume"
|
||||
|
|
18
app/src/main/res/menu/anime_history.xml
Normal file
18
app/src/main/res/menu/anime_history.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_search"
|
||||
android:icon="@drawable/ic_search_24dp"
|
||||
android:title="@string/action_search"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
app:iconTint="?attr/colorOnPrimary"
|
||||
app:showAsAction="ifRoom|collapseActionView" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_clear_history"
|
||||
android:title="@string/pref_clear_history"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
|
@ -11,7 +11,7 @@
|
|||
app:showAsAction="ifRoom|collapseActionView" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_clear_history"
|
||||
android:id="@+id/action_clear_anime_history"
|
||||
android:title="@string/pref_clear_history"
|
||||
app:showAsAction="never" />
|
||||
|
||||
|
|
|
@ -810,4 +810,8 @@
|
|||
<string name="spen_previous_page">Previous page</string>
|
||||
<string name="spen_next_page">Next page</string>
|
||||
<string name="watcher_controls_skip_text">+85 s</string>
|
||||
<string name="no_next_episode">Next Episode not found!</string>
|
||||
<string name="anime_description_cover">Cover of Anime</string>
|
||||
<string name="label_history">Manga</string>
|
||||
<string name="label_animehistory">Anime</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue