Merge remote-tracking branch 'upstream/master'

This commit is contained in:
jmir1 2022-07-02 13:41:15 +02:00
commit 20e6431fe0
23 changed files with 340 additions and 269 deletions

View file

@ -1,6 +1,7 @@
package eu.kanade.data.anime
import eu.kanade.domain.anime.model.Anime
import eu.kanade.domain.episode.model.Episode
val animeMapper: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long) -> Anime =
{ id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, _, initialized, viewer, episodeFlags, coverLastModified, dateAdded ->
@ -24,3 +25,40 @@ val animeMapper: (Long, Long, String, String?, String?, String?, List<String>?,
initialized = initialized,
)
}
val animeEpisodeMapper: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, Long, Long, String, String, String?, Boolean, Boolean, Long, Long, Float, Long, Long, Long) -> Pair<Anime, Episode> =
{ _id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, next_update, initialized, viewerFlags, episodeFlags, coverLastModified, dateAdded, episodeId, animeId, chapterUrl, name, scanlator, seen, bookmark, lastSecondSeen, totalSeconds, episodeNumber, sourceOrder, dateFetch, dateUpload ->
Anime(
id = _id,
source = source,
favorite = favorite,
lastUpdate = lastUpdate ?: 0,
dateAdded = dateAdded,
viewerFlags = viewerFlags,
episodeFlags = episodeFlags,
coverLastModified = coverLastModified,
url = url,
title = title,
artist = artist,
author = author,
description = description,
genre = genre,
status = status,
thumbnailUrl = thumbnailUrl,
initialized = initialized,
) to Episode(
id = episodeId,
animeId = animeId,
seen = seen,
bookmark = bookmark,
lastSecondSeen = lastSecondSeen,
totalSeconds = totalSeconds,
dateFetch = dateFetch,
sourceOrder = sourceOrder,
url = chapterUrl,
name = name,
dateUpload = dateUpload,
episodeNumber = episodeNumber,
scanlator = scanlator,
)
}

View file

@ -1,5 +1,6 @@
package eu.kanade.data.manga
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.manga.model.Manga
val mangaMapper: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long) -> Manga =
@ -24,3 +25,39 @@ val mangaMapper: (Long, Long, String, String?, String?, String?, List<String>?,
initialized = initialized,
)
}
val mangaChapterMapper: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, Long, Long, String, String, String?, Boolean, Boolean, Long, Float, Long, Long, Long) -> Pair<Manga, Chapter> =
{ _id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, next_update, initialized, viewerFlags, chapterFlags, coverLastModified, dateAdded, chapterId, mangaId, chapterUrl, name, scanlator, read, bookmark, lastPageRead, chapterNumber, sourceOrder, dateFetch, dateUpload ->
Manga(
id = _id,
source = source,
favorite = favorite,
lastUpdate = lastUpdate ?: 0,
dateAdded = dateAdded,
viewerFlags = viewerFlags,
chapterFlags = chapterFlags,
coverLastModified = coverLastModified,
url = url,
title = title,
artist = artist,
author = author,
description = description,
genre = genre,
status = status,
thumbnailUrl = thumbnailUrl,
initialized = initialized,
) to Chapter(
id = chapterId,
mangaId = mangaId,
read = read,
bookmark = bookmark,
lastPageRead = lastPageRead,
dateFetch = dateFetch,
sourceOrder = sourceOrder,
url = chapterUrl,
name = name,
dateUpload = dateUpload,
chapterNumber = chapterNumber,
scanlator = scanlator,
)
}

View file

