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:
jmir1 2021-05-13 01:22:32 +02:00
parent 1c78880221
commit 6f4d793813
16 changed files with 895 additions and 6 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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>

View 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>

View file

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

View 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>

View file

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

View file

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