@ -361,6 +361,7 @@ private fun AnimeScreenSmallImpl(
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
VerticalFastScroller(
listState = episodeListState,
thumbAllowed = { scrollBehavior.state.offset == scrollBehavior.state.offsetLimit },
topContentPadding = withNavBarContentPadding.calculateTopPadding(),
endContentPadding = withNavBarContentPadding.calculateEndPadding(LocalLayoutDirection.current),
) {

View file

@ -47,6 +47,7 @@ import kotlin.math.roundToInt
fun VerticalFastScroller(
listState: LazyListState,
modifier: Modifier = Modifier,
thumbAllowed: () -> Boolean = { true },
thumbColor: Color = MaterialTheme.colorScheme.primary,
topContentPadding: Dp = Dp.Hairline,
endContentPadding: Dp = Dp.Hairline,
@ -106,8 +107,12 @@ fun VerticalFastScroller(
val isThumbVisible = alpha.value > 0f
LaunchedEffect(scrolled, alpha) {
scrolled.collectLatest {
alpha.snapTo(1f)
alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec)
if (thumbAllowed()) {
alpha.snapTo(1f)
alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec)
} else {
alpha.animateTo(0f, animationSpec = ImmediateFadeOutAnimationSpec)
}
}
}
@ -187,6 +192,9 @@ private val FadeOutAnimationSpec = tween<Float>(
durationMillis = ViewConfiguration.getScrollBarFadeDuration(),
delayMillis = 2000,
)
private val ImmediateFadeOutAnimationSpec = tween<Float>(
durationMillis = ViewConfiguration.getScrollBarFadeDuration(),
)
private val LazyListItemInfo.top: Int
get() = offset

View file

@ -353,6 +353,7 @@ private fun MangaScreenSmallImpl(
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
VerticalFastScroller(
listState = chapterListState,
thumbAllowed = { scrollBehavior.state.offset == scrollBehavior.state.offsetLimit },
topContentPadding = withNavBarContentPadding.calculateTopPadding(),
endContentPadding = withNavBarContentPadding.calculateEndPadding(LocalLayoutDirection.current),
) {

View file

@ -1,15 +1,11 @@
package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.queries.Query
import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaChapter
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
import java.util.Date
interface ChapterQueries : DbProvider {
@ -24,18 +20,6 @@ interface ChapterQueries : DbProvider {
)
.prepare()
fun getRecentChapters(date: Date) = db.get()
.listOfObjects(MangaChapter::class.java)
.withQuery(
RawQuery.builder()
.query(getRecentsQuery())
.args(date.time)
.observesTables(ChapterTable.TABLE)
.build(),
)
.withGetResolver(MangaChapterGetResolver.INSTANCE)
.prepare()
fun getChapter(id: Long) = db.get()
.`object`(Chapter::class.java)
.withQuery(

View file

@ -1,15 +1,11 @@
package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.queries.Query
import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.Anime
import eu.kanade.tachiyomi.data.database.models.AnimeEpisode
import eu.kanade.tachiyomi.data.database.models.Episode
import eu.kanade.tachiyomi.data.database.resolvers.AnimeEpisodeGetResolver
import eu.kanade.tachiyomi.data.database.resolvers.EpisodeProgressPutResolver
import eu.kanade.tachiyomi.data.database.tables.EpisodeTable
import java.util.Date
interface EpisodeQueries : DbProvider {
@ -24,18 +20,6 @@ interface EpisodeQueries : DbProvider {
)
.prepare()
fun getRecentEpisodes(date: Date) = db.get()
.listOfObjects(AnimeEpisode::class.java)
.withQuery(
RawQuery.builder()
.query(getRecentsQueryAnime())
.args(date.time)
.observesTables(EpisodeTable.TABLE)
.build(),
)
.withGetResolver(AnimeEpisodeGetResolver.INSTANCE)
.prepare()
fun getEpisode(id: Long) = db.get()
.`object`(Episode::class.java)
.withQuery(

View file

@ -71,32 +71,6 @@ val animelibQuery =
ON MC.${AnimeCategory.COL_ANIME_ID} = M.${Anime.COL_ID}
"""
/**
* Query to get the recent chapters of manga from the library up to a date.
*/
fun getRecentsQuery() =
"""
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
WHERE ${Manga.COL_FAVORITE} = 1
AND ${Chapter.COL_DATE_UPLOAD} > ?
AND ${Chapter.COL_DATE_FETCH} > ${Manga.COL_DATE_ADDED}
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
"""
/**
* Query to get the recent chapters of manga from the library up to a date.
*/
fun getRecentsQueryAnime() =
"""
SELECT ${Anime.TABLE}.${Anime.COL_URL} as animeUrl, * FROM ${Anime.TABLE} JOIN ${Episode.TABLE}
ON ${Anime.TABLE}.${Anime.COL_ID} = ${Episode.TABLE}.${Episode.COL_ANIME_ID}
WHERE ${Anime.COL_FAVORITE} = 1
AND ${Episode.COL_DATE_UPLOAD} > ?
AND ${Episode.COL_DATE_FETCH} > ${Anime.COL_DATE_ADDED}
ORDER BY ${Episode.COL_DATE_UPLOAD} DESC
"""
fun getLastReadMangaQuery() =
"""
SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max

View file

@ -2,15 +2,13 @@ package eu.kanade.tachiyomi.ui.anime.episode.base
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
import eu.kanade.tachiyomi.data.database.models.Episode
import eu.kanade.domain.episode.model.Episode
import eu.kanade.tachiyomi.data.download.model.AnimeDownload
abstract class BaseEpisodeItem<T : BaseEpisodeHolder, H : AbstractHeaderItem<*>>(
val episode: Episode,
header: H? = null,
) :
AbstractSectionableItem<T, H?>(header),
Episode by episode {
) : AbstractSectionableItem<T, H?>(header) {
private var _status: AnimeDownload.State = AnimeDownload.State.NOT_DOWNLOADED
@ -35,12 +33,14 @@ abstract class BaseEpisodeItem<T : BaseEpisodeHolder, H : AbstractHeaderItem<*>>
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other is BaseEpisodeItem<*, *>) {
return episode.id!! == other.episode.id!!
return episode.id == other.episode.id && episode.seen == other.episode.seen
}
return false
}
override fun hashCode(): Int {
return episode.id!!.hashCode()
var result = episode.id.hashCode()
result = 31 * result + episode.seen.hashCode()
return result
}
}

View file

@ -2,16 +2,14 @@ package eu.kanade.tachiyomi.ui.manga.chapter.base
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.model.Page
abstract class BaseChapterItem<T : BaseChapterHolder, H : AbstractHeaderItem<*>>(
val chapter: Chapter,
header: H? = null,
) :
AbstractSectionableItem<T, H?>(header),
Chapter by chapter {
) : AbstractSectionableItem<T, H?>(header) {
private var _status: Download.State = Download.State.NOT_DOWNLOADED
@ -36,12 +34,14 @@ abstract class BaseChapterItem<T : BaseChapterHolder, H : AbstractHeaderItem<*>>
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other is BaseChapterItem<*, *>) {
return chapter.id!! == other.chapter.id!!
return chapter.id == other.chapter.id && chapter.read == other.chapter.read
}
return false
}
override fun hashCode(): Int {
return chapter.id!!.hashCode()
var result = chapter.id.hashCode()
result = 31 * result + chapter.read.hashCode()
return result
}
}

View file

@ -3,11 +3,11 @@ package eu.kanade.tachiyomi.ui.reader
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ScaleXSpan
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.text.set
import eu.kanade.tachiyomi.widget.OutlineSpan
/**
@ -31,10 +31,10 @@ class PageIndicatorTextView(
// Also add a bit of spacing between each character, as the stroke overlaps them
val finalText = SpannableString(currText.asIterable().joinToString("\u00A0")).apply {
// Apply text outline
set(1, length - 1, spanOutline)
setSpan(spanOutline, 1, length - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
for (i in 1..lastIndex step 2) {
set(i, i + 1, ScaleXSpan(0.2f))
setSpan(ScaleXSpan(0.2f), i, i + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}

View file

@ -9,7 +9,6 @@ import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.LinearLayout
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.view.updatePadding
import com.google.android.material.progressindicator.CircularProgressIndicator
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
@ -56,7 +55,7 @@ class PagerTransitionHolder(
orientation = VERTICAL
gravity = Gravity.CENTER
val sidePadding = 64.dpToPx
updatePadding(left = sidePadding, right = sidePadding)
setPadding(sidePadding, 0, sidePadding, 0)
val transitionView = ReaderTransitionView(context)
addView(transitionView)

View file

@ -12,12 +12,11 @@ import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.domain.anime.model.toDbAnime
import eu.kanade.domain.episode.model.toDbEpisode
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
import eu.kanade.tachiyomi.data.animelib.AnimelibUpdateService
import eu.kanade.tachiyomi.data.database.models.toDomainAnime
import eu.kanade.tachiyomi.data.database.models.toDomainEpisode
import eu.kanade.tachiyomi.data.download.AnimeDownloadService
import eu.kanade.tachiyomi.data.download.model.AnimeDownload
import eu.kanade.tachiyomi.data.notification.Notifications
@ -40,12 +39,13 @@ import eu.kanade.tachiyomi.util.system.notificationManager
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
import eu.kanade.tachiyomi.widget.ActionModeWithToolbar
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import logcat.LogPriority
import reactivecircus.flowbinding.recyclerview.scrollStateChanges
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
/**
@ -123,6 +123,24 @@ class AnimeUpdatesController :
binding.swipeRefresh.isRefreshing = false
}
.launchIn(viewScope)
viewScope.launch {
presenter.updates.collectLatest { updatesItems ->
destroyActionModeIfNeeded()
if (adapter == null) {
adapter = AnimeUpdatesAdapter(this@AnimeUpdatesController, binding.recycler.context, updatesItems)
binding.recycler.adapter = adapter
adapter!!.fastScroller = binding.fastScroller
} else {
adapter?.updateDataSet(updatesItems)
}
binding.swipeRefresh.isRefreshing = false
binding.fastScroller.isVisible = true
binding.recycler.onAnimationsFinished {
(activity as? MainActivity)?.ready = true
}
}
}
}
override fun onDestroyView(view: View) {
@ -211,7 +229,7 @@ class AnimeUpdatesController :
if (useExternal) {
openEpisodeExternal(item)
} else {
val intent = PlayerActivity.newIntent(activity, item.anime, item.episode)
val intent = PlayerActivity.newIntent(activity, item.anime.id, item.episode.id)
if (hasAnimation) {
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
}
@ -222,23 +240,22 @@ class AnimeUpdatesController :
private fun openEpisodeExternal(item: AnimeUpdatesItem) {
val context = activity ?: return
val anime = item.anime
val domainAnime = item.anime.toDomainAnime() ?: return
val domainAnime = item.anime
val episode = item.episode
val domainEpisode = item.episode.toDomainEpisode() ?: return
val source = sourceManager.get(item.anime.source) ?: return
launchIO {
val video = try {
EpisodeLoader.getLink(episode, anime, source).awaitSingle()
EpisodeLoader.getLink(episode.toDbEpisode(), anime.toDbAnime(), source).awaitSingle()
} catch (e: Exception) {
launchUI { context.toast(e.message) }
return@launchIO
}
if (video != null) {
AnimeController.EXT_EPISODE = domainEpisode
AnimeController.EXT_EPISODE = episode
AnimeController.EXT_ANIME = domainAnime
val extIntent = ExternalIntents(domainAnime, source).getExternalIntent(
domainEpisode,
episode,
video,
context,
)
@ -278,26 +295,6 @@ class AnimeUpdatesController :
destroyActionModeIfNeeded()
}
/**
* Populate adapter with episodes
* @param episodes list of [Any]
*/
fun onNextRecentEpisodes(episodes: List<IFlexible<*>>) {
destroyActionModeIfNeeded()
if (adapter == null) {
adapter = AnimeUpdatesAdapter(this@AnimeUpdatesController, binding.recycler.context, episodes)
binding.recycler.adapter = adapter
adapter!!.fastScroller = binding.fastScroller
} else {
adapter?.updateDataSet(episodes)
}
binding.swipeRefresh.isRefreshing = false
binding.fastScroller.isVisible = true
binding.recycler.onAnimationsFinished {
(activity as? MainActivity)?.ready = true
}
}
override fun onUpdateEmptyView(size: Int) {
if (size > 0) {
binding.emptyView.hide()
@ -356,7 +353,7 @@ class AnimeUpdatesController :
}
private fun openAnime(episode: AnimeUpdatesItem) {
parentController!!.router.pushController(AnimeController(episode.anime.id!!))
parentController!!.router.pushController(AnimeController(episode.anime.id))
}
/**
@ -395,8 +392,8 @@ class AnimeUpdatesController :
}
override fun startDownloadNow(position: Int) {
val episode = adapter?.getItem(position) as? AnimeUpdatesItem ?: return
presenter.startDownloadingNow(episode)
val item = adapter?.getItem(position) as? AnimeUpdatesItem ?: return
presenter.startDownloadingNow(item.episode)
}
private fun bookmarkEpisodes(episodes: List<AnimeUpdatesItem>, bookmarked: Boolean) {
@ -441,8 +438,8 @@ class AnimeUpdatesController :
if (episodes.isEmpty()) return
toolbar.findToolbarItem(R.id.action_download)?.isVisible = episodes.any { !it.isDownloaded }
toolbar.findToolbarItem(R.id.action_delete)?.isVisible = episodes.any { it.isDownloaded }
toolbar.findToolbarItem(R.id.action_bookmark)?.isVisible = episodes.any { !it.bookmark }
toolbar.findToolbarItem(R.id.action_remove_bookmark)?.isVisible = episodes.all { it.bookmark }
toolbar.findToolbarItem(R.id.action_bookmark)?.isVisible = episodes.any { !it.episode.bookmark }
toolbar.findToolbarItem(R.id.action_remove_bookmark)?.isVisible = episodes.all { it.episode.bookmark }
toolbar.findToolbarItem(R.id.action_mark_as_seen)?.isVisible = episodes.any { !it.episode.seen }
toolbar.findToolbarItem(R.id.action_mark_as_unseen)?.isVisible = episodes.all { it.episode.seen }
toolbar.findToolbarItem(R.id.action_play_externally)?.isVisible = !preferences.alwaysUseExternalPlayer() && episodes.size == 1

View file

@ -43,12 +43,12 @@ class AnimeUpdatesHolder(private val view: View, private val adapter: AnimeUpdat
} else {
binding.mangaTitle.setTextColor(adapter.unseenColor)
binding.chapterTitle.setTextColor(
if (item.bookmark) adapter.bookmarkedColor else adapter.unseenColorSecondary,
if (item.episode.bookmark) adapter.bookmarkedColor else adapter.unseenColorSecondary,
)
}
// Set bookmark status
binding.bookmarkIcon.isVisible = item.bookmark
binding.bookmarkIcon.isVisible = item.episode.bookmark
// Set episode status
binding.download.isVisible = item.anime.source != LocalAnimeSource.ID

View file

@ -4,9 +4,9 @@ import android.view.View
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.domain.anime.model.Anime
import eu.kanade.domain.episode.model.Episode
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Anime
import eu.kanade.tachiyomi.data.database.models.Episode
import eu.kanade.tachiyomi.ui.anime.episode.base.BaseEpisodeItem
import eu.kanade.tachiyomi.ui.recent.DateSectionItem

View file

@ -1,17 +1,28 @@
package eu.kanade.tachiyomi.ui.recent.animeupdates
import android.os.Bundle
import eu.kanade.data.AnimeDatabaseHandler
import eu.kanade.data.anime.animeEpisodeMapper
import eu.kanade.domain.anime.model.Anime
import eu.kanade.domain.anime.model.toDbAnime
import eu.kanade.domain.episode.interactor.UpdateEpisode
import eu.kanade.domain.episode.model.Episode
import eu.kanade.domain.episode.model.EpisodeUpdate
import eu.kanade.domain.episode.model.toDbEpisode
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
import eu.kanade.tachiyomi.data.database.AnimeDatabaseHelper
import eu.kanade.tachiyomi.data.database.models.AnimeEpisode
import eu.kanade.tachiyomi.data.database.models.Episode
import eu.kanade.tachiyomi.data.download.AnimeDownloadManager
import eu.kanade.tachiyomi.data.download.model.AnimeDownload
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.toDateKey
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import logcat.LogPriority
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
@ -25,24 +36,22 @@ import java.util.TreeMap
class AnimeUpdatesPresenter : BasePresenter<AnimeUpdatesController>() {
val preferences: PreferencesHelper by injectLazy()
private val db: AnimeDatabaseHelper by injectLazy()
private val downloadManager: AnimeDownloadManager by injectLazy()
private val sourceManager: AnimeSourceManager by injectLazy()
private val handler: AnimeDatabaseHandler by injectLazy()
private val updateEpisode: UpdateEpisode by injectLazy()
private val relativeTime: Int = preferences.relativeTime().get()
private val dateFormat: DateFormat = preferences.dateFormat()
/**
* List containing episode and anime information
*/
private var episodes: List<AnimeUpdatesItem> = emptyList()
private val _updates: MutableStateFlow<List<AnimeUpdatesItem>> = MutableStateFlow(listOf())
val updates: StateFlow<List<AnimeUpdatesItem>> = _updates.asStateFlow()
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
getUpdatesObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribeLatestCache(AnimeUpdatesController::onNextRecentEpisodes)
downloadManager.queue.getStatusObservable()
.observeOn(Schedulers.io())
@ -80,43 +89,49 @@ class AnimeUpdatesPresenter : BasePresenter<AnimeUpdatesController>() {
*
* @return observable containing recent episodes and date
*/
private fun getUpdatesObservable(): Observable<List<AnimeUpdatesItem>> {
private fun getUpdatesObservable() {
// Set date limit for recent episodes
val cal = Calendar.getInstance().apply {
time = Date()
add(Calendar.MONTH, -3)
}
return db.getRecentEpisodes(cal.time).asRxObservable()
// Convert to a list of recent episodes.
.map { animeEpisodes ->
val map = TreeMap<Date, MutableList<AnimeEpisode>> { d1, d2 -> d2.compareTo(d1) }
val byDay = animeEpisodes
.groupByTo(map) { it.episode.date_fetch.toDateKey() }
byDay.flatMap { entry ->
val dateItem = DateSectionItem(entry.key, relativeTime, dateFormat)
entry.value
.sortedWith(compareBy({ it.episode.date_fetch }, { it.episode.episode_number })).asReversed()
.map { AnimeUpdatesItem(it.episode, it.anime, dateItem) }
}
presenterScope.launchIO {
val cal = Calendar.getInstance().apply {
time = Date()
add(Calendar.MONTH, -3)
}
.doOnNext { list ->
list.forEach { item ->
// Find an active download for this episode.
val download = downloadManager.queue.find { it.episode.id == item.episode.id }
// If there's an active download, assign it, otherwise ask the manager if
// the episode is downloaded and assign it to the status.
if (download != null) {
item.download = download
handler
.subscribeToList {
animesQueries.getRecentlyUpdated(after = cal.timeInMillis, animeEpisodeMapper)
}
.map { animeEpisode ->
val map = TreeMap<Date, MutableList<Pair<Anime, Episode>>> { d1, d2 -> d2.compareTo(d1) }
val byDate = animeEpisode.groupByTo(map) { it.second.dateFetch.toDateKey() }
byDate.flatMap { entry ->
val dateItem = DateSectionItem(entry.key, relativeTime, dateFormat)
entry.value
.sortedWith(compareBy({ it.second.dateFetch }, { it.second.episodeNumber })).asReversed()
.map { AnimeUpdatesItem(it.second, it.first, dateItem) }
}
}
setDownloadedEpisodes(list)
episodes = list
.collectLatest { list ->
// TODO: remove this workaround when updates aren't cluttered anymore
_updates.value = list
list.forEach { item ->
// Find an active download for this episode.
val download = downloadManager.queue.find { it.episode.id == item.episode.id }
// Set unseen episode count for bottom bar badge
preferences.unseenUpdatesCount().set(list.count { !it.seen })
}
// If there's an active download, assign it, otherwise ask the manager if
// the chapter is downloaded and assign it to the status.
if (download != null) {
item.download = download
}
}
setDownloadedEpisodes(list)
_updates.value = list
// Set unseen episode count for bottom bar badge
preferences.unseenUpdatesCount().set(list.count { !it.episode.seen })
}
}
}
/**
@ -143,6 +158,7 @@ class AnimeUpdatesPresenter : BasePresenter<AnimeUpdatesController>() {
private fun onDownloadStatusChange(download: AnimeDownload) {
// Assign the download to the model object.
if (download.status == AnimeDownload.State.QUEUE) {
val episodes = (view?.adapter?.currentItems ?: emptyList()).filterIsInstance<AnimeUpdatesItem>()
val episode = episodes.find { it.episode.id == download.episode.id }
if (episode != null && episode.download == null) {
episode.download = download
@ -161,17 +177,16 @@ class AnimeUpdatesPresenter : BasePresenter<AnimeUpdatesController>() {
* @param seen seen/unseen status
*/
fun markEpisodeRead(items: List<AnimeUpdatesItem>, seen: Boolean) {
val episodes = items.map { it.episode }
episodes.forEach {
it.seen = seen
if (!seen) {
it.last_second_seen = 0
presenterScope.launchIO {
val toUpdate = items.map {
EpisodeUpdate(
seen = seen,
lastSecondSeen = if (!seen) 0 else null,
id = it.episode.id,
)
}
updateEpisode.awaitAll(toUpdate)
}
Observable.fromCallable { db.updateEpisodesProgress(episodes).executeAsBlocking() }
.subscribeOn(Schedulers.io())
.subscribe()
}
/**
@ -198,14 +213,15 @@ class AnimeUpdatesPresenter : BasePresenter<AnimeUpdatesController>() {
* @param bookmarked bookmark status
*/
fun bookmarkEpisodes(items: List<AnimeUpdatesItem>, bookmarked: Boolean) {
val episodes = items.map { it.episode }
episodes.forEach {
it.bookmark = bookmarked
presenterScope.launchIO {
val toUpdate = items.map {
EpisodeUpdate(
bookmark = bookmarked,
id = it.episode.id,
)
}
updateEpisode.awaitAll(toUpdate)
}
Observable.fromCallable { db.updateEpisodesProgress(episodes).executeAsBlocking() }
.subscribeOn(Schedulers.io())
.subscribe()
}
/**
@ -213,7 +229,7 @@ class AnimeUpdatesPresenter : BasePresenter<AnimeUpdatesController>() {
* @param items list of recent episodes seleted.
*/
fun downloadEpisodes(items: List<AnimeUpdatesItem>) {
items.forEach { downloadManager.downloadEpisodes(it.anime, listOf(it.episode)) }
items.forEach { downloadManager.downloadEpisodes(it.anime.toDbAnime(), listOf(it.episode.toDbEpisode())) }
}
/**
@ -221,7 +237,7 @@ class AnimeUpdatesPresenter : BasePresenter<AnimeUpdatesController>() {
* @param items list of recent episodes seleted.
*/
fun downloadEpisodesExternally(items: List<AnimeUpdatesItem>) {
items.forEach { downloadManager.downloadEpisodesAlt(it.anime, listOf(it.episode)) }
items.forEach { downloadManager.downloadEpisodesAlt(it.anime.toDbAnime(), listOf(it.episode.toDbEpisode())) }
}
/**
@ -232,9 +248,9 @@ class AnimeUpdatesPresenter : BasePresenter<AnimeUpdatesController>() {
private fun deleteEpisodesInternal(episodeItems: List<AnimeUpdatesItem>) {
val itemsByAnime = episodeItems.groupBy { it.anime.id }
for ((_, items) in itemsByAnime) {
val anime = items.first().anime
val anime = items.first().anime.toDbAnime()
val source = sourceManager.get(anime.source) ?: continue
val episodes = items.map { it.episode }
val episodes = items.map { it.episode.toDbEpisode() }
downloadManager.deleteEpisodes(episodes, anime, source)
items.forEach {

View file

@ -11,7 +11,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.download.model.Download
@ -30,8 +29,10 @@ import eu.kanade.tachiyomi.util.system.notificationManager
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
import eu.kanade.tachiyomi.widget.ActionModeWithToolbar
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import logcat.LogPriority
import reactivecircus.flowbinding.recyclerview.scrollStateChanges
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
@ -107,6 +108,24 @@ class UpdatesController :
binding.swipeRefresh.isRefreshing = false
}
.launchIn(viewScope)
viewScope.launch {
presenter.updates.collectLatest { updatesItems ->
destroyActionModeIfNeeded()
if (adapter == null) {
adapter = UpdatesAdapter(this@UpdatesController, binding.recycler.context, updatesItems)
binding.recycler.adapter = adapter
adapter!!.fastScroller = binding.fastScroller
} else {
adapter?.updateDataSet(updatesItems)
}
binding.swipeRefresh.isRefreshing = false
binding.fastScroller.isVisible = true
binding.recycler.onAnimationsFinished {
(activity as? MainActivity)?.ready = true
}
}
}
}
override fun onDestroyView(view: View) {
@ -191,7 +210,7 @@ class UpdatesController :
*/
private fun openChapter(item: UpdatesItem) {
val activity = activity ?: return
val intent = ReaderActivity.newIntent(activity, item.manga, item.chapter)
val intent = ReaderActivity.newIntent(activity, item.manga.id, item.chapter.id)
startActivity(intent)
}
@ -204,26 +223,6 @@ class UpdatesController :
destroyActionModeIfNeeded()
}
/**
* Populate adapter with chapters
* @param chapters list of [Any]
*/
fun onNextRecentChapters(chapters: List<IFlexible<*>>) {
destroyActionModeIfNeeded()
if (adapter == null) {
adapter = UpdatesAdapter(this@UpdatesController, binding.recycler.context, chapters)
binding.recycler.adapter = adapter
adapter!!.fastScroller = binding.fastScroller
} else {
adapter?.updateDataSet(chapters)
}
binding.swipeRefresh.isRefreshing = false
binding.fastScroller.isVisible = true
binding.recycler.onAnimationsFinished {
(activity as? MainActivity)?.ready = true
}
}
override fun onUpdateEmptyView(size: Int) {
if (size > 0) {
binding.emptyView.hide()
@ -317,8 +316,8 @@ class UpdatesController :
}
override fun startDownloadNow(position: Int) {
val chapter = adapter?.getItem(position) as? UpdatesItem ?: return
presenter.startDownloadingNow(chapter)
val item = adapter?.getItem(position) as? UpdatesItem ?: return
presenter.startDownloadingNow(item.chapter)
}
private fun bookmarkChapters(chapters: List<UpdatesItem>, bookmarked: Boolean) {
@ -357,8 +356,8 @@ class UpdatesController :
if (chapters.isEmpty()) return
toolbar.findToolbarItem(R.id.action_download)?.isVisible = chapters.any { !it.isDownloaded }
toolbar.findToolbarItem(R.id.action_delete)?.isVisible = chapters.any { it.isDownloaded }
toolbar.findToolbarItem(R.id.action_bookmark)?.isVisible = chapters.any { !it.bookmark }
toolbar.findToolbarItem(R.id.action_remove_bookmark)?.isVisible = chapters.all { it.bookmark }
toolbar.findToolbarItem(R.id.action_bookmark)?.isVisible = chapters.any { !it.chapter.bookmark }
toolbar.findToolbarItem(R.id.action_remove_bookmark)?.isVisible = chapters.all { it.chapter.bookmark }
toolbar.findToolbarItem(R.id.action_mark_as_read)?.isVisible = chapters.any { !it.chapter.read }
toolbar.findToolbarItem(R.id.action_mark_as_unread)?.isVisible = chapters.all { it.chapter.read }
}

View file

@ -44,12 +44,12 @@ class UpdatesHolder(private val view: View, private val adapter: UpdatesAdapter)
} else {
binding.mangaTitle.setTextColor(adapter.unreadColor)
binding.chapterTitle.setTextColor(
if (item.bookmark) adapter.bookmarkedColor else adapter.unreadColorSecondary,
if (item.chapter.bookmark) adapter.bookmarkedColor else adapter.unreadColorSecondary,
)
}
// Set bookmark status
binding.bookmarkIcon.isVisible = item.bookmark
binding.bookmarkIcon.isVisible = item.chapter.bookmark
// Set chapter status
binding.download.isVisible = item.manga.source != LocalSource.ID

View file

@ -4,9 +4,9 @@ import android.view.View
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChapterItem
import eu.kanade.tachiyomi.ui.recent.DateSectionItem

View file

@ -1,17 +1,28 @@
package eu.kanade.tachiyomi.ui.recent.updates
import android.os.Bundle
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.MangaChapter
import eu.kanade.data.DatabaseHandler
import eu.kanade.data.manga.mangaChapterMapper
import eu.kanade.domain.chapter.interactor.UpdateChapter
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.chapter.model.ChapterUpdate
import eu.kanade.domain.chapter.model.toDbChapter
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.manga.model.toDbManga
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.SourceManager
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.toDateKey
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import logcat.LogPriority
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
@ -25,24 +36,22 @@ import java.util.TreeMap
class UpdatesPresenter : BasePresenter<UpdatesController>() {
val preferences: PreferencesHelper by injectLazy()
private val db: DatabaseHelper by injectLazy()
private val downloadManager: DownloadManager by injectLazy()
private val sourceManager: SourceManager by injectLazy()
private val handler: DatabaseHandler by injectLazy()
private val updateChapter: UpdateChapter by injectLazy()
private val relativeTime: Int = preferences.relativeTime().get()
private val dateFormat: DateFormat = preferences.dateFormat()
/**
* List containing chapter and manga information
*/
private var chapters: List<UpdatesItem> = emptyList()
private val _updates: MutableStateFlow<List<UpdatesItem>> = MutableStateFlow(listOf())
val updates: StateFlow<List<UpdatesItem>> = _updates.asStateFlow()
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
getUpdatesObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribeLatestCache(UpdatesController::onNextRecentChapters)
downloadManager.queue.getStatusObservable()
.observeOn(Schedulers.io())
@ -72,43 +81,47 @@ class UpdatesPresenter : BasePresenter<UpdatesController>() {
*
* @return observable containing recent chapters and date
*/
private fun getUpdatesObservable(): Observable<List<UpdatesItem>> {
private fun getUpdatesObservable() {
// Set date limit for recent chapters
val cal = Calendar.getInstance().apply {
time = Date()
add(Calendar.MONTH, -3)
}
return db.getRecentChapters(cal.time).asRxObservable()
// Convert to a list of recent chapters.
.map { mangaChapters ->
val map = TreeMap<Date, MutableList<MangaChapter>> { d1, d2 -> d2.compareTo(d1) }
val byDay = mangaChapters
.groupByTo(map) { it.chapter.date_fetch.toDateKey() }
byDay.flatMap { entry ->
val dateItem = DateSectionItem(entry.key, relativeTime, dateFormat)
entry.value
.sortedWith(compareBy({ it.chapter.date_fetch }, { it.chapter.chapter_number })).asReversed()
.map { UpdatesItem(it.chapter, it.manga, dateItem) }
}
presenterScope.launchIO {
val cal = Calendar.getInstance().apply {
time = Date()
add(Calendar.MONTH, -3)
}
.doOnNext { list ->
list.forEach { item ->
// Find an active download for this chapter.
val download = downloadManager.queue.find { it.chapter.id == item.chapter.id }
// If there's an active download, assign it, otherwise ask the manager if
// the chapter is downloaded and assign it to the status.
if (download != null) {
item.download = download
handler
.subscribeToList {
mangasQueries.getRecentlyUpdated(after = cal.timeInMillis, mangaChapterMapper)
}
.map { mangaChapter ->
val map = TreeMap<Date, MutableList<Pair<Manga, Chapter>>> { d1, d2 -> d2.compareTo(d1) }
val byDate = mangaChapter.groupByTo(map) { it.second.dateFetch.toDateKey() }
byDate.flatMap { entry ->
val dateItem = DateSectionItem(entry.key, relativeTime, dateFormat)
entry.value
.sortedWith(compareBy({ it.second.dateFetch }, { it.second.chapterNumber })).asReversed()
.map { UpdatesItem(it.second, it.first, dateItem) }
}
}
setDownloadedChapters(list)
chapters = list
.collectLatest { list ->
list.forEach { item ->
// Find an active download for this chapter.
val download = downloadManager.queue.find { it.chapter.id == item.chapter.id }
// Set unread chapter count for bottom bar badge
preferences.unreadUpdatesCount().set(list.count { !it.read })
}
// If there's an active download, assign it, otherwise ask the manager if
// the chapter is downloaded and assign it to the status.
if (download != null) {
item.download = download
}
}
setDownloadedChapters(list)
_updates.value = list
// Set unread chapter count for bottom bar badge
preferences.unreadUpdatesCount().set(list.count { !it.chapter.read })
}
}
}
/**
@ -135,6 +148,7 @@ class UpdatesPresenter : BasePresenter<UpdatesController>() {
private fun onDownloadStatusChange(download: Download) {
// Assign the download to the model object.
if (download.status == Download.State.QUEUE) {
val chapters = (view?.adapter?.currentItems ?: emptyList()).filterIsInstance<UpdatesItem>()
val chapter = chapters.find { it.chapter.id == download.chapter.id }
if (chapter != null && chapter.download == null) {
chapter.download = download
@ -153,17 +167,16 @@ class UpdatesPresenter : BasePresenter<UpdatesController>() {
* @param read read status
*/
fun markChapterRead(items: List<UpdatesItem>, read: Boolean) {
val chapters = items.map { it.chapter }
chapters.forEach {
it.read = read
if (!read) {
it.last_page_read = 0
presenterScope.launchIO {
val toUpdate = items.map {
ChapterUpdate(
read = read,
lastPageRead = if (!read) 0 else null,
id = it.chapter.id,
)
}
updateChapter.awaitAll(toUpdate)
}
Observable.fromCallable { db.updateChaptersProgress(chapters).executeAsBlocking() }
.subscribeOn(Schedulers.io())
.subscribe()
}
/**
@ -190,14 +203,15 @@ class UpdatesPresenter : BasePresenter<UpdatesController>() {
* @param bookmarked bookmark status
*/
fun bookmarkChapters(items: List<UpdatesItem>, bookmarked: Boolean) {
val chapters = items.map { it.chapter }
chapters.forEach {
it.bookmark = bookmarked
presenterScope.launchIO {
val toUpdate = items.map {
ChapterUpdate(
bookmark = bookmarked,
id = it.chapter.id,
)
}
updateChapter.awaitAll(toUpdate)
}
Observable.fromCallable { db.updateChaptersProgress(chapters).executeAsBlocking() }
.subscribeOn(Schedulers.io())
.subscribe()
}
/**
@ -205,7 +219,7 @@ class UpdatesPresenter : BasePresenter<UpdatesController>() {
* @param items list of recent chapters seleted.
*/
fun downloadChapters(items: List<UpdatesItem>) {
items.forEach { downloadManager.downloadChapters(it.manga, listOf(it.chapter)) }
items.forEach { downloadManager.downloadChapters(it.manga.toDbManga(), listOf(it.chapter.toDbChapter())) }
}
/**
@ -216,9 +230,9 @@ class UpdatesPresenter : BasePresenter<UpdatesController>() {
private fun deleteChaptersInternal(chapterItems: List<UpdatesItem>) {
val itemsByManga = chapterItems.groupBy { it.manga.id }
for ((_, items) in itemsByManga) {
val manga = items.first().manga
val manga = items.first().manga.toDbManga()
val source = sourceManager.get(manga.source) ?: continue
val chapters = items.map { it.chapter }
val chapters = items.map { it.chapter.toDbChapter() }
downloadManager.deleteChapters(chapters, manga, source)
items.forEach {

View file

@ -12,7 +12,6 @@ import android.util.AttributeSet
import android.view.animation.LinearInterpolator
import android.widget.TextView
import androidx.annotation.FloatRange
import androidx.core.graphics.drawable.updateBounds
import androidx.core.graphics.withTranslation
import androidx.lifecycle.coroutineScope
import androidx.lifecycle.findViewTreeLifecycleOwner
@ -93,7 +92,7 @@ class TachiyomiAppBarLayout @JvmOverloads constructor(
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
super.onLayout(changed, l, t, r, b)
statusBarForeground?.updateBounds(right = width, bottom = paddingTop)
statusBarForeground?.setBounds(0, 0, width, paddingTop)
}
override fun onOffsetChanged(offset: Int) {

View file

@ -76,6 +76,16 @@ FROM mangas
WHERE favorite = 0
GROUP BY source;
getRecentlyUpdated:
SELECT *
FROM mangas M
JOIN chapters C
ON M._id = C.manga_id
WHERE M.favorite = 1
AND C.date_upload > :after
AND C.date_fetch > M.date_added
ORDER BY C.date_upload DESC;
deleteMangasNotInLibraryBySourceIds:
DELETE FROM mangas
WHERE favorite = 0 AND source IN :sourceIds;

View file

@ -76,6 +76,16 @@ FROM animes
WHERE favorite = 0
GROUP BY source;
getRecentlyUpdated:
SELECT *
FROM animes A
JOIN episodes EP
ON A._id = EP.anime_id
WHERE A.favorite = 1
AND EP.date_upload > :after
AND EP.date_fetch > A.date_added
ORDER BY EP.date_upload DESC;
deleteAnimesNotInLibraryBySourceIds:
DELETE FROM animes
WHERE favorite = 0 AND source IN :sourceIds;