Merge remote-tracking branch 'upstream/master'

This commit is contained in:
jmir1 2022-06-20 17:31:01 +02:00
commit 640cadb70b
130 changed files with 2452 additions and 1409 deletions

View file

@ -21,7 +21,7 @@ jobs:
uses: gradle/wrapper-validation-action@v1
- name: Dependency Review
uses: actions/dependency-review-action@v1
uses: actions/dependency-review-action@v2
- name: Set up JDK 11
uses: actions/setup-java@v3

View file

@ -2,9 +2,7 @@ package eu.kanade.data
import androidx.paging.PagingSource
import com.squareup.sqldelight.Query
import com.squareup.sqldelight.Transacter
import com.squareup.sqldelight.android.AndroidSqliteDriver
import com.squareup.sqldelight.android.paging3.QueryPagingSource
import com.squareup.sqldelight.db.SqlDriver
import com.squareup.sqldelight.runtime.coroutines.asFlow
import com.squareup.sqldelight.runtime.coroutines.mapToList
import com.squareup.sqldelight.runtime.coroutines.mapToOne
@ -17,7 +15,7 @@ import kotlinx.coroutines.withContext
class AndroidAnimeDatabaseHandler(
val db: AnimeDatabase,
private val driver: AndroidSqliteDriver,
private val driver: SqlDriver,
val queryDispatcher: CoroutineDispatcher = Dispatchers.IO,
val transactionDispatcher: CoroutineDispatcher = queryDispatcher,
) : AnimeDatabaseHandler {
@ -63,13 +61,11 @@ class AndroidAnimeDatabaseHandler(
override fun <T : Any> subscribeToPagingSource(
countQuery: AnimeDatabase.() -> Query<Long>,
transacter: AnimeDatabase.() -> Transacter,
queryProvider: AnimeDatabase.(Long, Long) -> Query<T>,
): PagingSource<Long, T> {
return QueryPagingSource(
countQuery = countQuery(db),
transacter = transacter(db),
dispatcher = queryDispatcher,
return AnimeQueryPagingSource(
handler = this,
countQuery = countQuery,
queryProvider = { limit, offset ->
queryProvider.invoke(db, limit, offset)
},

View file

@ -2,8 +2,6 @@ package eu.kanade.data
import androidx.paging.PagingSource
import com.squareup.sqldelight.Query
import com.squareup.sqldelight.Transacter
import com.squareup.sqldelight.android.paging3.QueryPagingSource
import com.squareup.sqldelight.db.SqlDriver
import com.squareup.sqldelight.runtime.coroutines.asFlow
import com.squareup.sqldelight.runtime.coroutines.mapToList
@ -63,13 +61,11 @@ class AndroidDatabaseHandler(
override fun <T : Any> subscribeToPagingSource(
countQuery: Database.() -> Query<Long>,
transacter: Database.() -> Transacter,
queryProvider: Database.(Long, Long) -> Query<T>,
): PagingSource<Long, T> {
return QueryPagingSource(
countQuery = countQuery(db),
transacter = transacter(db),
dispatcher = queryDispatcher,
handler = this,
countQuery = countQuery,
queryProvider = { limit, offset ->
queryProvider.invoke(db, limit, offset)
},

View file

@ -2,7 +2,6 @@ package eu.kanade.data
import androidx.paging.PagingSource
import com.squareup.sqldelight.Query
import com.squareup.sqldelight.Transacter
import eu.kanade.tachiyomi.mi.AnimeDatabase
import kotlinx.coroutines.flow.Flow
@ -33,7 +32,6 @@ interface AnimeDatabaseHandler {
fun <T : Any> subscribeToPagingSource(
countQuery: AnimeDatabase.() -> Query<Long>,
transacter: AnimeDatabase.() -> Transacter,
queryProvider: AnimeDatabase.(Long, Long) -> Query<T>,
): PagingSource<Long, T>
}

View file

@ -0,0 +1,72 @@
package eu.kanade.data
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.squareup.sqldelight.Query
import eu.kanade.tachiyomi.mi.AnimeDatabase
import kotlin.properties.Delegates
class AnimeQueryPagingSource<RowType : Any>(
val handler: AnimeDatabaseHandler,
val countQuery: AnimeDatabase.() -> Query<Long>,
val queryProvider: AnimeDatabase.(Long, Long) -> Query<RowType>,
) : PagingSource<Long, RowType>(), Query.Listener {
override val jumpingSupported: Boolean = true
private var currentQuery: Query<RowType>? by Delegates.observable(null) { _, old, new ->
old?.removeListener(this)
new?.addListener(this)
}
init {
registerInvalidatedCallback {
currentQuery?.removeListener(this)
currentQuery = null
}
}
override suspend fun load(params: LoadParams<Long>): LoadResult<Long, RowType> {
try {
val key = params.key ?: 0L
val loadSize = params.loadSize
val count = handler.awaitOne { countQuery() }
val (offset, limit) = when (params) {
is LoadParams.Prepend -> key - loadSize to loadSize.toLong()
else -> key to loadSize.toLong()
}
val data = handler.awaitList {
queryProvider(limit, offset)
.also { currentQuery = it }
}
val (prevKey, nextKey) = when (params) {
is LoadParams.Append -> { offset - loadSize to offset + loadSize }
else -> { offset to offset + loadSize }
}
return LoadResult.Page(
data = data,
prevKey = if (offset <= 0L || prevKey < 0L) null else prevKey,
nextKey = if (offset + loadSize >= count) null else nextKey,
itemsBefore = maxOf(0L, offset).toInt(),
itemsAfter = maxOf(0L, count - (offset + loadSize)).toInt(),
)
} catch (e: Exception) {
return LoadResult.Error(throwable = e)
}
}
override fun getRefreshKey(state: PagingState<Long, RowType>): Long? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey ?: anchorPage?.nextKey
}
}
override fun queryResultsChanged() {
invalidate()
}
}

View file

@ -2,7 +2,6 @@ package eu.kanade.data
import androidx.paging.PagingSource
import com.squareup.sqldelight.Query
import com.squareup.sqldelight.Transacter
import eu.kanade.tachiyomi.Database
import kotlinx.coroutines.flow.Flow
@ -33,7 +32,6 @@ interface DatabaseHandler {
fun <T : Any> subscribeToPagingSource(
countQuery: Database.() -> Query<Long>,
transacter: Database.() -> Transacter,
queryProvider: Database.(Long, Long) -> Query<T>,
): PagingSource<Long, T>
}

View file

@ -0,0 +1,72 @@
package eu.kanade.data
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.squareup.sqldelight.Query
import eu.kanade.tachiyomi.Database
import kotlin.properties.Delegates
class QueryPagingSource<RowType : Any>(
val handler: DatabaseHandler,
val countQuery: Database.() -> Query<Long>,
val queryProvider: Database.(Long, Long) -> Query<RowType>,
) : PagingSource<Long, RowType>(), Query.Listener {
override val jumpingSupported: Boolean = true
private var currentQuery: Query<RowType>? by Delegates.observable(null) { _, old, new ->
old?.removeListener(this)
new?.addListener(this)
}
init {
registerInvalidatedCallback {
currentQuery?.removeListener(this)
currentQuery = null
}
}
override suspend fun load(params: LoadParams<Long>): LoadResult<Long, RowType> {
try {
val key = params.key ?: 0L
val loadSize = params.loadSize
val count = handler.awaitOne { countQuery() }
val (offset, limit) = when (params) {
is LoadParams.Prepend -> key - loadSize to loadSize.toLong()
else -> key to loadSize.toLong()
}
val data = handler.awaitList {
queryProvider(limit, offset)
.also { currentQuery = it }
}
val (prevKey, nextKey) = when (params) {
is LoadParams.Append -> { offset - loadSize to offset + loadSize }
else -> { offset to offset + loadSize }
}
return LoadResult.Page(
data = data,
prevKey = if (offset <= 0L || prevKey < 0L) null else prevKey,
nextKey = if (offset + loadSize >= count) null else nextKey,
itemsBefore = maxOf(0L, offset).toInt(),
itemsAfter = maxOf(0L, count - (offset + loadSize)).toInt(),
)
} catch (e: Exception) {
return LoadResult.Error(throwable = e)
}
}
override fun getRefreshKey(state: PagingState<Long, RowType>): Long? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey ?: anchorPage?.nextKey
}
}
override fun queryResultsChanged() {
invalidate()
}
}

View file

@ -22,6 +22,12 @@ class AnimeRepositoryImpl(
return handler.subscribeToList { animesQueries.getFavoriteBySourceId(sourceId, animeMapper) }
}
override suspend fun getDuplicateLibraryAnime(title: String, sourceId: Long): Anime? {
return handler.awaitOneOrNull {
animesQueries.getDuplicateLibraryAnime(title, sourceId, animeMapper)
}
}
override suspend fun resetViewerFlags(): Boolean {
return try {
handler.await { animesQueries.resetViewerFlags() }

View file

@ -2,6 +2,7 @@ package eu.kanade.data.animehistory
import eu.kanade.domain.animehistory.model.AnimeHistory
import eu.kanade.domain.animehistory.model.AnimeHistoryWithRelations
import eu.kanade.domain.manga.model.MangaCover
import java.util.Date
val animehistoryMapper: (Long, Long, Date?) -> AnimeHistory = { id, episodeId, seenAt ->
@ -12,15 +13,21 @@ val animehistoryMapper: (Long, Long, Date?) -> AnimeHistory = { id, episodeId, s
)
}
val animehistoryWithRelationsMapper: (Long, Long, Long, String, String?, Float, Date?) -> AnimeHistoryWithRelations = {
historyId, animeId, episodeId, title, thumbnailUrl, episodeNumber, seenAt ->
val animehistoryWithRelationsMapper: (Long, Long, Long, String, String?, Long, Boolean, Long, Float, Date?) -> AnimeHistoryWithRelations = {
historyId, animeId, episodeId, title, thumbnailUrl, sourceId, isFavorite, coverLastModified, episodeNumber, seenAt ->
AnimeHistoryWithRelations(
id = historyId,
episodeId = episodeId,
animeId = animeId,
title = title,
thumbnailUrl = thumbnailUrl ?: "",
episodeNumber = episodeNumber,
seenAt = seenAt,
coverData = MangaCover(
mangaId = animeId,
sourceId = sourceId,
isMangaFavorite = isFavorite,
url = thumbnailUrl,
lastModified = coverLastModified,
),
)
}

View file

@ -19,7 +19,6 @@ class AnimeHistoryRepositoryImpl(
override fun getHistory(query: String): PagingSource<Long, AnimeHistoryWithRelations> {
return handler.subscribeToPagingSource(
countQuery = { animehistoryViewQueries.countHistory(query) },
transacter = { animehistoryViewQueries },
queryProvider = { limit, offset ->
animehistoryViewQueries.animehistory(query, limit, offset, animehistoryWithRelationsMapper)
},

View file

@ -2,6 +2,7 @@ package eu.kanade.data.history
import eu.kanade.domain.history.model.History
import eu.kanade.domain.history.model.HistoryWithRelations
import eu.kanade.domain.manga.model.MangaCover
import java.util.Date
val historyMapper: (Long, Long, Date?, Long) -> History = { id, chapterId, readAt, readDuration ->
@ -13,16 +14,22 @@ val historyMapper: (Long, Long, Date?, Long) -> History = { id, chapterId, readA
)
}
val historyWithRelationsMapper: (Long, Long, Long, String, String?, Float, Date?, Long) -> HistoryWithRelations = {
historyId, mangaId, chapterId, title, thumbnailUrl, chapterNumber, readAt, readDuration ->
val historyWithRelationsMapper: (Long, Long, Long, String, String?, Long, Boolean, Long, Float, Date?, Long) -> HistoryWithRelations = {
historyId, mangaId, chapterId, title, thumbnailUrl, sourceId, isFavorite, coverLastModified, chapterNumber, readAt, readDuration ->
HistoryWithRelations(
id = historyId,
chapterId = chapterId,
mangaId = mangaId,
title = title,
thumbnailUrl = thumbnailUrl ?: "",
chapterNumber = chapterNumber,
readAt = readAt,
readDuration = readDuration,
coverData = MangaCover(
mangaId = mangaId,
sourceId = sourceId,
isMangaFavorite = isFavorite,
url = thumbnailUrl,
lastModified = coverLastModified,
),
)
}

View file

@ -19,7 +19,6 @@ class HistoryRepositoryImpl(
override fun getHistory(query: String): PagingSource<Long, HistoryWithRelations> {
return handler.subscribeToPagingSource(
countQuery = { historyViewQueries.countHistory(query) },
transacter = { historyViewQueries },
queryProvider = { limit, offset ->
historyViewQueries.history(query, limit, offset, historyWithRelationsMapper)
},

View file

@ -22,6 +22,12 @@ class MangaRepositoryImpl(
return handler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, mangaMapper) }
}
override suspend fun getDuplicateLibraryManga(title: String, sourceId: Long): Manga? {
return handler.awaitOneOrNull {
mangasQueries.getDuplicateLibraryManga(title, sourceId, mangaMapper)
}
}
override suspend fun resetViewerFlags(): Boolean {
return try {
handler.await { mangasQueries.resetViewerFlags() }

View file

@ -11,6 +11,7 @@ import eu.kanade.data.history.HistoryRepositoryImpl
import eu.kanade.data.manga.MangaRepositoryImpl
import eu.kanade.data.source.SourceRepositoryImpl
import eu.kanade.domain.anime.interactor.GetAnimeById
import eu.kanade.domain.anime.interactor.GetDuplicateLibraryAnime
import eu.kanade.domain.anime.interactor.UpdateAnime
import eu.kanade.domain.anime.repository.AnimeRepository
import eu.kanade.domain.animeextension.interactor.GetAnimeExtensionLanguages
@ -64,6 +65,7 @@ import eu.kanade.domain.history.interactor.RemoveHistoryById
import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId
import eu.kanade.domain.history.interactor.UpsertHistory
import eu.kanade.domain.history.repository.HistoryRepository
import eu.kanade.domain.manga.interactor.GetDuplicateLibraryManga
import eu.kanade.domain.manga.interactor.GetFavoritesBySourceId
import eu.kanade.domain.manga.interactor.GetMangaById
import eu.kanade.domain.manga.interactor.ResetViewerFlags
@ -92,6 +94,7 @@ class DomainModule : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingletonFactory<AnimeRepository> { AnimeRepositoryImpl(get()) }
addFactory { GetDuplicateLibraryAnime(get()) }
addFactory { GetFavoritesBySourceIdAnime(get()) }
addFactory { GetAnimeById(get()) }
addFactory { GetNextEpisode(get()) }
@ -117,6 +120,7 @@ class DomainModule : InjektModule {
addFactory { DeleteCategory(get()) }
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
addFactory { GetDuplicateLibraryManga(get()) }
addFactory { GetFavoritesBySourceId(get()) }
addFactory { GetMangaById(get()) }
addFactory { GetNextChapter(get()) }

View file

@ -0,0 +1,13 @@
package eu.kanade.domain.anime.interactor
import eu.kanade.domain.anime.model.Anime
import eu.kanade.domain.anime.repository.AnimeRepository
class GetDuplicateLibraryAnime(
private val animeRepository: AnimeRepository,
) {
suspend fun await(title: String, sourceId: Long): Anime? {
return animeRepository.getDuplicateLibraryAnime(title.lowercase(), sourceId)
}
}

View file

@ -58,4 +58,8 @@ class UpdateAnime(
suspend fun awaitUpdateLastUpdate(animeId: Long): Boolean {
return animeRepository.update(AnimeUpdate(id = animeId, lastUpdate = Date().time))
}
suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean {
return animeRepository.update(AnimeUpdate(id = mangaId, coverLastModified = Date().time))
}
}

View file

@ -2,7 +2,9 @@ package eu.kanade.domain.anime.model
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import tachiyomi.animesource.model.AnimeInfo
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -45,15 +47,99 @@ data class Anime(
}
}
companion object {
val displayMode: Long
get() = episodeFlags and EPISODE_DISPLAY_MASK
val unseenFilterRaw: Long
get() = episodeFlags and EPISODE_UNSEEN_MASK
val downloadedFilterRaw: Long
get() = episodeFlags and EPISODE_DOWNLOADED_MASK
val bookmarkedFilterRaw: Long
get() = episodeFlags and EPISODE_BOOKMARKED_MASK
val unseenFilter: TriStateFilter
get() = when (unseenFilterRaw) {
EPISODE_SHOW_UNSEEN -> TriStateFilter.ENABLED_IS
EPISODE_SHOW_SEEN -> TriStateFilter.ENABLED_NOT
else -> TriStateFilter.DISABLED
}
val downloadedFilter: TriStateFilter
get() {
if (forceDownloaded()) return TriStateFilter.ENABLED_IS
return when (downloadedFilterRaw) {
EPISODE_SHOW_DOWNLOADED -> TriStateFilter.ENABLED_IS
EPISODE_SHOW_NOT_DOWNLOADED -> TriStateFilter.ENABLED_NOT
else -> TriStateFilter.DISABLED
}
}
val bookmarkedFilter: TriStateFilter
get() = when (bookmarkedFilterRaw) {
EPISODE_SHOW_BOOKMARKED -> TriStateFilter.ENABLED_IS
EPISODE_SHOW_NOT_BOOKMARKED -> TriStateFilter.ENABLED_NOT
else -> TriStateFilter.DISABLED
}
fun episodesFiltered(): Boolean {
return unseenFilter != TriStateFilter.DISABLED ||
downloadedFilter != TriStateFilter.DISABLED ||
bookmarkedFilter != TriStateFilter.DISABLED
}
fun forceDownloaded(): Boolean {
return favorite && Injekt.get<PreferencesHelper>().downloadedOnly().get()
}
fun sortDescending(): Boolean {
return episodeFlags and EPISODE_SORT_DIR_MASK == EPISODE_SORTING_DESC
}
companion object {
// Generic filter that does not filter anything
const val SHOW_ALL = 0x00000000L
const val EPISODE_SORT_DESC = 0x00000000L
const val EPISODE_SORT_ASC = 0x00000001L
const val EPISODE_SORT_DIR_MASK = 0x00000001L
const val EPISODE_SHOW_UNSEEN = 0x00000002L
const val EPISODE_SHOW_SEEN = 0x00000004L
const val EPISODE_UNSEEN_MASK = 0x00000006L
const val EPISODE_SHOW_DOWNLOADED = 0x00000008L
const val EPISODE_SHOW_NOT_DOWNLOADED = 0x00000010L
const val EPISODE_DOWNLOADED_MASK = 0x00000018L
const val EPISODE_SHOW_BOOKMARKED = 0x00000020L
const val EPISODE_SHOW_NOT_BOOKMARKED = 0x00000040L
const val EPISODE_BOOKMARKED_MASK = 0x00000060L
const val EPISODE_SORTING_SOURCE = 0x00000000L
const val EPISODE_SORTING_NUMBER = 0x00000100L
const val EPISODE_SORTING_UPLOAD_DATE = 0x00000200L
const val EPISODE_SORTING_MASK = 0x00000300L
const val EPISODE_SORTING_DESC = 0x00000000L
const val EPISODE_DISPLAY_NAME = 0x00000000L
const val EPISODE_DISPLAY_NUMBER = 0x00100000L
const val EPISODE_DISPLAY_MASK = 0x00100000L
}
}
enum class TriStateFilter {
DISABLED, // Disable filter
ENABLED_IS, // Enabled with "is" filter
ENABLED_NOT, // Enabled with "not" filter
}
fun TriStateFilter.toTriStateGroupState(): ExtendedNavigationView.Item.TriStateGroup.State {
return when (this) {
TriStateFilter.DISABLED -> ExtendedNavigationView.Item.TriStateGroup.State.IGNORE
TriStateFilter.ENABLED_IS -> ExtendedNavigationView.Item.TriStateGroup.State.INCLUDE
TriStateFilter.ENABLED_NOT -> ExtendedNavigationView.Item.TriStateGroup.State.EXCLUDE
}
}

View file

@ -10,6 +10,8 @@ interface AnimeRepository {
fun getFavoritesBySourceId(sourceId: Long): Flow<List<Anime>>
suspend fun getDuplicateLibraryAnime(title: String, sourceId: Long): Anime?
suspend fun resetViewerFlags(): Boolean
suspend fun update(update: AnimeUpdate): Boolean

View file

@ -1,5 +1,6 @@
package eu.kanade.domain.animehistory.model
import eu.kanade.domain.manga.model.MangaCover
import java.util.Date
data class AnimeHistoryWithRelations(
@ -7,7 +8,7 @@ data class AnimeHistoryWithRelations(
val episodeId: Long,
val animeId: Long,
val title: String,
val thumbnailUrl: String,
val episodeNumber: Float,
val seenAt: Date?,
val coverData: MangaCover,
)

View file

@ -1,6 +1,7 @@
package eu.kanade.domain.category.model
import java.io.Serializable
import eu.kanade.tachiyomi.data.database.models.Category as DbCategory
data class Category(
val id: Long,
@ -8,3 +9,9 @@ data class Category(
val order: Long,
val flags: Long,
) : Serializable
fun Category.toDbCategory(): DbCategory = DbCategory.create(name).also {
it.id = id.toInt()
it.order = order.toInt()
it.flags = flags.toInt()
}

View file

@ -1,5 +1,6 @@
package eu.kanade.domain.history.model
import eu.kanade.domain.manga.model.MangaCover
import java.util.Date
data class HistoryWithRelations(
@ -7,8 +8,8 @@ data class HistoryWithRelations(
val chapterId: Long,
val mangaId: Long,
val title: String,
val thumbnailUrl: String,
val chapterNumber: Float,
val readAt: Date?,
val readDuration: Long,
val coverData: MangaCover,
)

View file

@ -0,0 +1,13 @@
package eu.kanade.domain.manga.interactor
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.manga.repository.MangaRepository
class GetDuplicateLibraryManga(
private val mangaRepository: MangaRepository,
) {
suspend fun await(title: String, sourceId: Long): Manga? {
return mangaRepository.getDuplicateLibraryManga(title.lowercase(), sourceId)
}
}

View file

@ -5,6 +5,7 @@ import eu.kanade.domain.manga.repository.MangaRepository
class ResetViewerFlags(
private val mangaRepository: MangaRepository,
) {
suspend fun await(): Boolean {
return mangaRepository.resetViewerFlags()
}

View file

@ -58,4 +58,8 @@ class UpdateManga(
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Date().time))
}
suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean {
return mangaRepository.update(MangaUpdate(id = mangaId, coverLastModified = Date().time))
}
}

View file

@ -1,8 +1,10 @@
package eu.kanade.domain.manga.model
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import tachiyomi.source.model.MangaInfo
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -45,15 +47,99 @@ data class Manga(
}
}
companion object {
val displayMode: Long
get() = chapterFlags and CHAPTER_DISPLAY_MASK
val unreadFilterRaw: Long
get() = chapterFlags and CHAPTER_UNREAD_MASK
val downloadedFilterRaw: Long
get() = chapterFlags and CHAPTER_DOWNLOADED_MASK
val bookmarkedFilterRaw: Long
get() = chapterFlags and CHAPTER_BOOKMARKED_MASK
val unreadFilter: TriStateFilter
get() = when (unreadFilterRaw) {
CHAPTER_SHOW_UNREAD -> TriStateFilter.ENABLED_IS
CHAPTER_SHOW_READ -> TriStateFilter.ENABLED_NOT
else -> TriStateFilter.DISABLED
}
val downloadedFilter: TriStateFilter
get() {
if (forceDownloaded()) return TriStateFilter.ENABLED_IS
return when (downloadedFilterRaw) {
CHAPTER_SHOW_DOWNLOADED -> TriStateFilter.ENABLED_IS
CHAPTER_SHOW_NOT_DOWNLOADED -> TriStateFilter.ENABLED_NOT
else -> TriStateFilter.DISABLED
}
}
val bookmarkedFilter: TriStateFilter
get() = when (bookmarkedFilterRaw) {
CHAPTER_SHOW_BOOKMARKED -> TriStateFilter.ENABLED_IS
CHAPTER_SHOW_NOT_BOOKMARKED -> TriStateFilter.ENABLED_NOT
else -> TriStateFilter.DISABLED
}
fun chaptersFiltered(): Boolean {
return unreadFilter != TriStateFilter.DISABLED ||
downloadedFilter != TriStateFilter.DISABLED ||
bookmarkedFilter != TriStateFilter.DISABLED
}
fun forceDownloaded(): Boolean {
return favorite && Injekt.get<PreferencesHelper>().downloadedOnly().get()
}
fun sortDescending(): Boolean {
return chapterFlags and CHAPTER_SORT_DIR_MASK == CHAPTER_SORTING_DESC
}
companion object {
// Generic filter that does not filter anything
const val SHOW_ALL = 0x00000000L
const val CHAPTER_SORT_DESC = 0x00000000L
const val CHAPTER_SORT_ASC = 0x00000001L
const val CHAPTER_SORT_DIR_MASK = 0x00000001L
const val CHAPTER_SHOW_UNREAD = 0x00000002L
const val CHAPTER_SHOW_READ = 0x00000004L
const val CHAPTER_UNREAD_MASK = 0x00000006L
const val CHAPTER_SHOW_DOWNLOADED = 0x00000008L
const val CHAPTER_SHOW_NOT_DOWNLOADED = 0x00000010L
const val CHAPTER_DOWNLOADED_MASK = 0x00000018L
const val CHAPTER_SHOW_BOOKMARKED = 0x00000020L
const val CHAPTER_SHOW_NOT_BOOKMARKED = 0x00000040L
const val CHAPTER_BOOKMARKED_MASK = 0x00000060L
const val CHAPTER_SORTING_SOURCE = 0x00000000L
const val CHAPTER_SORTING_NUMBER = 0x00000100L
const val CHAPTER_SORTING_UPLOAD_DATE = 0x00000200L
const val CHAPTER_SORTING_MASK = 0x00000300L
const val CHAPTER_SORTING_DESC = 0x00000000L
const val CHAPTER_DISPLAY_NAME = 0x00000000L
const val CHAPTER_DISPLAY_NUMBER = 0x00100000L
const val CHAPTER_DISPLAY_MASK = 0x00100000L
}
}
enum class TriStateFilter {
DISABLED, // Disable filter
ENABLED_IS, // Enabled with "is" filter
ENABLED_NOT, // Enabled with "not" filter
}
fun TriStateFilter.toTriStateGroupState(): ExtendedNavigationView.Item.TriStateGroup.State {
return when (this) {
TriStateFilter.DISABLED -> ExtendedNavigationView.Item.TriStateGroup.State.IGNORE
TriStateFilter.ENABLED_IS -> ExtendedNavigationView.Item.TriStateGroup.State.INCLUDE
TriStateFilter.ENABLED_NOT -> ExtendedNavigationView.Item.TriStateGroup.State.EXCLUDE
}
}
@ -66,6 +152,7 @@ fun Manga.toDbManga(): DbManga = DbManga.create(url, title, source).also {
it.viewer_flags = viewerFlags.toInt()
it.chapter_flags = chapterFlags.toInt()
it.cover_last_modified = coverLastModified
it.thumbnail_url = thumbnailUrl
}
fun Manga.toMangaInfo(): MangaInfo = MangaInfo(

View file

@ -0,0 +1,12 @@
package eu.kanade.domain.manga.model
/**
* Contains the required data for MangaCoverFetcher
*/
data class MangaCover(
val mangaId: Long,
val sourceId: Long,
val isMangaFavorite: Boolean,
val url: String?,
val lastModified: Long,
)

View file

@ -10,6 +10,8 @@ interface MangaRepository {
fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>>
suspend fun getDuplicateLibraryManga(title: String, sourceId: Long): Manga?
suspend fun resetViewerFlags(): Boolean
suspend fun update(update: MangaUpdate): Boolean

View file

@ -41,13 +41,13 @@ fun BaseAnimeListItem(
}
}
private val defaultCover: @Composable RowScope.(Anime, () -> Unit) -> Unit = { manga, onClick ->
private val defaultCover: @Composable RowScope.(Anime, () -> Unit) -> Unit = { anime, onClick ->
MangaCover.Square(
modifier = Modifier
.padding(vertical = 8.dp)
.clickable(onClick = onClick)
.fillMaxHeight(),
data = manga.thumbnailUrl,
data = anime,
onClick = onClick,
)
}

View file

@ -1,24 +1,21 @@
package eu.kanade.presentation.animehistory
import androidx.compose.foundation.clickable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.selection.toggleable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@ -29,12 +26,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
@ -43,22 +39,20 @@ import androidx.paging.compose.items
import eu.kanade.domain.animehistory.model.AnimeHistoryWithRelations
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.MangaCover
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.history.components.AnimeHistoryItem
import eu.kanade.presentation.history.components.HistoryHeader
import eu.kanade.presentation.history.components.HistoryItemShimmer
import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.shimmerGradient
import eu.kanade.presentation.util.topPaddingValues
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.recent.animehistory.AnimeHistoryPresenter
import eu.kanade.tachiyomi.ui.recent.animehistory.HistoryState
import eu.kanade.tachiyomi.util.lang.toRelativeString
import eu.kanade.tachiyomi.util.lang.toTimestampString
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.DateFormat
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.Date
@Composable
@ -93,10 +87,7 @@ fun HistoryContent(
preferences: PreferencesHelper = Injekt.get(),
nestedScroll: NestedScrollConnection,
) {
if (history.loadState.refresh is LoadState.Loading) {
LoadingScreen()
return
} else if (history.loadState.refresh is LoadState.NotLoading && history.itemCount == 0) {
if (history.loadState.refresh is LoadState.NotLoading && history.itemCount == 0) {
EmptyScreen(textResource = R.string.information_no_recent_anime)
return
}
@ -107,6 +98,29 @@ fun HistoryContent(
var removeState by remember { mutableStateOf<AnimeHistoryWithRelations?>(null) }
val scrollState = rememberLazyListState()
val transition = rememberInfiniteTransition()
val translateAnimation = transition.animateFloat(
initialValue = 0f,
targetValue = 1000f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1000,
easing = LinearEasing,
),
),
)
val brush = Brush.linearGradient(
colors = shimmerGradient,
start = Offset(0f, 0f),
end = Offset(
x = translateAnimation.value,
y = 00f,
),
)
ScrollbarLazyColumn(
modifier = Modifier
.nestedScroll(nestedScroll),
@ -126,7 +140,7 @@ fun HistoryContent(
}
is HistoryUiModel.Item -> {
val value = item.item
HistoryItem(
AnimeHistoryItem(
modifier = Modifier.animateItemPlacement(),
history = value,
onClickCover = { onClickCover(value) },
@ -134,7 +148,9 @@ fun HistoryContent(
onClickDelete = { removeState = value },
)
}
null -> {}
null -> {
HistoryItemShimmer(brush = brush)
}
}
}
}
@ -150,88 +166,6 @@ fun HistoryContent(
}
}
@Composable
fun HistoryHeader(
modifier: Modifier = Modifier,
date: Date,
relativeTime: Int,
dateFormat: DateFormat,
) {
Text(
modifier = modifier
.padding(horizontal = horizontalPadding, vertical = 8.dp),
text = date.toRelativeString(
LocalContext.current,
relativeTime,
dateFormat,
),
style = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.SemiBold,
),
)
}
@Composable
fun HistoryItem(
modifier: Modifier = Modifier,
history: AnimeHistoryWithRelations,
onClickCover: () -> Unit,
onClickResume: () -> Unit,
onClickDelete: () -> Unit,
) {
Row(
modifier = modifier
.clickable(onClick = onClickResume)
.height(96.dp)
.padding(horizontal = horizontalPadding, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
MangaCover.Book(
modifier = Modifier
.fillMaxHeight()
.clickable(onClick = onClickCover),
data = history.thumbnailUrl,
)
Column(
modifier = Modifier
.weight(1f)
.padding(start = horizontalPadding, end = 8.dp),
) {
val textStyle = MaterialTheme.typography.bodyMedium
Text(
text = history.title,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = textStyle.copy(fontWeight = FontWeight.SemiBold),
)
Row {
Text(
text = if (history.episodeNumber > -1) {
stringResource(
R.string.recent_anime_time,
episodeFormatter.format(history.episodeNumber),
history.seenAt?.toTimestampString() ?: "",
)
} else {
history.seenAt?.toTimestampString() ?: ""
},
modifier = Modifier.padding(top = 4.dp),
style = textStyle,
)
}
}
IconButton(onClick = onClickDelete) {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = stringResource(R.string.action_delete),
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
}
@Composable
fun RemoveHistoryDialog(
onPositive: (Boolean) -> Unit,
@ -282,11 +216,6 @@ fun RemoveHistoryDialog(
)
}
private val episodeFormatter = DecimalFormat(
"#.###",
DecimalFormatSymbols().apply { decimalSeparator = '.' },
)
sealed class HistoryUiModel {
data class Header(val date: Date) : HistoryUiModel()
data class Item(val item: AnimeHistoryWithRelations) : HistoryUiModel()

View file

@ -37,7 +37,7 @@ import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import eu.kanade.presentation.browse.components.BaseBrowseItem
import eu.kanade.presentation.browse.components.ExtensionIcon
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.components.FastScrollLazyColumn
import eu.kanade.presentation.components.SwipeRefreshIndicator
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.util.plus
@ -109,7 +109,7 @@ fun ExtensionContent(
) {
var trustState by remember { mutableStateOf<AnimeExtension.Untrusted?>(null) }
ScrollbarLazyColumn(
FastScrollLazyColumn(
contentPadding = WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
) {
items(

View file

@ -40,7 +40,7 @@ import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import eu.kanade.presentation.browse.components.BaseBrowseItem
import eu.kanade.presentation.browse.components.ExtensionIcon
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.components.FastScrollLazyColumn
import eu.kanade.presentation.components.SwipeRefreshIndicator
import eu.kanade.presentation.theme.header
import eu.kanade.presentation.util.horizontalPadding
@ -113,7 +113,7 @@ fun ExtensionContent(
) {
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
ScrollbarLazyColumn(
FastScrollLazyColumn(
contentPadding = WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
) {
items(

View file

@ -1,6 +1,5 @@
package eu.kanade.presentation.browse
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -16,10 +15,8 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@ -117,17 +114,7 @@ fun MigrateAnimeSourceItem(
showLanguageInContent = source.lang != "",
onClickItem = onClickItem,
onLongClickItem = onLongClickItem,
icon = {
if (source.isStub) {
Image(
painter = painterResource(R.drawable.ic_warning_white_24dp),
contentDescription = "",
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error),
)
} else {
AnimeSourceIcon(source = source)
}
},
icon = { AnimeSourceIcon(source = source) },
action = { ItemBadges(primaryText = "$count") },
content = { source, showLanguageInContent ->
Column(

View file

@ -1,6 +1,5 @@
package eu.kanade.presentation.browse
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -16,10 +15,8 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@ -117,17 +114,7 @@ fun MigrateSourceItem(
showLanguageInContent = source.lang != "",
onClickItem = onClickItem,
onLongClickItem = onLongClickItem,
icon = {
if (source.isStub) {
Image(
painter = painterResource(R.drawable.ic_warning_white_24dp),
contentDescription = "",
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error),
)
} else {
SourceIcon(source = source)
}
},
icon = { SourceIcon(source = source) },
action = { ItemBadges(primaryText = "$count") },
content = { source, showLanguageInContent ->
Column(

View file

@ -7,6 +7,9 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
@ -14,6 +17,7 @@ import androidx.compose.runtime.produceState
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.ColorPainter
@ -40,18 +44,29 @@ fun SourceIcon(
) {
val icon = source.icon
if (icon != null) {
Image(
bitmap = icon,
contentDescription = "",
modifier = modifier.then(defaultModifier),
)
} else {
Image(
painter = painterResource(id = R.mipmap.ic_local_source),
contentDescription = "",
modifier = modifier.then(defaultModifier),
)
when {
source.isStub && icon == null -> {
Image(
imageVector = Icons.Default.Warning,
contentDescription = "",
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error),
modifier = modifier.then(defaultModifier),
)
}
icon != null -> {
Image(
bitmap = icon,
contentDescription = "",
modifier = modifier.then(defaultModifier),
)
}
else -> {
Image(
painter = painterResource(id = R.mipmap.ic_local_source),
contentDescription = "",
modifier = modifier.then(defaultModifier),
)
}
}
}

View file

@ -1,10 +1,8 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -15,12 +13,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.R
@Composable
@ -72,7 +68,6 @@ fun AppBarActions(
}
DropdownMenu(
modifier = Modifier.widthIn(min = 200.dp),
expanded = showMenu,
onDismissRequest = { showMenu = false },
) {

View file

@ -0,0 +1,28 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.PopupProperties
import androidx.compose.material3.DropdownMenu as ComposeDropdownMenu
@Composable
fun DropdownMenu(
expanded: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
properties: PopupProperties = PopupProperties(focusable = true),
content: @Composable ColumnScope.() -> Unit,
) {
ComposeDropdownMenu(
expanded = expanded,
onDismissRequest = onDismissRequest,
modifier = modifier.sizeIn(minWidth = 196.dp, maxWidth = 196.dp),
offset = DpOffset(8.dp, (-8).dp),
properties = properties,
content = content,
)
}

View file

@ -56,3 +56,38 @@ fun ScrollbarLazyColumn(
content = content,
)
}
/**
* LazyColumn with fast scroller.
*/
@Composable
fun FastScrollLazyColumn(
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical =
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
content: LazyListScope.() -> Unit,
) {
VerticalFastScroller(
listState = state,
modifier = modifier,
topContentPadding = contentPadding.calculateTopPadding(),
endContentPadding = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
) {
LazyColumn(
state = state,
contentPadding = contentPadding,
reverseLayout = reverseLayout,
verticalArrangement = verticalArrangement,
horizontalAlignment = horizontalAlignment,
flingBehavior = flingBehavior,
userScrollEnabled = userScrollEnabled,
content = content,
)
}
}

View file

@ -1,5 +1,6 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
@ -9,21 +10,23 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import eu.kanade.presentation.util.rememberResourceBitmapPainter
import eu.kanade.tachiyomi.R
enum class MangaCover(private val ratio: Float) {
enum class MangaCover(val ratio: Float) {
Square(1f / 1f),
Book(2f / 3f);
@Composable
operator fun invoke(
modifier: Modifier = Modifier,
data: String?,
data: Any?,
contentDescription: String? = null,
shape: Shape? = null,
onClick: (() -> Unit)? = null,
) {
AsyncImage(
model = data,
@ -32,7 +35,15 @@ enum class MangaCover(private val ratio: Float) {
contentDescription = contentDescription,
modifier = modifier
.aspectRatio(ratio)
.clip(shape ?: RoundedCornerShape(4.dp)),
.clip(shape ?: RoundedCornerShape(4.dp))
.then(
if (onClick != null) {
Modifier.clickable(
role = Role.Button,
onClick = onClick,
)
} else Modifier,
),
contentScale = ContentScale.Crop,
)
}

View file

@ -17,8 +17,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import eu.kanade.core.prefs.PreferenceMutableState
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.util.horizontalPadding
const val DIVIDER_ALPHA = 0.2f
@ -102,7 +104,8 @@ fun PreferenceRow(
@Composable
fun SwitchPreference(
modifier: Modifier = Modifier,
preference: PreferenceMutableState<Boolean>,
checked: Boolean,
onClick: () -> Unit,
title: String,
subtitle: String? = null,
painter: Painter? = null,
@ -112,7 +115,53 @@ fun SwitchPreference(
title = title,
subtitle = subtitle,
painter = painter,
action = { Switch(checked = preference.value, onCheckedChange = null) },
action = { Switch(checked = checked, onCheckedChange = null) },
onClick = onClick,
)
}
@Composable
fun SwitchPreference(
modifier: Modifier = Modifier,
preference: PreferenceMutableState<Boolean>,
title: String,
subtitle: String? = null,
painter: Painter? = null,
) {
SwitchPreference(
modifier = modifier,
title = title,
subtitle = subtitle,
painter = painter,
checked = preference.value,
onClick = { preference.value = !preference.value },
)
}
@Preview
@Composable
private fun PreferencesPreview() {
TachiyomiTheme {
Column {
PreferenceRow(
title = "Plain",
subtitle = "Subtitle",
)
Divider()
SwitchPreference(
title = "Switch (on)",
subtitle = "Subtitle",
checked = true,
onClick = {},
)
SwitchPreference(
title = "Switch (off)",
subtitle = "Subtitle",
checked = false,
onClick = {},
)
}
}
}

View file

@ -0,0 +1,195 @@
package eu.kanade.presentation.components
import android.view.ViewConfiguration
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.systemGestureExclusion
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMaxBy
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collectLatest
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
@Composable
fun VerticalFastScroller(
listState: LazyListState,
modifier: Modifier = Modifier,
thumbColor: Color = MaterialTheme.colorScheme.primary,
topContentPadding: Dp = Dp.Hairline,
endContentPadding: Dp = Dp.Hairline,
content: @Composable () -> Unit,
) {
SubcomposeLayout(modifier = modifier) { constraints ->
val contentPlaceable = subcompose("content", content).map { it.measure(constraints) }
val contentHeight = contentPlaceable.fastMaxBy { it.height }?.height ?: 0
val contentWidth = contentPlaceable.fastMaxBy { it.width }?.width ?: 0
val scrollerConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val scrollerPlaceable = subcompose("scroller") {
val layoutInfo = listState.layoutInfo
val showScroller = layoutInfo.visibleItemsInfo.size < layoutInfo.totalItemsCount
if (!showScroller) return@subcompose
val thumbTopPadding = with(LocalDensity.current) { topContentPadding.toPx() }
var thumbOffsetY by remember(thumbTopPadding) { mutableStateOf(thumbTopPadding) }
val dragInteractionSource = remember { MutableInteractionSource() }
val isThumbDragged by dragInteractionSource.collectIsDraggedAsState()
val scrolled = remember {
MutableSharedFlow<Unit>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
}
val heightPx = contentHeight.toFloat() - thumbTopPadding - listState.layoutInfo.afterContentPadding
val thumbHeightPx = with(LocalDensity.current) { ThumbLength.toPx() }
val trackHeightPx = heightPx - thumbHeightPx
// When thumb dragged
LaunchedEffect(thumbOffsetY) {
if (layoutInfo.totalItemsCount == 0 || !isThumbDragged) return@LaunchedEffect
val scrollRatio = (thumbOffsetY - thumbTopPadding) / trackHeightPx
val scrollItem = layoutInfo.totalItemsCount * scrollRatio
val scrollItemRounded = scrollItem.roundToInt()
val scrollItemSize = layoutInfo.visibleItemsInfo.find { it.index == scrollItemRounded }?.size ?: 0
val scrollItemOffset = scrollItemSize * (scrollItem - scrollItemRounded)
listState.scrollToItem(index = scrollItemRounded, scrollOffset = scrollItemOffset.roundToInt())
scrolled.tryEmit(Unit)
}
// When list scrolled
LaunchedEffect(listState.firstVisibleItemScrollOffset) {
if (listState.layoutInfo.totalItemsCount == 0 || isThumbDragged) return@LaunchedEffect
val scrollOffset = computeScrollOffset(state = listState)
val scrollRange = computeScrollRange(state = listState)
val proportion = scrollOffset.toFloat() / (scrollRange.toFloat() - heightPx)
thumbOffsetY = trackHeightPx * proportion + thumbTopPadding
scrolled.tryEmit(Unit)
}
// Thumb alpha
val alpha = remember { Animatable(0f) }
val isThumbVisible = alpha.value > 0f
LaunchedEffect(scrolled, alpha) {
scrolled.collectLatest {
alpha.snapTo(1f)
alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec)
}
}
Box(
modifier = Modifier
.offset { IntOffset(0, thumbOffsetY.roundToInt()) }
.height(ThumbLength)
.then(
// Exclude thumb from gesture area only when needed
if (isThumbVisible && !isThumbDragged && !listState.isScrollInProgress) {
Modifier.systemGestureExclusion()
} else Modifier,
)
.padding(horizontal = 8.dp)
.padding(end = endContentPadding)
.width(ThumbThickness)
.alpha(alpha.value)
.background(color = thumbColor, shape = ThumbShape)
.then(
// Recompose opts
if (!listState.isScrollInProgress) {
Modifier.draggable(
interactionSource = dragInteractionSource,
orientation = Orientation.Vertical,
enabled = isThumbVisible,
state = rememberDraggableState { delta ->
val newOffsetY = thumbOffsetY + delta
thumbOffsetY = newOffsetY.coerceIn(thumbTopPadding, thumbTopPadding + trackHeightPx)
},
)
} else Modifier,
),
)
}.map { it.measure(scrollerConstraints) }
val scrollerWidth = scrollerPlaceable.fastMaxBy { it.width }?.width ?: 0
layout(contentWidth, contentHeight) {
contentPlaceable.fastForEach {
it.place(0, 0)
}
scrollerPlaceable.fastForEach {
it.placeRelative(contentWidth - scrollerWidth, 0)
}
}
}
}
private fun computeScrollOffset(state: LazyListState): Int {
if (state.layoutInfo.totalItemsCount == 0) return 0
val visibleItems = state.layoutInfo.visibleItemsInfo
val startChild = visibleItems.first()
val endChild = visibleItems.last()
val minPosition = min(startChild.index, endChild.index)
val maxPosition = max(startChild.index, endChild.index)
val itemsBefore = minPosition.coerceAtLeast(0)
val startDecoratedTop = startChild.top
val laidOutArea = abs(endChild.bottom - startDecoratedTop)
val itemRange = abs(minPosition - maxPosition) + 1
val avgSizePerRow = laidOutArea.toFloat() / itemRange
return (itemsBefore * avgSizePerRow + (0 - startDecoratedTop)).roundToInt()
}
private fun computeScrollRange(state: LazyListState): Int {
if (state.layoutInfo.totalItemsCount == 0) return 0
val visibleItems = state.layoutInfo.visibleItemsInfo
val startChild = visibleItems.first()
val endChild = visibleItems.last()
val laidOutArea = endChild.bottom - startChild.top
val laidOutRange = abs(startChild.index - endChild.index) + 1
return (laidOutArea.toFloat() / laidOutRange * state.layoutInfo.totalItemsCount).roundToInt()
}
private val ThumbLength = 48.dp
private val ThumbThickness = 8.dp
private val ThumbShape = RoundedCornerShape(ThumbThickness / 2)
private val FadeOutAnimationSpec = tween<Float>(
durationMillis = ViewConfiguration.getScrollBarFadeDuration(),
delayMillis = 2000,
)
private val LazyListItemInfo.top: Int
get() = offset
private val LazyListItemInfo.bottom: Int
get() = offset + size

View file

@ -1,24 +1,21 @@
package eu.kanade.presentation.history
import androidx.compose.foundation.clickable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.selection.toggleable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@ -29,12 +26,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush.Companion.linearGradient
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
@ -43,22 +39,20 @@ import androidx.paging.compose.items
import eu.kanade.domain.history.model.HistoryWithRelations
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.MangaCover
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.history.components.HistoryHeader
import eu.kanade.presentation.history.components.HistoryItem
import eu.kanade.presentation.history.components.HistoryItemShimmer
import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.shimmerGradient
import eu.kanade.presentation.util.topPaddingValues
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.recent.history.HistoryPresenter
import eu.kanade.tachiyomi.ui.recent.history.HistoryState
import eu.kanade.tachiyomi.util.lang.toRelativeString
import eu.kanade.tachiyomi.util.lang.toTimestampString
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.DateFormat
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.Date
@Composable
@ -93,10 +87,7 @@ fun HistoryContent(
preferences: PreferencesHelper = Injekt.get(),
nestedScroll: NestedScrollConnection,
) {
if (history.loadState.refresh is LoadState.Loading) {
LoadingScreen()
return
} else if (history.loadState.refresh is LoadState.NotLoading && history.itemCount == 0) {
if (history.loadState.refresh is LoadState.NotLoading && history.itemCount == 0) {
EmptyScreen(textResource = R.string.information_no_recent_manga)
return
}
@ -107,6 +98,29 @@ fun HistoryContent(
var removeState by remember { mutableStateOf<HistoryWithRelations?>(null) }
val scrollState = rememberLazyListState()
val transition = rememberInfiniteTransition()
val translateAnimation = transition.animateFloat(
initialValue = 0f,
targetValue = 1000f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1000,
easing = LinearEasing,
),
),
)
val brush = linearGradient(
colors = shimmerGradient,
start = Offset(0f, 0f),
end = Offset(
x = translateAnimation.value,
y = 00f,
),
)
ScrollbarLazyColumn(
modifier = Modifier
.nestedScroll(nestedScroll),
@ -134,7 +148,9 @@ fun HistoryContent(
onClickDelete = { removeState = value },
)
}
null -> {}
null -> {
HistoryItemShimmer(brush = brush)
}
}
}
}
@ -150,88 +166,6 @@ fun HistoryContent(
}
}
@Composable
fun HistoryHeader(
modifier: Modifier = Modifier,
date: Date,
relativeTime: Int,
dateFormat: DateFormat,
) {
Text(
modifier = modifier
.padding(horizontal = horizontalPadding, vertical = 8.dp),
text = date.toRelativeString(
LocalContext.current,
relativeTime,
dateFormat,
),
style = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.SemiBold,
),
)
}
@Composable
fun HistoryItem(
modifier: Modifier = Modifier,
history: HistoryWithRelations,
onClickCover: () -> Unit,
onClickResume: () -> Unit,
onClickDelete: () -> Unit,
) {
Row(
modifier = modifier
.clickable(onClick = onClickResume)
.height(96.dp)
.padding(horizontal = horizontalPadding, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
MangaCover.Book(
modifier = Modifier
.fillMaxHeight()
.clickable(onClick = onClickCover),
data = history.thumbnailUrl,
)
Column(
modifier = Modifier
.weight(1f)
.padding(start = horizontalPadding, end = 8.dp),
) {
val textStyle = MaterialTheme.typography.bodyMedium
Text(
text = history.title,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = textStyle.copy(fontWeight = FontWeight.SemiBold),
)
Row {
Text(
text = if (history.chapterNumber > -1) {
stringResource(
R.string.recent_manga_time,
chapterFormatter.format(history.chapterNumber),
history.readAt?.toTimestampString() ?: "",
)
} else {
history.readAt?.toTimestampString() ?: ""
},
modifier = Modifier.padding(top = 4.dp),
style = textStyle,
)
}
}
IconButton(onClick = onClickDelete) {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = stringResource(R.string.action_delete),
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
}
@Composable
fun RemoveHistoryDialog(
onPositive: (Boolean) -> Unit,
@ -282,11 +216,6 @@ fun RemoveHistoryDialog(
)
}
private val chapterFormatter = DecimalFormat(
"#.###",
DecimalFormatSymbols().apply { decimalSeparator = '.' },
)
sealed class HistoryUiModel {
data class Header(val date: Date) : HistoryUiModel()
data class Item(val item: HistoryWithRelations) : HistoryUiModel()

View file

@ -0,0 +1,36 @@
package eu.kanade.presentation.history.components
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.tachiyomi.util.lang.toRelativeString
import java.text.DateFormat
import java.util.Date
@Composable
fun HistoryHeader(
modifier: Modifier = Modifier,
date: Date,
relativeTime: Int,
dateFormat: DateFormat,
) {
Text(
modifier = modifier
.padding(horizontal = horizontalPadding, vertical = 8.dp),
text = date.toRelativeString(
LocalContext.current,
relativeTime,
dateFormat,
),
style = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.SemiBold,
),
)
}

View file

@ -0,0 +1,200 @@
package eu.kanade.presentation.history.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import eu.kanade.domain.animehistory.model.AnimeHistoryWithRelations
import eu.kanade.domain.history.model.HistoryWithRelations
import eu.kanade.presentation.components.MangaCover
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.toTimestampString
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
private val HISTORY_ITEM_HEIGHT = 96.dp
@Composable
fun HistoryItem(
modifier: Modifier = Modifier,
history: HistoryWithRelations,
onClickCover: () -> Unit,
onClickResume: () -> Unit,
onClickDelete: () -> Unit,
) {
Row(
modifier = modifier
.clickable(onClick = onClickResume)
.height(HISTORY_ITEM_HEIGHT)
.padding(horizontal = horizontalPadding, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
MangaCover.Book(
modifier = Modifier.fillMaxHeight(),
data = history.coverData,
onClick = onClickCover,
)
Column(
modifier = Modifier
.weight(1f)
.padding(start = horizontalPadding, end = 8.dp),
) {
val textStyle = MaterialTheme.typography.bodyMedium
Text(
text = history.title,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = textStyle.copy(fontWeight = FontWeight.SemiBold),
)
Text(
text = if (history.chapterNumber > -1) {
stringResource(
R.string.recent_manga_time,
chapterFormatter.format(history.chapterNumber),
history.readAt?.toTimestampString() ?: "",
)
} else {
history.readAt?.toTimestampString() ?: ""
},
modifier = Modifier.padding(top = 4.dp),
style = textStyle,
)
}
IconButton(onClick = onClickDelete) {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = stringResource(R.string.action_delete),
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
}
@Composable
fun AnimeHistoryItem(
modifier: Modifier = Modifier,
history: AnimeHistoryWithRelations,
onClickCover: () -> Unit,
onClickResume: () -> Unit,
onClickDelete: () -> Unit,
) {
Row(
modifier = modifier
.clickable(onClick = onClickResume)
.height(HISTORY_ITEM_HEIGHT)
.padding(horizontal = horizontalPadding, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
MangaCover.Book(
modifier = Modifier.fillMaxHeight(),
data = history.coverData,
onClick = onClickCover,
)
Column(
modifier = Modifier
.weight(1f)
.padding(start = horizontalPadding, end = 8.dp),
) {
val textStyle = MaterialTheme.typography.bodyMedium
Text(
text = history.title,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = textStyle.copy(fontWeight = FontWeight.SemiBold),
)
Text(
text = if (history.episodeNumber > -1) {
stringResource(
R.string.recent_anime_time,
chapterFormatter.format(history.episodeNumber),
history.seenAt?.toTimestampString() ?: "",
)
} else {
history.seenAt?.toTimestampString() ?: ""
},
modifier = Modifier.padding(top = 4.dp),
style = textStyle,
)
}
IconButton(onClick = onClickDelete) {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = stringResource(R.string.action_delete),
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
}
@Composable
fun HistoryItemShimmer(brush: Brush) {
Row(
modifier = Modifier
.height(HISTORY_ITEM_HEIGHT)
.padding(horizontal = horizontalPadding, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.fillMaxHeight()
.aspectRatio(MangaCover.Book.ratio)
.clip(RoundedCornerShape(4.dp))
.drawBehind {
drawRect(brush = brush)
},
)
Column(
modifier = Modifier
.weight(1f)
.padding(start = horizontalPadding, end = 8.dp),
) {
Box(
modifier = Modifier
.drawBehind {
drawRect(brush = brush)
}
.height(14.dp)
.fillMaxWidth(0.70f),
)
Box(
modifier = Modifier
.padding(top = 4.dp)
.height(14.dp)
.fillMaxWidth(0.45f)
.drawBehind {
drawRect(brush = brush)
},
)
}
}
}
private val chapterFormatter = DecimalFormat(
"#.###",
DecimalFormatSymbols().apply { decimalSeparator = '.' },
)

View file

@ -45,9 +45,9 @@ private val defaultCover: @Composable RowScope.(Manga, () -> Unit) -> Unit = { m
MangaCover.Square(
modifier = Modifier
.padding(vertical = 8.dp)
.clickable(onClick = onClick)
.fillMaxHeight(),
data = manga.thumbnailUrl,
data = manga,
onClick = onClick,
)
}

View file

@ -0,0 +1,9 @@
package eu.kanade.presentation.util
import androidx.compose.ui.graphics.Color
val shimmerGradient = listOf(
Color.LightGray.copy(alpha = 0.8f),
Color.LightGray.copy(alpha = 0.2f),
Color.LightGray.copy(alpha = 0.8f),
)

View file

@ -27,8 +27,12 @@ import coil.util.DebugLogger
import eu.kanade.domain.DomainModule
import eu.kanade.tachiyomi.data.coil.AnimeCoverFetcher
import eu.kanade.tachiyomi.data.coil.AnimeCoverKeyer
import eu.kanade.tachiyomi.data.coil.AnimeKeyer
import eu.kanade.tachiyomi.data.coil.DomainAnimeKeyer
import eu.kanade.tachiyomi.data.coil.DomainMangaKeyer
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
import eu.kanade.tachiyomi.data.coil.MangaKeyer
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferenceValues
@ -138,6 +142,14 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
add(TachiyomiImageDecoder.Factory())
add(MangaCoverFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit)))
add(AnimeCoverFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit)))
add(AnimeCoverFetcher.DomainAnimeFactory(lazy(callFactoryInit), lazy(diskCacheInit)))
add(AnimeCoverFetcher.AnimeCoverFactory(lazy(callFactoryInit), lazy(diskCacheInit)))
add(AnimeKeyer())
add(DomainAnimeKeyer())
add(MangaCoverFetcher.DomainMangaFactory(lazy(callFactoryInit), lazy(diskCacheInit)))
add(MangaCoverFetcher.MangaCoverFactory(lazy(callFactoryInit), lazy(diskCacheInit)))
add(MangaKeyer())
add(DomainMangaKeyer())
add(MangaCoverKeyer())
add(AnimeCoverKeyer())
}

View file

@ -52,7 +52,7 @@ class AppModule(val app: Application) : InjektModule {
// This is used to allow incremental migration from Storio
val openHelperMangaConfig = SupportSQLiteOpenHelper.Configuration.builder(app)
.callback(DbOpenCallback())
.name(DbOpenCallback.DATABASE_NAME)
.name(DbOpenCallback.DATABASE_FILENAME)
.noBackupDirectory(false)
.build()
@ -65,7 +65,7 @@ class AppModule(val app: Application) : InjektModule {
val openHelperAnimeConfig = SupportSQLiteOpenHelper.Configuration.builder(app)
.callback(AnimeDbOpenCallback())
.name(AnimeDbOpenCallback.DATABASE_NAME)
.name(AnimeDbOpenCallback.DATABASE_FILENAME)
.noBackupDirectory(false)
.build()

View file

@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.animesource.model.toEpisodeInfo
import eu.kanade.tachiyomi.animesource.model.toSAnime
import eu.kanade.tachiyomi.animesource.model.toSEpisode
import eu.kanade.tachiyomi.animesource.model.toVideoUrl
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.AnimeExtensionManager
import eu.kanade.tachiyomi.util.lang.awaitSingle
import rx.Observable
@ -104,3 +105,18 @@ fun AnimeSource.icon(): Drawable? = Injekt.get<AnimeExtensionManager>().getAppIc
fun AnimeSource.getPreferenceKey(): String = "source_$id"
fun AnimeSource.toAnimeSourceData(): AnimeSourceData = AnimeSourceData(id = id, lang = lang, name = name)
fun AnimeSource.getNameForAnimeInfo(): String {
val preferences = Injekt.get<PreferencesHelper>()
val enabledLanguages = preferences.enabledLanguages().get()
.filterNot { it in listOf("all", "other") }
val hasOneActiveLanguages = enabledLanguages.size == 1
val isInEnabledLanguages = lang in enabledLanguages
return when {
// For edge cases where user disables a source they got manga of in their library.
hasOneActiveLanguages && !isInEnabledLanguages -> toString()
// Hide the language tag when only one language is used.
hasOneActiveLanguages && isInEnabledLanguages -> name
else -> toString()
}
}

View file

@ -548,10 +548,12 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
handler
.awaitOneOrNull { chaptersQueries.getChapterByUrl(url) }
?.let {
HistoryUpdate(
chapterId = it._id,
readAt = Date(lastRead),
sessionReadDuration = 0,
toUpdate.add(
HistoryUpdate(
chapterId = it._id,
readAt = Date(lastRead),
sessionReadDuration = 0,
),
)
}
}

View file

@ -10,11 +10,15 @@ import coil.fetch.SourceResult
import coil.network.HttpException
import coil.request.Options
import coil.request.Parameters
import eu.kanade.domain.manga.model.MangaCover
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.coil.AnimeCoverFetcher.Companion.USE_CUSTOM_COVER
import eu.kanade.tachiyomi.data.database.models.Anime
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority
@ -30,6 +34,7 @@ import okio.sink
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.net.HttpURLConnection
import eu.kanade.domain.anime.model.Anime as DomainAnime
/**
* A [Fetcher] that fetches cover image for [Anime] object.
@ -48,7 +53,7 @@ class AnimeCoverFetcher(
private val coverFileLazy: Lazy<File?>,
private val customCoverFileLazy: Lazy<File>,
private val diskCacheKeyLazy: Lazy<String>,
private val sourceLazy: Lazy<HttpSource?>,
private val sourceLazy: Lazy<AnimeHttpSource?>,
private val callFactoryLazy: Lazy<Call.Factory>,
private val diskCacheLazy: Lazy<DiskCache>,
) : Fetcher {
@ -290,8 +295,54 @@ class AnimeCoverFetcher(
options = options,
coverFileLazy = lazy { coverCache.getCoverFile(data.thumbnail_url) },
customCoverFileLazy = lazy { coverCache.getCustomCoverFile(data.id) },
diskCacheKeyLazy = lazy { AnimeKeyer().key(data, options) },
sourceLazy = lazy { sourceManager.get(data.source) as? AnimeHttpSource },
callFactoryLazy = callFactoryLazy,
diskCacheLazy = diskCacheLazy,
)
}
}
class DomainAnimeFactory(
private val callFactoryLazy: Lazy<Call.Factory>,
private val diskCacheLazy: Lazy<DiskCache>,
) : Fetcher.Factory<DomainAnime> {
private val coverCache: CoverCache by injectLazy()
private val sourceManager: SourceManager by injectLazy()
override fun create(data: DomainAnime, options: Options, imageLoader: ImageLoader): Fetcher {
return AnimeCoverFetcher(
url = data.thumbnailUrl,
isLibraryAnime = data.favorite,
options = options,
coverFileLazy = lazy { coverCache.getCoverFile(data.thumbnailUrl) },
customCoverFileLazy = lazy { coverCache.getCustomCoverFile(data.id) },
diskCacheKeyLazy = lazy { DomainAnimeKeyer().key(data, options) },
sourceLazy = lazy { sourceManager.get(data.source) as? AnimeHttpSource },
callFactoryLazy = callFactoryLazy,
diskCacheLazy = diskCacheLazy,
)
}
}
class AnimeCoverFactory(
private val callFactoryLazy: Lazy<Call.Factory>,
private val diskCacheLazy: Lazy<DiskCache>,
) : Fetcher.Factory<MangaCover> {
private val coverCache: AnimeCoverCache by injectLazy()
private val sourceManager: AnimeSourceManager by injectLazy()
override fun create(data: MangaCover, options: Options, imageLoader: ImageLoader): Fetcher {
return MangaCoverFetcher(
url = data.url,
isLibraryManga = data.isMangaFavorite,
options = options,
coverFileLazy = lazy { coverCache.getCoverFile(data.url) },
customCoverFileLazy = lazy { coverCache.getCustomCoverFile(data.mangaId) },
diskCacheKeyLazy = lazy { AnimeCoverKeyer().key(data, options) },
sourceLazy = lazy { sourceManager.get(data.source) as? HttpSource },
sourceLazy = lazy { sourceManager.get(data.sourceId) as? HttpSource },
callFactoryLazy = callFactoryLazy,
diskCacheLazy = diskCacheLazy,
)

View file

@ -2,10 +2,16 @@ package eu.kanade.tachiyomi.data.coil
import coil.key.Keyer
import coil.request.Options
import eu.kanade.domain.anime.model.hasCustomCover
import eu.kanade.domain.manga.model.MangaCover
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
import eu.kanade.tachiyomi.data.database.models.Anime
import eu.kanade.tachiyomi.util.hasCustomCover
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import eu.kanade.domain.anime.model.Anime as DomainAnime
class AnimeCoverKeyer : Keyer<Anime> {
class AnimeKeyer : Keyer<Anime> {
override fun key(data: Anime, options: Options): String {
return if (data.hasCustomCover()) {
"${data.id};${data.cover_last_modified}"
@ -14,3 +20,23 @@ class AnimeCoverKeyer : Keyer<Anime> {
}
}
}
class DomainAnimeKeyer : Keyer<DomainAnime> {
override fun key(data: DomainAnime, options: Options): String {
return if (data.hasCustomCover()) {
"${data.id};${data.coverLastModified}"
} else {
"${data.thumbnailUrl};${data.coverLastModified}"
}
}
}
class AnimeCoverKeyer : Keyer<MangaCover> {
override fun key(data: MangaCover, options: Options): String {
return if (Injekt.get<AnimeCoverCache>().getCustomCoverFile(data.mangaId).exists()) {
"${data.mangaId};${data.lastModified}"
} else {
"${data.url};${data.lastModified}"
}
}
}

View file

@ -10,6 +10,7 @@ import coil.fetch.SourceResult
import coil.network.HttpException
import coil.request.Options
import coil.request.Parameters
import eu.kanade.domain.manga.model.MangaCover
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER
import eu.kanade.tachiyomi.data.database.models.Manga
@ -30,6 +31,7 @@ import okio.sink
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.net.HttpURLConnection
import eu.kanade.domain.manga.model.Manga as DomainManga
/**
* A [Fetcher] that fetches cover image for [Manga] object.
@ -290,7 +292,7 @@ class MangaCoverFetcher(
options = options,
coverFileLazy = lazy { coverCache.getCoverFile(data.thumbnail_url) },
customCoverFileLazy = lazy { coverCache.getCustomCoverFile(data.id) },
diskCacheKeyLazy = lazy { MangaCoverKeyer().key(data, options) },
diskCacheKeyLazy = lazy { MangaKeyer().key(data, options) },
sourceLazy = lazy { sourceManager.get(data.source) as? HttpSource },
callFactoryLazy = callFactoryLazy,
diskCacheLazy = diskCacheLazy,
@ -298,6 +300,52 @@ class MangaCoverFetcher(
}
}
class DomainMangaFactory(
private val callFactoryLazy: Lazy<Call.Factory>,
private val diskCacheLazy: Lazy<DiskCache>,
) : Fetcher.Factory<DomainManga> {
private val coverCache: CoverCache by injectLazy()
private val sourceManager: SourceManager by injectLazy()
override fun create(data: DomainManga, options: Options, imageLoader: ImageLoader): Fetcher {
return MangaCoverFetcher(
url = data.thumbnailUrl,
isLibraryManga = data.favorite,
options = options,
coverFileLazy = lazy { coverCache.getCoverFile(data.thumbnailUrl) },
customCoverFileLazy = lazy { coverCache.getCustomCoverFile(data.id) },
diskCacheKeyLazy = lazy { DomainMangaKeyer().key(data, options) },
sourceLazy = lazy { sourceManager.get(data.source) as? HttpSource },
callFactoryLazy = callFactoryLazy,
diskCacheLazy = diskCacheLazy,
)
}
}
class MangaCoverFactory(
private val callFactoryLazy: Lazy<Call.Factory>,
private val diskCacheLazy: Lazy<DiskCache>,
) : Fetcher.Factory<MangaCover> {
private val coverCache: CoverCache by injectLazy()
private val sourceManager: SourceManager by injectLazy()
override fun create(data: MangaCover, options: Options, imageLoader: ImageLoader): Fetcher {
return MangaCoverFetcher(
url = data.url,
isLibraryManga = data.isMangaFavorite,
options = options,
coverFileLazy = lazy { coverCache.getCoverFile(data.url) },
customCoverFileLazy = lazy { coverCache.getCustomCoverFile(data.mangaId) },
diskCacheKeyLazy = lazy { MangaCoverKeyer().key(data, options) },
sourceLazy = lazy { sourceManager.get(data.sourceId) as? HttpSource },
callFactoryLazy = callFactoryLazy,
diskCacheLazy = diskCacheLazy,
)
}
}
companion object {
const val USE_CUSTOM_COVER = "use_custom_cover"

View file

@ -2,10 +2,16 @@ package eu.kanade.tachiyomi.data.coil
import coil.key.Keyer
import coil.request.Options
import eu.kanade.domain.manga.model.MangaCover
import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.util.hasCustomCover
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import eu.kanade.domain.manga.model.Manga as DomainManga
class MangaCoverKeyer : Keyer<Manga> {
class MangaKeyer : Keyer<Manga> {
override fun key(data: Manga, options: Options): String {
return if (data.hasCustomCover()) {
"${data.id};${data.cover_last_modified}"
@ -14,3 +20,23 @@ class MangaCoverKeyer : Keyer<Manga> {
}
}
}
class DomainMangaKeyer : Keyer<DomainManga> {
override fun key(data: DomainManga, options: Options): String {
return if (data.hasCustomCover()) {
"${data.id};${data.coverLastModified}"
} else {
"${data.thumbnailUrl};${data.coverLastModified}"
}
}
}
class MangaCoverKeyer : Keyer<MangaCover> {
override fun key(data: MangaCover, options: Options): String {
return if (Injekt.get<CoverCache>().getCustomCoverFile(data.mangaId).exists()) {
"${data.mangaId};${data.lastModified}"
} else {
"${data.url};${data.lastModified}"
}
}
}

View file

@ -12,7 +12,7 @@ class AnimeDbOpenCallback : SupportSQLiteOpenHelper.Callback(AnimeDatabase.Schem
/**
* Name of the database file.
*/
const val DATABASE_NAME = "tachiyomi.animedb"
const val DATABASE_FILENAME = "tachiyomi.animedb"
}
override fun onCreate(db: SupportSQLiteDatabase) {

View file

@ -9,10 +9,7 @@ import logcat.logcat
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(Database.Schema.version) {
companion object {
/**
* Name of the database file.
*/
const val DATABASE_NAME = "tachiyomi.db"
const val DATABASE_FILENAME = "tachiyomi.db"
}
override fun onCreate(db: SupportSQLiteDatabase) {

View file

@ -3,6 +3,5 @@ package eu.kanade.tachiyomi.data.database
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
interface DbProvider {
val db: DefaultStorIOSQLite
}

View file

@ -6,8 +6,6 @@ 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.ChapterBackupPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.ChapterKnownBackupPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
@ -49,17 +47,6 @@ interface ChapterQueries : DbProvider {
)
.prepare()
fun getChapter(url: String) = db.get()
.`object`(Chapter::class.java)
.withQuery(
Query.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ?")
.whereArgs(url)
.build(),
)
.prepare()
fun getChapter(url: String, mangaId: Long) = db.get()
.`object`(Chapter::class.java)
.withQuery(
@ -75,16 +62,6 @@ interface ChapterQueries : DbProvider {
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
fun updateChaptersBackup(chapters: List<Chapter>) = db.put()
.objects(chapters)
.withPutResolver(ChapterBackupPutResolver())
.prepare()
fun updateKnownChaptersBackup(chapters: List<Chapter>) = db.put()
.objects(chapters)
.withPutResolver(ChapterKnownBackupPutResolver())
.prepare()
fun updateChapterProgress(chapter: Chapter) = db.put()
.`object`(chapter)
.withPutResolver(ChapterProgressPutResolver())

View file

@ -7,8 +7,6 @@ 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.EpisodeBackupPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.EpisodeKnownBackupPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.EpisodeProgressPutResolver
import eu.kanade.tachiyomi.data.database.tables.EpisodeTable
import java.util.Date
@ -75,16 +73,6 @@ interface EpisodeQueries : DbProvider {
fun deleteEpisodes(episodes: List<Episode>) = db.delete().objects(episodes).prepare()
fun updateEpisodesBackup(episodes: List<Episode>) = db.put()
.objects(episodes)
.withPutResolver(EpisodeBackupPutResolver())
.prepare()
fun updateKnownEpisodesBackup(episodes: List<Episode>) = db.put()
.objects(episodes)
.withPutResolver(EpisodeKnownBackupPutResolver())
.prepare()
fun updateEpisodeProgress(episode: Episode) = db.put()
.`object`(episode)
.withPutResolver(EpisodeProgressPutResolver())

View file

@ -28,21 +28,6 @@ interface MangaQueries : DbProvider {
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
.prepare()
fun getDuplicateLibraryManga(manga: Manga) = db.get()
.`object`(Manga::class.java)
.withQuery(
Query.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_FAVORITE} = 1 AND LOWER(${MangaTable.COL_TITLE}) = ? AND ${MangaTable.COL_SOURCE} != ?")
.whereArgs(
manga.title.lowercase(),
manga.source,
)
.limit(1)
.build(),
)
.prepare()
fun getFavoriteMangas(sortByTitle: Boolean = true): PreparedGetListOfObjects<Manga> {
var queryBuilder = Query.builder()
.table(MangaTable.TABLE)

View file

@ -97,42 +97,6 @@ fun getRecentsQueryAnime() =
ORDER BY ${Episode.COL_DATE_UPLOAD} DESC
"""
fun getHistoryByMangaId() =
"""
SELECT ${History.TABLE}.*
FROM ${History.TABLE}
JOIN ${Chapter.TABLE}
ON ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
"""
fun getHistoryByAnimeId() =
"""
SELECT ${AnimeHistory.TABLE}.*
FROM ${AnimeHistory.TABLE}
JOIN ${Episode.TABLE}
ON ${AnimeHistory.TABLE}.${AnimeHistory.COL_EPISODE_ID} = ${Episode.TABLE}.${Episode.COL_ID}
WHERE ${Episode.TABLE}.${Episode.COL_ANIME_ID} = ? AND ${AnimeHistory.TABLE}.${AnimeHistory.COL_EPISODE_ID} = ${Episode.TABLE}.${Episode.COL_ID}
"""
fun getHistoryByChapterUrl() =
"""
SELECT ${History.TABLE}.*
FROM ${History.TABLE}
JOIN ${Chapter.TABLE}
ON ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
WHERE ${Chapter.TABLE}.${Chapter.COL_URL} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
"""
fun getHistoryByEpisodeUrl() =
"""
SELECT ${AnimeHistory.TABLE}.*
FROM ${AnimeHistory.TABLE}
JOIN ${Episode.TABLE}
ON ${AnimeHistory.TABLE}.${AnimeHistory.COL_EPISODE_ID} = ${Episode.TABLE}.${Episode.COL_ID}
WHERE ${Episode.TABLE}.${Episode.COL_URL} = ? AND ${AnimeHistory.TABLE}.${AnimeHistory.COL_EPISODE_ID} = ${Episode.TABLE}.${Episode.COL_ID}
"""
fun getLastReadMangaQuery() =
"""
SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max

View file

@ -1,52 +0,0 @@
package eu.kanade.tachiyomi.data.database.resolvers
import androidx.annotation.NonNull
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.Query
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.mappers.AnimeHistoryPutResolver
import eu.kanade.tachiyomi.data.database.models.AnimeHistory
import eu.kanade.tachiyomi.data.database.tables.AnimeHistoryTable
class AnimeHistoryUpsertResolver : AnimeHistoryPutResolver() {
/**
* Updates last_read time of chapter
*/
override fun performPut(@NonNull db: StorIOSQLite, @NonNull history: AnimeHistory): PutResult = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(history)
val cursor = db.lowLevel().query(
Query.builder()
.table(updateQuery.table())
.where(updateQuery.where())
.whereArgs(updateQuery.whereArgs())
.build(),
)
cursor.use { putCursor ->
if (putCursor.count == 0) {
val insertQuery = mapToInsertQuery(history)
val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history))
PutResult.newInsertResult(insertedId, insertQuery.table())
} else {
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, mapToUpdateContentValues(history))
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
}
}
override fun mapToUpdateQuery(obj: AnimeHistory) = UpdateQuery.builder()
.table(AnimeHistoryTable.TABLE)
.where("${AnimeHistoryTable.COL_EPISODE_ID} = ?")
.whereArgs(obj.episode_id)
.build()
private fun mapToUpdateContentValues(history: AnimeHistory) =
contentValuesOf(
AnimeHistoryTable.COL_LAST_SEEN to history.last_seen,
)
}

View file

@ -1,34 +0,0 @@
package eu.kanade.tachiyomi.data.database.resolvers
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
class ChapterBackupPutResolver : PutResolver<Chapter>() {
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(chapter)
val contentValues = mapToContentValues(chapter)
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ?")
.whereArgs(chapter.url)
.build()
fun mapToContentValues(chapter: Chapter) =
contentValuesOf(
ChapterTable.COL_READ to chapter.read,
ChapterTable.COL_BOOKMARK to chapter.bookmark,
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read,
)
}

View file

@ -1,34 +0,0 @@
package eu.kanade.tachiyomi.data.database.resolvers
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
class ChapterKnownBackupPutResolver : PutResolver<Chapter>() {
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(chapter)
val contentValues = mapToContentValues(chapter)
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_ID} = ?")
.whereArgs(chapter.id)
.build()
fun mapToContentValues(chapter: Chapter) =
contentValuesOf(
ChapterTable.COL_READ to chapter.read,
ChapterTable.COL_BOOKMARK to chapter.bookmark,
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read,
)
}

View file

@ -1,35 +0,0 @@
package eu.kanade.tachiyomi.data.database.resolvers
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Episode
import eu.kanade.tachiyomi.data.database.tables.EpisodeTable
class EpisodeBackupPutResolver : PutResolver<Episode>() {
override fun performPut(db: StorIOSQLite, episode: Episode) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(episode)
val contentValues = mapToContentValues(episode)
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(episode: Episode) = UpdateQuery.builder()
.table(EpisodeTable.TABLE)
.where("${EpisodeTable.COL_URL} = ?")
.whereArgs(episode.url)
.build()
fun mapToContentValues(episode: Episode) =
contentValuesOf(
EpisodeTable.COL_SEEN to episode.seen,
EpisodeTable.COL_BOOKMARK to episode.bookmark,
EpisodeTable.COL_LAST_SECOND_SEEN to episode.last_second_seen,
EpisodeTable.COL_TOTAL_SECONDS to episode.total_seconds,
)
}

View file

@ -1,35 +0,0 @@
package eu.kanade.tachiyomi.data.database.resolvers
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Episode
import eu.kanade.tachiyomi.data.database.tables.EpisodeTable
class EpisodeKnownBackupPutResolver : PutResolver<Episode>() {
override fun performPut(db: StorIOSQLite, episode: Episode) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(episode)
val contentValues = mapToContentValues(episode)
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(episode: Episode) = UpdateQuery.builder()
.table(EpisodeTable.TABLE)
.where("${EpisodeTable.COL_ID} = ?")
.whereArgs(episode.id)
.build()
fun mapToContentValues(episode: Episode) =
contentValuesOf(
EpisodeTable.COL_SEEN to episode.seen,
EpisodeTable.COL_BOOKMARK to episode.bookmark,
EpisodeTable.COL_LAST_SECOND_SEEN to episode.last_second_seen,
EpisodeTable.COL_TOTAL_SECONDS to episode.total_seconds,
)
}

View file

@ -1,52 +0,0 @@
package eu.kanade.tachiyomi.data.database.resolvers
import androidx.annotation.NonNull
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.Query
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.mappers.HistoryPutResolver
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
class HistoryUpsertResolver : HistoryPutResolver() {
/**
* Updates last_read time of chapter
*/
override fun performPut(@NonNull db: StorIOSQLite, @NonNull history: History): PutResult = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(history)
val cursor = db.lowLevel().query(
Query.builder()
.table(updateQuery.table())
.where(updateQuery.where())
.whereArgs(updateQuery.whereArgs())
.build(),
)
cursor.use { putCursor ->
if (putCursor.count == 0) {
val insertQuery = mapToInsertQuery(history)
val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history))
PutResult.newInsertResult(insertedId, insertQuery.table())
} else {
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, mapToUpdateContentValues(history))
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
}
}
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
.table(HistoryTable.TABLE)
.where("${HistoryTable.COL_CHAPTER_ID} = ?")
.whereArgs(obj.chapter_id)
.build()
private fun mapToUpdateContentValues(history: History) =
contentValuesOf(
HistoryTable.COL_LAST_READ to history.last_read,
)
}

View file

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.source
import android.graphics.drawable.Drawable
import eu.kanade.domain.source.model.SourceData
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
@ -105,3 +106,18 @@ fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSourc
fun Source.getPreferenceKey(): String = "source_$id"
fun Source.toSourceData(): SourceData = SourceData(id = id, lang = lang, name = name)
fun Source.getNameForMangaInfo(): String {
val preferences = Injekt.get<PreferencesHelper>()
val enabledLanguages = preferences.enabledLanguages().get()
.filterNot { it in listOf("all", "other") }
val hasOneActiveLanguages = enabledLanguages.size == 1
val isInEnabledLanguages = lang in enabledLanguages
return when {
// For edge cases where user disables a source they got manga of in their library.
hasOneActiveLanguages && !isInEnabledLanguages -> toString()
// Hide the language tag when only one language is used.
hasOneActiveLanguages && isInEnabledLanguages -> name
else -> toString()
}
}

View file

@ -40,7 +40,7 @@ class AddDuplicateAnimeDialog(bundle: Bundle? = null) : DialogController(bundle)
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(activity?.getString(R.string.action_show_anime)) { _, _ ->
dismissDialog()
router.pushController(AnimeController(libraryAnime))
router.pushController(AnimeController(libraryAnime.id!!))
}
.setCancelable(true)
.create()

View file

@ -28,7 +28,6 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import coil.imageLoader
import coil.request.ImageRequest
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
@ -40,6 +39,7 @@ import eu.kanade.data.episode.NoEpisodesException
import eu.kanade.domain.animehistory.interactor.UpsertAnimeHistory
import eu.kanade.domain.animehistory.model.AnimeHistoryUpdate
import eu.kanade.domain.animehistory.model.AnimeHistoryWithRelations
import eu.kanade.domain.category.model.toDbCategory
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
@ -99,6 +99,7 @@ import eu.kanade.tachiyomi.util.hasCustomCover
import eu.kanade.tachiyomi.util.lang.awaitSingle
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast
@ -111,6 +112,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import logcat.LogPriority
import reactivecircus.flowbinding.recyclerview.scrollStateChanges
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
@ -135,22 +137,18 @@ class AnimeController :
constructor(history: AnimeHistoryWithRelations) : this(history.animeId)
constructor(anime: Anime?, fromSource: Boolean = false) : super(
constructor(animeId: Long, fromSource: Boolean = false) : super(
bundleOf(
ANIME_EXTRA to (anime?.id ?: 0),
ANIME_EXTRA to animeId,
FROM_SOURCE_EXTRA to fromSource,
),
) {
this.anime = anime
if (anime != null) {
source = Injekt.get<AnimeSourceManager>().getOrStub(anime.source)
this.anime = Injekt.get<AnimeDatabaseHelper>().getAnime(animeId).executeAsBlocking()
if (this.anime != null) {
source = Injekt.get<AnimeSourceManager>().getOrStub(this.anime!!.source)
}
}
constructor(animeId: Long) : this(
Injekt.get<AnimeDatabaseHelper>().getAnime(animeId).executeAsBlocking(),
)
@Suppress("unused")
constructor(bundle: Bundle) : this(bundle.getLong(ANIME_EXTRA))
@ -196,8 +194,6 @@ class AnimeController :
private var trackSheet: TrackSheet? = null
private var dialog: DialogController? = null
/**
* For [recyclerViewUpdatesToolbarTitleAlpha]
*/
@ -220,8 +216,10 @@ class AnimeController :
super.onChangeStarted(handler, type)
// Hide toolbar title on enter
// No need to update alpha for cover dialog
if (dialog == null) {
updateToolbarTitleAlpha(if (type.isEnter) 0F else 1F)
if (!type.isEnter) {
if (!type.isPush || router.backstack.lastOrNull()?.controller !is DialogController) {
updateToolbarTitleAlpha(1f)
}
}
recyclerViewUpdatesToolbarTitleAlpha(type.isEnter)
}
@ -319,13 +317,9 @@ class AnimeController :
}
.launchIn(viewScope)
settingsSheet = EpisodesSettingsSheet(router, presenter) { group ->
if (group is EpisodesSettingsSheet.Filter.FilterGroup) {
updateFilterIconState()
}
}
settingsSheet = EpisodesSettingsSheet(router, presenter)
trackSheet = TrackSheet(this, anime!!, (activity as MainActivity).supportFragmentManager)
trackSheet = TrackSheet(this, (activity as MainActivity).supportFragmentManager)
updateFilterIconState()
recyclerViewUpdatesToolbarTitleAlpha(true)
@ -442,13 +436,16 @@ class AnimeController :
}
override fun onPrepareOptionsMenu(menu: Menu) {
// Hide options for local anime
menu.findItem(R.id.action_share).isVisible = !isLocalSource
menu.findItem(R.id.download_group).isVisible = !isLocalSource
runBlocking {
// Hide options for local anime
menu.findItem(R.id.action_share).isVisible = !isLocalSource
menu.findItem(R.id.download_group).isVisible = !isLocalSource
// Hide options for non-animelib anime
menu.findItem(R.id.action_edit_categories).isVisible = presenter.anime.favorite && presenter.getCategories().isNotEmpty()
menu.findItem(R.id.action_migrate).isVisible = presenter.anime.favorite
// Hide options for non-animelib anime
menu.findItem(R.id.action_edit_categories).isVisible =
presenter.anime.favorite && presenter.getCategories().isNotEmpty()
menu.findItem(R.id.action_migrate).isVisible = presenter.anime.favorite
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -558,12 +555,17 @@ class AnimeController :
activity?.toast(activity?.getString(R.string.manga_removed_library))
activity?.invalidateOptionsMenu()
} else {
val duplicateAnime = presenter.getDuplicateAnimelibAnime(anime)
if (duplicateAnime != null) {
AddDuplicateAnimeDialog(this, duplicateAnime) { addToAnimelib(anime) }
.showDialog(router)
} else {
addToAnimelib(anime)
launchIO {
val duplicateAnime = presenter.getDuplicateLibraryAnime(anime)
withUIContext {
if (duplicateAnime != null) {
AddDuplicateAnimeDialog(this@AnimeController, duplicateAnime) { addToLibrary(anime) }
.showDialog(router)
} else {
addToLibrary(anime)
}
}
}
}
}
@ -572,40 +574,48 @@ class AnimeController :
trackSheet?.show()
}
private fun addToAnimelib(newAnime: Anime) {
val categories = presenter.getCategories()
val defaultCategoryId = preferences.defaultAnimeCategory()
val defaultCategory = categories.find { it.id == defaultCategoryId }
private fun addToLibrary(newAnime: Anime) {
launchIO {
val categories = presenter.getCategories()
val defaultCategoryId = preferences.defaultAnimeCategory()
val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
when {
// Default category set
defaultCategory != null -> {
toggleFavorite()
presenter.moveAnimeToCategory(newAnime, defaultCategory)
activity?.toast(activity?.getString(R.string.manga_added_library))
activity?.invalidateOptionsMenu()
}
// Automatic 'Default' or no categories
defaultCategoryId == 0 || categories.isEmpty() -> {
toggleFavorite()
presenter.moveAnimeToCategory(newAnime, null)
activity?.toast(activity?.getString(R.string.manga_added_library))
activity?.invalidateOptionsMenu()
}
// Choose a category
else -> {
val ids = presenter.getAnimeCategoryIds(newAnime)
val preselected = categories.map {
if (it.id!! in ids) {
QuadStateTextView.State.CHECKED.ordinal
} else {
QuadStateTextView.State.UNCHECKED.ordinal
withUIContext {
when {
// Default category set
defaultCategory != null -> {
toggleFavorite()
presenter.moveAnimeToCategory(newAnime, defaultCategory.toDbCategory())
activity?.toast(activity?.getString(R.string.manga_added_library))
activity?.invalidateOptionsMenu()
}
}.toIntArray()
showChangeCategoryDialog(newAnime, categories, preselected)
// Automatic 'Default' or no categories
defaultCategoryId == 0 || categories.isEmpty() -> {
toggleFavorite()
presenter.moveAnimeToCategory(newAnime, null)
activity?.toast(activity?.getString(R.string.manga_added_library))
activity?.invalidateOptionsMenu()
}
// Choose a category
else -> {
val ids = presenter.getAnimeCategoryIds(newAnime)
val preselected = categories.map {
if (it.id in ids) {
QuadStateTextView.State.CHECKED.ordinal
} else {
QuadStateTextView.State.UNCHECKED.ordinal
}
}.toTypedArray()
showChangeCategoryDialog(
newAnime,
categories.map { it.toDbCategory() },
preselected,
)
}
}
}
}
@ -647,33 +657,32 @@ class AnimeController :
}
fun onCategoriesClick() {
val anime = presenter.anime
val categories = presenter.getCategories()
launchIO {
val anime = presenter.anime
val categories = presenter.getCategories()
val ids = presenter.getAnimeCategoryIds(anime)
val preselected = categories.map {
if (it.id!! in ids) {
QuadStateTextView.State.CHECKED.ordinal
} else {
QuadStateTextView.State.UNCHECKED.ordinal
if (categories.isEmpty()) {
return@launchIO
}
}.toIntArray()
showChangeCategoryDialog(anime, categories, preselected)
val ids = presenter.getAnimeCategoryIds(anime)
val preselected = categories.map {
if (it.id in ids) {
QuadStateTextView.State.CHECKED.ordinal
} else {
QuadStateTextView.State.UNCHECKED.ordinal
}
}.toTypedArray()
withUIContext {
showChangeCategoryDialog(anime, categories.map { it.toDbCategory() }, preselected)
}
}
}
private fun showChangeCategoryDialog(anime: Anime, categories: List<Category>, preselected: IntArray) {
if (dialog != null) return
dialog = ChangeAnimeCategoriesDialog(this, listOf(anime), categories, preselected)
dialog?.addLifecycleListener(
object : LifecycleListener() {
override fun postDestroy(controller: Controller) {
super.postDestroy(controller)
dialog = null
}
},
)
dialog?.showDialog(router)
private fun showChangeCategoryDialog(anime: Anime, categories: List<Category>, preselected: Array<Int>) {
ChangeAnimeCategoriesDialog(this, listOf(anime), categories, preselected.toIntArray())
.showDialog(router)
}
override fun updateCategoriesForAnimes(animes: List<Anime>, addCategories: List<Category>, removeCategories: List<Category>) {
@ -773,18 +782,9 @@ class AnimeController :
}
fun showFullCoverDialog() {
if (dialog != null) return
val anime = anime ?: return
dialog = AnimeFullCoverDialog(this, anime)
dialog?.addLifecycleListener(
object : LifecycleListener() {
override fun postDestroy(controller: Controller) {
super.postDestroy(controller)
dialog = null
}
},
)
dialog?.showDialog(router)
AnimeFullCoverDialog(this, anime)
.showDialog(router)
}
fun shareCover() {
@ -874,7 +874,7 @@ class AnimeController :
val dataUri = data?.data
if (dataUri == null || resultCode != Activity.RESULT_OK) return
val activity = activity ?: return
presenter.editCover(anime!!, activity, dataUri)
presenter.editCover(activity, dataUri)
}
if (requestCode == REQUEST_EXTERNAL && resultCode == Activity.RESULT_OK) {
val anime = EXT_ANIME ?: return
@ -917,7 +917,7 @@ class AnimeController :
fun onSetCoverSuccess() {
animeInfoAdapter?.notifyItemChanged(0, this)
(dialog as? AnimeFullCoverDialog)?.setImage(anime)
(router.backstack.lastOrNull()?.controller as? AnimeFullCoverDialog)?.setImage(anime)
activity?.toast(R.string.cover_updated)
}
@ -965,6 +965,7 @@ class AnimeController :
}
updateFabVisibility()
updateFilterIconState()
}
private fun fetchEpisodesFromSource(manualFetch: Boolean = false) {
@ -1263,7 +1264,7 @@ class AnimeController :
addSnackbar = (activity as? MainActivity)?.binding?.rootCoordinator?.snack(view.context.getString(R.string.snack_add_to_animelib)) {
setAction(R.string.action_add) {
if (!anime.favorite) {
addToAnimelib(anime)
addToLibrary(anime)
}
}
}
@ -1284,7 +1285,7 @@ class AnimeController :
addSnackbar = (activity as? MainActivity)?.binding?.rootCoordinator?.snack(view.context.getString(R.string.snack_add_to_animelib)) {
setAction(R.string.action_add) {
if (!anime.favorite) {
addToAnimelib(anime)
addToLibrary(anime)
}
}
}

View file

@ -8,8 +8,12 @@ import android.os.Bundle
import coil.imageLoader
import coil.memory.MemoryCache
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.domain.anime.interactor.GetDuplicateLibraryAnime
import eu.kanade.domain.anime.model.toDbAnime
import eu.kanade.domain.category.interactor.GetCategoriesAnime
import eu.kanade.domain.episode.interactor.GetEpisodeByAnimeId
import eu.kanade.domain.episode.model.toDbEpisode
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.LocalAnimeSource
import eu.kanade.tachiyomi.animesource.model.toSAnime
import eu.kanade.tachiyomi.animesource.model.toSEpisode
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
@ -20,6 +24,7 @@ import eu.kanade.tachiyomi.data.database.models.AnimeTrack
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Episode
import eu.kanade.tachiyomi.data.database.models.toAnimeInfo
import eu.kanade.tachiyomi.data.database.models.toDomainAnime
import eu.kanade.tachiyomi.data.download.AnimeDownloadManager
import eu.kanade.tachiyomi.data.download.model.AnimeDownload
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -32,12 +37,14 @@ import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.ui.anime.episode.EpisodeItem
import eu.kanade.tachiyomi.ui.anime.track.TrackItem
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.editCover
import eu.kanade.tachiyomi.util.episode.EpisodeSettingsHelper
import eu.kanade.tachiyomi.util.episode.getEpisodeSort
import eu.kanade.tachiyomi.util.episode.syncEpisodesWithSource
import eu.kanade.tachiyomi.util.episode.syncEpisodesWithTrackServiceTwoWay
import eu.kanade.tachiyomi.util.isLocal
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.prepUpdateCover
import eu.kanade.tachiyomi.util.removeCovers
@ -49,6 +56,8 @@ import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Stat
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.supervisorScope
import logcat.LogPriority
import rx.Observable
@ -59,6 +68,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.Date
import eu.kanade.domain.category.model.Category as DomainCategory
class AnimePresenter(
val anime: Anime,
@ -68,7 +78,9 @@ class AnimePresenter(
private val trackManager: TrackManager = Injekt.get(),
private val downloadManager: AnimeDownloadManager = Injekt.get(),
private val coverCache: AnimeCoverCache = Injekt.get(),
// private val getEpisodeByAnimeId: GetEpisodeByAnimeId = Injekt.get(),
private val getEpisodeByAnimeId: GetEpisodeByAnimeId = Injekt.get(),
private val getDuplicateLibraryAnime: GetDuplicateLibraryAnime = Injekt.get(),
private val getCategories: GetCategoriesAnime = Injekt.get(),
) : BasePresenter<AnimeController>() {
/**
@ -155,32 +167,26 @@ class AnimePresenter(
// Episodes list - start
// Keeps subscribed to changes and sends the list of chapters to the relay.
add(
db.getEpisodes(anime).asRxObservable()
.map { episodes ->
// Convert every episode to a model.
episodes.map { it.toModel() }
}
.doOnNext { episodes ->
// Find downloaded episodes
setDownloadedEpisodes(episodes)
// Store the last emission
this.allEpisodes = episodes
// Listen for download status changes
observeDownloads()
}
.subscribe { episodesRelay.call(it) },
)
presenterScope.launchIO {
anime.id?.let { animeId ->
getEpisodeByAnimeId.subscribe(animeId)
.collectLatest { domainEpisodes ->
val episodeItems = domainEpisodes.map { it.toDbEpisode().toModel() }
setDownloadedEpisodes(episodeItems)
this@AnimePresenter.allEpisodes = episodeItems
observeDownloads()
episodesRelay.call(episodeItems)
}
}
}
// Episodes list - end
fetchTrackers()
}
fun getDuplicateAnimelibAnime(anime: Anime): Anime? {
return db.getDuplicateAnimelibAnime(anime).executeAsBlocking()
suspend fun getDuplicateLibraryAnime(anime: Anime): Anime? {
return getDuplicateLibraryAnime.await(anime.title, anime.source)?.toDbAnime()
}
// Anime info - start
@ -264,8 +270,8 @@ class AnimePresenter(
*
* @return List of categories, not including the default category
*/
fun getCategories(): List<Category> {
return db.getCategories().executeAsBlocking()
suspend fun getCategories(): List<DomainCategory> {
return getCategories.subscribe().firstOrNull() ?: emptyList()
}
/**
@ -274,9 +280,9 @@ class AnimePresenter(
* @param anime the anime to get categories from.
* @return Array of category ids the anime is in, if none returns default id
*/
fun getAnimeCategoryIds(anime: Anime): IntArray {
fun getAnimeCategoryIds(anime: Anime): Array<Long> {
val categories = db.getCategoriesForAnime(anime).executeAsBlocking()
return categories.mapNotNull { it.id }.toIntArray()
return categories.mapNotNull { it?.id?.toLong() }.toTypedArray()
}
/**
@ -355,31 +361,20 @@ class AnimePresenter(
/**
* Update cover with local file.
*
* @param anime the anime edited.
* @param context Context.
* @param data uri of the cover resource.
*/
fun editCover(anime: Anime, context: Context, data: Uri) {
Observable
.fromCallable {
context.contentResolver.openInputStream(data)?.use {
if (anime.isLocal()) {
LocalAnimeSource.updateCover(context, anime, it)
anime.updateCoverLastModified(db)
db.insertAnime(anime).executeAsBlocking()
} else if (anime.favorite) {
coverCache.setCustomCoverToCache(anime, it)
anime.updateCoverLastModified(db)
}
true
fun editCover(context: Context, data: Uri) {
presenterScope.launchIO {
context.contentResolver.openInputStream(data)?.use {
try {
val result = anime.toDomainAnime()!!.editCover(context, it)
launchUI { if (result) view?.onSetCoverSuccess() }
} catch (e: Exception) {
launchUI { view?.onSetCoverError(e) }
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeFirst(
{ view, _ -> view.onSetCoverSuccess() },
{ view, e -> view.onSetCoverError(e) },
)
}
}
fun deleteCustomCover(anime: Anime) {
@ -505,10 +500,10 @@ class AnimePresenter(
private fun applyEpisodeFilters(episodes: List<EpisodeItem>): Observable<List<EpisodeItem>> {
var observable = Observable.from(episodes).subscribeOn(Schedulers.io())
val unreadFilter = onlyUnread()
if (unreadFilter == State.INCLUDE) {
val unseenFilter = onlyUnseen()
if (unseenFilter == State.INCLUDE) {
observable = observable.filter { !it.seen }
} else if (unreadFilter == State.EXCLUDE) {
} else if (unseenFilter == State.EXCLUDE) {
observable = observable.filter { it.seen }
}
@ -599,27 +594,6 @@ class AnimePresenter(
}
}
/**
* Mark the selected episode list as read/unread.
* @param selectedEpisodes the list of selected episodes.
* @param read whether to mark episodes as read or unread.
*/
fun setEpisodesProgress(selectedEpisodes: List<EpisodeItem>) {
val episodes = selectedEpisodes.map { episode ->
val progress = preferences.progressPreference()
if (!episode.seen) episode.seen = episode.last_second_seen >= episode.total_seconds * progress
episode
}
launchIO {
db.updateEpisodesProgress(episodes).executeAsBlocking()
if (preferences.removeAfterMarkedAsRead()) {
deleteEpisodes(episodes.filter { it.seen })
}
}
}
/**
* Downloads the given list of episodes with the manager.
* @param episodes the list of episodes to download.
@ -691,10 +665,10 @@ class AnimePresenter(
}
/**
* Sets the read filter and requests an UI update.
* @param state whether to display only unread episodes or all episodes.
* Sets the seen filter and requests an UI update.
* @param state whether to display only unseen episodes or all episodes.
*/
fun setUnreadFilter(state: State) {
fun setUnseenFilter(state: State) {
anime.seenFilter = when (state) {
State.IGNORE -> Anime.SHOW_ALL
State.INCLUDE -> Anime.EPISODE_SHOW_UNSEEN
@ -755,14 +729,14 @@ class AnimePresenter(
/**
* Whether downloaded only mode is enabled.
*/
fun forceDownloaded(): Boolean {
private fun forceDownloaded(): Boolean {
return anime.favorite && preferences.downloadedOnly().get()
}
/**
* Whether the display only downloaded filter is enabled.
*/
fun onlyDownloaded(): State {
private fun onlyDownloaded(): State {
if (forceDownloaded()) {
return State.INCLUDE
}
@ -776,7 +750,7 @@ class AnimePresenter(
/**
* Whether the display only bookmarked filter is enabled.
*/
fun onlyBookmarked(): State {
private fun onlyBookmarked(): State {
return when (anime.bookmarkedFilter) {
Anime.EPISODE_SHOW_BOOKMARKED -> State.INCLUDE
Anime.EPISODE_SHOW_NOT_BOOKMARKED -> State.EXCLUDE
@ -787,7 +761,7 @@ class AnimePresenter(
/**
* Whether the display only unread filter is enabled.
*/
fun onlyUnread(): State {
private fun onlyUnseen(): State {
return when (anime.seenFilter) {
Anime.EPISODE_SHOW_UNSEEN -> State.INCLUDE
Anime.EPISODE_SHOW_SEEN -> State.EXCLUDE

View file

@ -6,35 +6,51 @@ import android.util.AttributeSet
import android.view.View
import androidx.core.view.isVisible
import com.bluelinelabs.conductor.Router
import eu.kanade.domain.anime.model.Anime
import eu.kanade.domain.anime.model.toTriStateGroupState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Anime
import eu.kanade.tachiyomi.data.database.models.toDomainAnime
import eu.kanade.tachiyomi.ui.anime.AnimePresenter
import eu.kanade.tachiyomi.util.view.popupMenu
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
import eu.kanade.tachiyomi.widget.sheet.TabbedBottomSheetDialog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
class EpisodesSettingsSheet(
private val router: Router,
private val presenter: AnimePresenter,
private val onGroupClickListener: (ExtendedNavigationView.Group) -> Unit,
) : TabbedBottomSheetDialog(router.activity!!) {
val filters = Filter(router.activity!!)
private val sort = Sort(router.activity!!)
private val display = Display(router.activity!!)
private lateinit var scope: CoroutineScope
private var anime: Anime? = null
val filters = Filter(context)
private val sort = Sort(context)
private val display = Display(context)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
filters.onGroupClicked = onGroupClickListener
sort.onGroupClicked = onGroupClickListener
display.onGroupClicked = onGroupClickListener
binding.menu.isVisible = true
binding.menu.setOnClickListener { it.post { showPopupMenu(it) } }
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
scope = MainScope()
// TODO: Listen to changes
updateAnime()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
scope.cancel()
}
override fun getTabViews(): List<View> = listOf(
filters,
sort,
@ -47,6 +63,10 @@ class EpisodesSettingsSheet(
R.string.action_display,
)
private fun updateAnime() {
anime = presenter.anime.toDomainAnime()
}
private fun showPopupMenu(view: View) {
view.popupMenu(
menuRes = R.menu.default_chapter_filter,
@ -79,25 +99,35 @@ class EpisodesSettingsSheet(
return filterGroup.items.any { it.state != State.IGNORE.value }
}
override fun updateView() {
filterGroup.updateModels()
}
inner class FilterGroup : Group {
private val downloaded = Item.TriStateGroup(R.string.action_filter_downloaded, this)
private val unread = Item.TriStateGroup(R.string.action_filter_unseen, this)
private val unseen = Item.TriStateGroup(R.string.action_filter_unseen, this)
private val bookmarked = Item.TriStateGroup(R.string.action_filter_bookmarked, this)
override val header: Item? = null
override val items = listOf(downloaded, unread, bookmarked)
override val items = listOf(downloaded, unseen, bookmarked)
override val footer: Item? = null
override fun initModels() {
if (presenter.forceDownloaded()) {
val anime = anime ?: return
if (anime.forceDownloaded()) {
downloaded.state = State.INCLUDE.value
downloaded.enabled = false
} else {
downloaded.state = presenter.onlyDownloaded().value
downloaded.state = anime.downloadedFilter.toTriStateGroupState().value
}
unread.state = presenter.onlyUnread().value
bookmarked.state = presenter.onlyBookmarked().value
unseen.state = anime.unseenFilter.toTriStateGroupState().value
bookmarked.state = anime.bookmarkedFilter.toTriStateGroupState().value
}
fun updateModels() {
initModels()
adapter.notifyItemRangeChanged(0, 3)
}
override fun onItemClicked(item: Item) {
@ -108,16 +138,16 @@ class EpisodesSettingsSheet(
State.EXCLUDE.value -> State.IGNORE
else -> throw Exception("Unknown State")
}
item.state = newState.value
when (item) {
downloaded -> presenter.setDownloadedFilter(newState)
unread -> presenter.setUnreadFilter(newState)
unseen -> presenter.setUnseenFilter(newState)
bookmarked -> presenter.setBookmarkedFilter(newState)
else -> {}
}
initModels()
adapter.notifyItemChanged(items.indexOf(item), item)
// TODO: Remove
updateAnime()
updateView()
}
}
}
@ -128,14 +158,20 @@ class EpisodesSettingsSheet(
inner class Sort @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
Settings(context, attrs) {
private val group = SortGroup()
init {
setGroups(listOf(SortGroup()))
setGroups(listOf(group))
}
override fun updateView() {
group.updateModels()
}
inner class SortGroup : Group {
private val source = Item.MultiSort(R.string.sort_by_source, this)
private val episodeNum = Item.MultiSort(R.string.sort_by_episode_number, this)
private val episodeNum = Item.MultiSort(R.string.sort_by_number, this)
private val uploadDate = Item.MultiSort(R.string.sort_by_upload_date, this)
override val header: Item? = null
@ -143,8 +179,9 @@ class EpisodesSettingsSheet(
override val footer: Item? = null
override fun initModels() {
val sorting = presenter.anime.sorting
val order = if (presenter.anime.sortDescending()) {
val anime = anime ?: return
val sorting = anime.sorting
val order = if (anime.sortDescending()) {
Item.MultiSort.SORT_DESC
} else {
Item.MultiSort.SORT_ASC
@ -158,29 +195,23 @@ class EpisodesSettingsSheet(
if (sorting == Anime.EPISODE_SORTING_UPLOAD_DATE) order else Item.MultiSort.SORT_NONE
}
override fun onItemClicked(item: Item) {
items.forEachIndexed { i, multiSort ->
multiSort.state = if (multiSort == item) {
when (item.state) {
Item.MultiSort.SORT_NONE -> Item.MultiSort.SORT_ASC
Item.MultiSort.SORT_ASC -> Item.MultiSort.SORT_DESC
Item.MultiSort.SORT_DESC -> Item.MultiSort.SORT_ASC
else -> throw Exception("Unknown state")
}
} else {
Item.MultiSort.SORT_NONE
}
adapter.notifyItemChanged(i, multiSort)
}
fun updateModels() {
initModels()
adapter.notifyItemRangeChanged(0, 3)
}
override fun onItemClicked(item: Item) {
when (item) {
source -> presenter.setSorting(Anime.EPISODE_SORTING_SOURCE)
episodeNum -> presenter.setSorting(Anime.EPISODE_SORTING_NUMBER)
uploadDate -> presenter.setSorting(Anime.EPISODE_SORTING_UPLOAD_DATE)
source -> presenter.setSorting(Anime.EPISODE_SORTING_SOURCE.toInt())
episodeNum -> presenter.setSorting(Anime.EPISODE_SORTING_NUMBER.toInt())
uploadDate -> presenter.setSorting(Anime.EPISODE_SORTING_UPLOAD_DATE.toInt())
else -> throw Exception("Unknown sorting")
}
// TODO: Remove
presenter.reverseSortOrder()
updateAnime()
updateView()
}
}
}
@ -191,8 +222,14 @@ class EpisodesSettingsSheet(
inner class Display @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
Settings(context, attrs) {
private val group = DisplayGroup()
init {
setGroups(listOf(DisplayGroup()))
setGroups(listOf(group))
}
override fun updateView() {
group.updateModels()
}
inner class DisplayGroup : Group {
@ -205,25 +242,29 @@ class EpisodesSettingsSheet(
override val footer: Item? = null
override fun initModels() {
val mode = presenter.anime.displayMode
val mode = anime?.displayMode ?: return
displayTitle.checked = mode == Anime.EPISODE_DISPLAY_NAME
displayEpisodeNum.checked = mode == Anime.EPISODE_DISPLAY_NUMBER
}
fun updateModels() {
initModels()
adapter.notifyItemRangeChanged(0, 2)
}
override fun onItemClicked(item: Item) {
item as Item.Radio
if (item.checked) return
items.forEachIndexed { index, radio ->
radio.checked = item == radio
adapter.notifyItemChanged(index, radio)
}
when (item) {
displayTitle -> presenter.setDisplayMode(Anime.EPISODE_DISPLAY_NAME)
displayEpisodeNum -> presenter.setDisplayMode(Anime.EPISODE_DISPLAY_NUMBER)
displayTitle -> presenter.setDisplayMode(Anime.EPISODE_DISPLAY_NAME.toInt())
displayEpisodeNum -> presenter.setDisplayMode(Anime.EPISODE_DISPLAY_NUMBER.toInt())
else -> throw NotImplementedError("Unknown display mode")
}
// TODO: Remove
updateAnime()
updateView()
}
}
}
@ -246,6 +287,9 @@ class EpisodesSettingsSheet(
addView(recycler)
}
open fun updateView() {
}
/**
* Adapter of the recycler view.
*/

View file

@ -10,6 +10,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
import eu.kanade.tachiyomi.animesource.getNameForAnimeInfo
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.data.database.models.Anime
@ -100,7 +101,7 @@ class AnimeInfoHeaderAdapter(
.onEach { controller.onFavoriteClick() }
.launchIn(controller.viewScope)
if (controller.presenter.anime.favorite && controller.presenter.getCategories().isNotEmpty()) {
if (controller.presenter.anime.favorite) {
binding.btnFavorite.longClicks()
.onEach { controller.onCategoriesClick() }
.launchIn(controller.viewScope)
@ -247,20 +248,8 @@ class AnimeInfoHeaderAdapter(
// If anime source is known update source TextView.
binding.mangaMissingSourceIcon.isVisible = source is AnimeSourceManager.StubSource
val animeSource = source.toString()
with(binding.mangaSource) {
val enabledLanguages = preferences.enabledLanguages().get()
.filterNot { it in listOf("all", "other") }
val hasOneActiveLanguages = enabledLanguages.size == 1
val isInEnabledLanguages = source.lang in enabledLanguages
text = when {
// For edge cases where user disables a source they got anime of in their library.
hasOneActiveLanguages && !isInEnabledLanguages -> animeSource
// Hide the language tag when only one language is used.
hasOneActiveLanguages && isInEnabledLanguages -> source.name
else -> animeSource
}
text = source.getNameForAnimeInfo()
setOnClickListener {
controller.performSearch(sourceManager.getOrStub(source.id).name)
}

View file

@ -11,8 +11,6 @@ import com.google.android.material.datepicker.DateValidatorPointBackward
import com.google.android.material.datepicker.DateValidatorPointForward
import com.google.android.material.datepicker.MaterialDatePicker
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
import eu.kanade.tachiyomi.data.database.models.Anime
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
import eu.kanade.tachiyomi.databinding.TrackControllerBinding
import eu.kanade.tachiyomi.ui.anime.AnimeController
@ -24,14 +22,10 @@ import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class TrackSheet(
val controller: AnimeController,
val anime: Anime,
val fragmentManager: FragmentManager,
private val sourceManager: AnimeSourceManager = Injekt.get(),
) : BaseBottomSheetDialog(controller.activity!!),
TrackAdapter.OnClickListener,
SetTrackStatusDialog.Listener,
@ -80,6 +74,8 @@ class TrackSheet(
override fun onSetClick(position: Int) {
val item = adapter.getItem(position) ?: return
val anime = controller.presenter.anime
val source = controller.presenter.source
if (item.service is EnhancedTrackService) {
if (item.track != null) {
@ -87,7 +83,7 @@ class TrackSheet(
return
}
if (!item.service.accept(sourceManager.getOrStub(anime.source))) {
if (!item.service.accept(source)) {
controller.presenter.view?.applicationContext?.toast(R.string.source_unsupported)
return
}

View file

@ -493,7 +493,7 @@ class AnimelibController(
// Notify the presenter a anime is being opened.
presenter.onOpenAnime()
router.pushController(AnimeController(anime))
router.pushController(AnimeController(anime.id!!))
}
/**

View file

@ -21,6 +21,7 @@ import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.domain.animesource.model.AnimeSource
import eu.kanade.domain.category.model.toDbCategory
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.LocalAnimeSource
@ -42,6 +43,8 @@ import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.more.MoreController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.preference.asImmediateFlow
import eu.kanade.tachiyomi.util.system.connectivityManager
import eu.kanade.tachiyomi.util.system.logcat
@ -572,7 +575,7 @@ open class BrowseAnimeSourceController(bundle: Bundle) :
*/
override fun onItemClick(view: View, position: Int): Boolean {
val item = adapter?.getItem(position) as? AnimeSourceItem ?: return false
router.pushController(AnimeController(item.anime, true))
router.pushController(AnimeController(item.anime.id!!, true))
return false
}
@ -589,68 +592,87 @@ open class BrowseAnimeSourceController(bundle: Bundle) :
override fun onItemLongClick(position: Int) {
val activity = activity ?: return
val anime = (adapter?.getItem(position) as? AnimeSourceItem?)?.anime ?: return
val duplicateAnime = presenter.getDuplicateAnimelibAnime(anime)
launchIO {
val duplicateAnime = presenter.getDuplicateLibraryAnime(anime)
if (anime.favorite) {
MaterialAlertDialogBuilder(activity)
.setTitle(anime.title)
.setItems(arrayOf(activity.getString(R.string.remove_from_library))) { _, which ->
when (which) {
0 -> {
presenter.changeAnimeFavorite(anime)
adapter?.notifyItemChanged(position)
activity.toast(activity.getString(R.string.manga_removed_library))
withUIContext {
if (anime.favorite) {
MaterialAlertDialogBuilder(activity)
.setTitle(anime.title)
.setItems(arrayOf(activity.getString(R.string.remove_from_library))) { _, which ->
when (which) {
0 -> {
presenter.changeAnimeFavorite(anime)
adapter?.notifyItemChanged(position)
activity.toast(activity.getString(R.string.manga_removed_library))
}
}
}
.show()
} else {
if (duplicateAnime != null) {
AddDuplicateAnimeDialog(this@BrowseAnimeSourceController, duplicateAnime) {
addToLibrary(
anime,
position,
)
}
.showDialog(router)
} else {
addToLibrary(anime, position)
}
}
.show()
} else {
if (duplicateAnime != null) {
AddDuplicateAnimeDialog(this, duplicateAnime) { addToLibrary(anime, position) }
.showDialog(router)
} else {
addToLibrary(anime, position)
}
}
}
private fun addToLibrary(newAnime: Anime, position: Int) {
val activity = activity ?: return
val categories = presenter.getCategories()
val defaultCategoryId = preferences.defaultAnimeCategory()
val defaultCategory = categories.find { it.id == defaultCategoryId }
launchIO {
val categories = presenter.getCategories()
val defaultCategoryId = preferences.defaultAnimeCategory()
val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
when {
// Default category set
defaultCategory != null -> {
presenter.moveAnimeToCategory(newAnime, defaultCategory)
withUIContext {
when {
// Default category set
defaultCategory != null -> {
presenter.moveAnimeToCategory(newAnime, defaultCategory.toDbCategory())
presenter.changeAnimeFavorite(newAnime)
adapter?.notifyItemChanged(position)
activity.toast(activity.getString(R.string.manga_added_library))
}
// Automatic 'Default' or no categories
defaultCategoryId == 0 || categories.isEmpty() -> {
presenter.moveAnimeToCategory(newAnime, null)
presenter.changeAnimeFavorite(newAnime)
adapter?.notifyItemChanged(position)
activity.toast(activity.getString(R.string.manga_added_library))
}
// Choose a category
else -> {
val ids = presenter.getAnimeCategoryIds(newAnime)
val preselected = categories.map {
if (it.id in ids) {
QuadStateTextView.State.CHECKED.ordinal
} else {
QuadStateTextView.State.UNCHECKED.ordinal
presenter.changeAnimeFavorite(newAnime)
adapter?.notifyItemChanged(position)
activity.toast(activity.getString(R.string.manga_added_library))
}
}.toIntArray()
ChangeAnimeCategoriesDialog(this, listOf(newAnime), categories, preselected)
.showDialog(router)
// Automatic 'Default' or no categories
defaultCategoryId == 0 || categories.isEmpty() -> {
presenter.moveAnimeToCategory(newAnime, null)
presenter.changeAnimeFavorite(newAnime)
adapter?.notifyItemChanged(position)
activity.toast(activity.getString(R.string.manga_added_library))
}
// Choose a category
else -> {
val ids = presenter.getAnimeCategoryIds(newAnime)
val preselected = categories.map {
if (it.id in ids) {
QuadStateTextView.State.CHECKED.ordinal
} else {
QuadStateTextView.State.UNCHECKED.ordinal
}
}.toTypedArray()
ChangeAnimeCategoriesDialog(
this@BrowseAnimeSourceController,
listOf(newAnime),
categories.map { it.toDbCategory() },
preselected.toIntArray(),
)
.showDialog(router)
}
}
}
}
}

View file

@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.ui.browse.animesource.browse
import android.os.Bundle
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.domain.anime.interactor.GetDuplicateLibraryAnime
import eu.kanade.domain.anime.model.toDbAnime
import eu.kanade.domain.category.interactor.GetCategoriesAnime
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
@ -43,6 +46,7 @@ import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import logcat.LogPriority
@ -52,6 +56,7 @@ import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
import eu.kanade.domain.category.model.Category as DomainCategory
open class BrowseAnimeSourcePresenter(
private val sourceId: Long,
@ -60,6 +65,8 @@ open class BrowseAnimeSourcePresenter(
private val db: AnimeDatabaseHelper = Injekt.get(),
private val prefs: PreferencesHelper = Injekt.get(),
private val coverCache: AnimeCoverCache = Injekt.get(),
private val getDuplicateLibraryAnime: GetDuplicateLibraryAnime = Injekt.get(),
private val getCategories: GetCategoriesAnime = Injekt.get(),
) : BasePresenter<BrowseAnimeSourceController>() {
/**
@ -344,12 +351,12 @@ open class BrowseAnimeSourcePresenter(
*
* @return List of categories, not including the default category
*/
fun getCategories(): List<Category> {
return db.getCategories().executeAsBlocking()
suspend fun getCategories(): List<DomainCategory> {
return getCategories.subscribe().firstOrNull() ?: emptyList()
}
fun getDuplicateAnimelibAnime(anime: Anime): Anime? {
return db.getDuplicateAnimelibAnime(anime).executeAsBlocking()
suspend fun getDuplicateLibraryAnime(anime: Anime): Anime? {
return getDuplicateLibraryAnime.await(anime.title, anime.source)?.toDbAnime()
}
/**
@ -358,9 +365,9 @@ open class BrowseAnimeSourcePresenter(
* @param anime the anime to get categories from.
* @return Array of category ids the anime is in, if none returns default id
*/
fun getAnimeCategoryIds(anime: Anime): Array<Int?> {
fun getAnimeCategoryIds(anime: Anime): Array<Long?> {
val categories = db.getCategoriesForAnime(anime).executeAsBlocking()
return categories.mapNotNull { it.id }.toTypedArray()
return categories.mapNotNull { it?.id?.toLong() }.toTypedArray()
}
/**

View file

@ -77,7 +77,7 @@ open class GlobalAnimeSearchController(
* @param anime clicked item containing anime information.
*/
override fun onAnimeClick(anime: Anime) {
router.pushController(AnimeController(anime, true))
router.pushController(AnimeController(anime.id!!, true))
}
/**

View file

@ -84,7 +84,7 @@ class AnimeSearchController(
if (!isReplacingAnime) {
router.popController(this)
if (newAnime != null) {
val newAnimeController = RouterTransaction.with(AnimeController(newAnime))
val newAnimeController = RouterTransaction.with(AnimeController(newAnime.id!!))
if (router.backstack.lastOrNull()?.controller is AnimeController) {
// Replace old AnimeController
router.replaceTopController(newAnimeController)

View file

@ -84,7 +84,7 @@ class SearchController(
if (!isReplacingManga) {
router.popController(this)
if (newManga != null) {
val newMangaController = RouterTransaction.with(MangaController(newManga))
val newMangaController = RouterTransaction.with(MangaController(newManga.id!!))
if (router.backstack.lastOrNull()?.controller is MangaController) {
// Replace old MangaController
router.replaceTopController(newMangaController)
@ -140,7 +140,7 @@ class SearchController(
}
.setNeutralButton(activity?.getString(R.string.action_show_manga)) { _, _ ->
dismissDialog()
router.pushController(MangaController(newManga))
router.pushController(MangaController(newManga!!.id!!))
}
.create()
}

View file

@ -20,6 +20,7 @@ import com.google.android.material.snackbar.Snackbar
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.domain.category.model.toDbCategory
import eu.kanade.domain.source.model.Source
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category
@ -42,6 +43,8 @@ import eu.kanade.tachiyomi.ui.manga.AddDuplicateMangaDialog
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.more.MoreController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.preference.asImmediateFlow
import eu.kanade.tachiyomi.util.system.connectivityManager
import eu.kanade.tachiyomi.util.system.logcat
@ -572,7 +575,7 @@ open class BrowseSourceController(bundle: Bundle) :
*/
override fun onItemClick(view: View, position: Int): Boolean {
val item = adapter?.getItem(position) as? SourceItem ?: return false
router.pushController(MangaController(item.manga, true))
router.pushController(MangaController(item.manga.id!!, true))
return false
}
@ -589,69 +592,87 @@ open class BrowseSourceController(bundle: Bundle) :
override fun onItemLongClick(position: Int) {
val activity = activity ?: return
val manga = (adapter?.getItem(position) as? SourceItem?)?.manga ?: return
val duplicateManga = presenter.getDuplicateLibraryManga(manga)
launchIO {
val duplicateManga = presenter.getDuplicateLibraryManga(manga)
if (manga.favorite) {
MaterialAlertDialogBuilder(activity)
.setTitle(manga.title)
.setItems(arrayOf(activity.getString(R.string.remove_from_library))) { _, which ->
when (which) {
0 -> {
presenter.changeMangaFavorite(manga)
adapter?.notifyItemChanged(position)
activity.toast(activity.getString(R.string.manga_removed_library))
withUIContext {
if (manga.favorite) {
MaterialAlertDialogBuilder(activity)
.setTitle(manga.title)
.setItems(arrayOf(activity.getString(R.string.remove_from_library))) { _, which ->
when (which) {
0 -> {
presenter.changeMangaFavorite(manga)
adapter?.notifyItemChanged(position)
activity.toast(activity.getString(R.string.manga_removed_library))
}
}
}
.show()
} else {
if (duplicateManga != null) {
AddDuplicateMangaDialog(this@BrowseSourceController, duplicateManga) {
addToLibrary(
manga,
position,
)
}
.showDialog(router)
} else {
addToLibrary(manga, position)
}
}
.show()
} else {
if (duplicateManga != null) {
AddDuplicateMangaDialog(this, duplicateManga) { addToLibrary(manga, position) }
.showDialog(router)
} else {
addToLibrary(manga, position)
}
}
}
private fun addToLibrary(newManga: Manga, position: Int) {
val activity = activity ?: return
val categories = presenter.getCategories()
val defaultCategoryId = preferences.defaultCategory()
val defaultCategory = categories.find { it.id == defaultCategoryId }
launchIO {
val categories = presenter.getCategories()
val defaultCategoryId = preferences.defaultCategory()
val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
when {
// Default category set
defaultCategory != null -> {
presenter.moveMangaToCategory(newManga, defaultCategory)
withUIContext {
when {
// Default category set
defaultCategory != null -> {
presenter.moveMangaToCategory(newManga, defaultCategory.toDbCategory())
presenter.changeMangaFavorite(newManga)
adapter?.notifyItemChanged(position)
activity.toast(activity.getString(R.string.manga_added_library))
}
// Automatic 'Default' or no categories
defaultCategoryId == 0 || categories.isEmpty() -> {
presenter.moveMangaToCategory(newManga, null)
presenter.changeMangaFavorite(newManga)
adapter?.notifyItemChanged(position)
activity.toast(activity.getString(R.string.manga_added_library))
}
// Choose a category
else -> {
val ids = presenter.getMangaCategoryIds(newManga)
val preselected = categories.map {
if (it.id in ids) {
QuadStateTextView.State.CHECKED.ordinal
} else {
QuadStateTextView.State.UNCHECKED.ordinal
presenter.changeMangaFavorite(newManga)
adapter?.notifyItemChanged(position)
activity.toast(activity.getString(R.string.manga_added_library))
}
}.toIntArray()
ChangeMangaCategoriesDialog(this, listOf(newManga), categories, preselected)
.showDialog(router)
// Automatic 'Default' or no categories
defaultCategoryId == 0 || categories.isEmpty() -> {
presenter.moveMangaToCategory(newManga, null)
presenter.changeMangaFavorite(newManga)
adapter?.notifyItemChanged(position)
activity.toast(activity.getString(R.string.manga_added_library))
}
// Choose a category
else -> {
val ids = presenter.getMangaCategoryIds(newManga)
val preselected = categories.map {
if (it.id in ids) {
QuadStateTextView.State.CHECKED.ordinal
} else {
QuadStateTextView.State.UNCHECKED.ordinal
}
}.toTypedArray()
ChangeMangaCategoriesDialog(
this@BrowseSourceController,
listOf(newManga),
categories.map { it.toDbCategory() },
preselected.toIntArray(),
)
.showDialog(router)
}
}
}
}
}

View file

@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.ui.browse.source.browse
import android.os.Bundle
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.manga.interactor.GetDuplicateLibraryManga
import eu.kanade.domain.manga.model.toDbManga
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
@ -43,6 +46,7 @@ import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import logcat.LogPriority
@ -52,6 +56,7 @@ import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
import eu.kanade.domain.category.model.Category as DomainCategory
open class BrowseSourcePresenter(
private val sourceId: Long,
@ -60,6 +65,8 @@ open class BrowseSourcePresenter(
private val db: DatabaseHelper = Injekt.get(),
private val prefs: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(),
private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
) : BasePresenter<BrowseSourceController>() {
/**
@ -344,12 +351,12 @@ open class BrowseSourcePresenter(
*
* @return List of categories, not including the default category
*/
fun getCategories(): List<Category> {
return db.getCategories().executeAsBlocking()
suspend fun getCategories(): List<DomainCategory> {
return getCategories.subscribe().firstOrNull() ?: emptyList()
}
fun getDuplicateLibraryManga(manga: Manga): Manga? {
return db.getDuplicateLibraryManga(manga).executeAsBlocking()
suspend fun getDuplicateLibraryManga(manga: Manga): Manga? {
return getDuplicateLibraryManga.await(manga.title, manga.source)?.toDbManga()
}
/**
@ -358,9 +365,9 @@ open class BrowseSourcePresenter(
* @param manga the manga to get categories from.
* @return Array of category ids the manga is in, if none returns default id
*/
fun getMangaCategoryIds(manga: Manga): Array<Int?> {
fun getMangaCategoryIds(manga: Manga): Array<Long?> {
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
return categories.mapNotNull { it.id }.toTypedArray()
return categories.mapNotNull { it?.id?.toLong() }.toTypedArray()
}
/**

View file

@ -65,7 +65,7 @@ open class GlobalSearchController(
* @param manga clicked item containing manga information.
*/
override fun onMangaClick(manga: Manga) {
router.pushController(MangaController(manga, true))
router.pushController(MangaController(manga.id!!, true))
}
/**

View file

@ -493,7 +493,7 @@ class LibraryController(
// Notify the presenter a manga is being opened.
presenter.onOpenManga()
router.pushController(MangaController(manga))
router.pushController(MangaController(manga.id!!))
}
/**

View file

@ -639,15 +639,28 @@ class MainActivity : BaseActivity() {
}
private fun syncActivityViewWithController(
to: Controller? = router.backstack.lastOrNull()?.controller,
to: Controller? = null,
from: Controller? = null,
isPush: Boolean = true,
) {
if (from is DialogController || to is DialogController) {
return
}
if (from is PreferenceDialogController || to is PreferenceDialogController) {
return
var internalTo = to
if (internalTo == null) {
// Should go here when the activity is recreated and dialog controller is on top of the backstack
// Then we'll assume the top controller is the parent controller of this dialog
val backstack = router.backstack
internalTo = backstack.lastOrNull()?.controller
if (internalTo is DialogController || internalTo is PreferenceDialogController) {
internalTo = backstack.getOrNull(backstack.size - 2)?.controller ?: return
}
} else {
// Ignore changes for normal transactions
if (from is DialogController || internalTo is DialogController) {
return
}
if (from is PreferenceDialogController || internalTo is PreferenceDialogController) {
return
}
}
supportActionBar?.setDisplayHomeAsUpEnabled(router.backstackSize != 1)
@ -655,10 +668,10 @@ class MainActivity : BaseActivity() {
// Always show appbar again when changing controllers
binding.appbar.setExpanded(true)
if ((from == null || from is RootController) && to !is RootController) {
if ((from == null || from is RootController) && internalTo !is RootController) {
showNav(false)
}
if (to is RootController) {
if (internalTo is RootController) {
// Always show bottom nav again when returning to a RootController
showNav(true)
}
@ -666,8 +679,8 @@ class MainActivity : BaseActivity() {
if (from is TabbedController) {
from.cleanupTabs(binding.tabs)
}
if (to is TabbedController) {
if (to.configureTabs(binding.tabs)) {
if (internalTo is TabbedController) {
if (internalTo.configureTabs(binding.tabs)) {
binding.tabs.isVisible = true
}
} else {
@ -677,41 +690,41 @@ class MainActivity : BaseActivity() {
if (from is FabController) {
from.cleanupFab(binding.fabLayout.rootFab)
}
if (to is FabController) {
if (internalTo is FabController) {
binding.fabLayout.rootFab.show()
to.configureFab(binding.fabLayout.rootFab)
internalTo.configureFab(binding.fabLayout.rootFab)
} else {
binding.fabLayout.rootFab.hide()
}
if (!isTablet()) noTabletActivityView(isPush, from, to)
}
private fun noTabletActivityView(isPush: Boolean, from: Controller?, to: Controller?) {
// Save lift state
if (isPush) {
if (router.backstackSize > 1) {
// Save lift state
from?.let {
backstackLiftState[it.instanceId] = binding.appbar.isLifted
if (!isTablet()) {
// Save lift state
if (isPush) {
if (router.backstackSize > 1) {
// Save lift state
from?.let {
backstackLiftState[it.instanceId] = binding.appbar.isLifted
}
} else {
backstackLiftState.clear()
}
binding.appbar.isLifted = false
} else {
backstackLiftState.clear()
}
binding.appbar.isLifted = false
} else {
to?.let {
binding.appbar.isLifted = backstackLiftState.getOrElse(it.instanceId) { false }
}
from?.let {
backstackLiftState.remove(it.instanceId)
internalTo?.let {
binding.appbar.isLifted = backstackLiftState.getOrElse(it.instanceId) { false }
}
from?.let {
backstackLiftState.remove(it.instanceId)
}
}
binding.root.isLiftAppBarOnScroll = internalTo !is NoAppBarElevationController
binding.appbar.isTransparentWhenNotLifted = internalTo is MangaController ||
internalTo is AnimeController
binding.controllerContainer.overlapHeader = internalTo is MangaController ||
internalTo is AnimeController
}
binding.root.isLiftAppBarOnScroll = to !is NoAppBarElevationController
binding.appbar.isTransparentWhenNotLifted = to is MangaController || to is AnimeController
binding.controllerContainer.overlapHeader = to is MangaController || to is AnimeController
}
private fun showNav(visible: Boolean) {

View file

@ -40,7 +40,7 @@ class AddDuplicateMangaDialog(bundle: Bundle? = null) : DialogController(bundle)
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(activity?.getString(R.string.action_show_manga)) { _, _ ->
dismissDialog()
router.pushController(MangaController(libraryManga))
router.pushController(MangaController(libraryManga.id!!))
}
.setCancelable(true)
.create()

View file

@ -26,7 +26,6 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import coil.imageLoader
import coil.request.ImageRequest
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
@ -35,6 +34,7 @@ import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.kanade.data.chapter.NoChaptersException
import eu.kanade.domain.category.model.toDbCategory
import eu.kanade.domain.history.model.HistoryWithRelations
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
@ -90,6 +90,7 @@ import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.hasCustomCover
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast
@ -99,6 +100,7 @@ import eu.kanade.tachiyomi.widget.ActionModeWithToolbar
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import logcat.LogPriority
import reactivecircus.flowbinding.recyclerview.scrollStateChanges
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
@ -122,22 +124,18 @@ class MangaController :
constructor(history: HistoryWithRelations) : this(history.mangaId)
constructor(manga: Manga?, fromSource: Boolean = false) : super(
constructor(mangaId: Long, fromSource: Boolean = false) : super(
bundleOf(
MANGA_EXTRA to (manga?.id ?: 0),
MANGA_EXTRA to mangaId,
FROM_SOURCE_EXTRA to fromSource,
),
) {
this.manga = manga
if (manga != null) {
source = Injekt.get<SourceManager>().getOrStub(manga.source)
this.manga = Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking()
if (this.manga != null) {
source = Injekt.get<SourceManager>().getOrStub(this.manga!!.source)
}
}
constructor(mangaId: Long) : this(
Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking(),
)
@Suppress("unused")
constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA))
@ -184,8 +182,6 @@ class MangaController :
private var trackSheet: TrackSheet? = null
private var dialog: DialogController? = null
/**
* For [recyclerViewUpdatesToolbarTitleAlpha]
*/
@ -208,8 +204,10 @@ class MangaController :
super.onChangeStarted(handler, type)
// Hide toolbar title on enter
// No need to update alpha for cover dialog
if (dialog == null) {
updateToolbarTitleAlpha(if (type.isEnter) 0F else 1F)
if (!type.isEnter) {
if (!type.isPush || router.backstack.lastOrNull()?.controller !is DialogController) {
updateToolbarTitleAlpha(1f)
}
}
recyclerViewUpdatesToolbarTitleAlpha(type.isEnter)
}
@ -307,13 +305,9 @@ class MangaController :
}
.launchIn(viewScope)
settingsSheet = ChaptersSettingsSheet(router, presenter) { group ->
if (group is ChaptersSettingsSheet.Filter.FilterGroup) {
updateFilterIconState()
}
}
settingsSheet = ChaptersSettingsSheet(router, presenter)
trackSheet = TrackSheet(this, manga!!, (activity as MainActivity).supportFragmentManager)
trackSheet = TrackSheet(this, (activity as MainActivity).supportFragmentManager)
updateFilterIconState()
recyclerViewUpdatesToolbarTitleAlpha(true)
@ -403,13 +397,16 @@ class MangaController :
}
override fun onPrepareOptionsMenu(menu: Menu) {
// Hide options for local manga
menu.findItem(R.id.action_share).isVisible = !isLocalSource
menu.findItem(R.id.download_group).isVisible = !isLocalSource
runBlocking {
// Hide options for local manga
menu.findItem(R.id.action_share).isVisible = !isLocalSource
menu.findItem(R.id.download_group).isVisible = !isLocalSource
// Hide options for non-library manga
menu.findItem(R.id.action_edit_categories).isVisible = presenter.manga.favorite && presenter.getCategories().isNotEmpty()
menu.findItem(R.id.action_migrate).isVisible = presenter.manga.favorite
// Hide options for non-library manga
menu.findItem(R.id.action_edit_categories).isVisible =
presenter.manga.favorite && presenter.getCategories().isNotEmpty()
menu.findItem(R.id.action_migrate).isVisible = presenter.manga.favorite
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -519,12 +516,17 @@ class MangaController :
activity?.toast(activity?.getString(R.string.manga_removed_library))
activity?.invalidateOptionsMenu()
} else {
val duplicateManga = presenter.getDuplicateLibraryManga(manga)
if (duplicateManga != null) {
AddDuplicateMangaDialog(this, duplicateManga) { addToLibrary(manga) }
.showDialog(router)
} else {
addToLibrary(manga)
launchIO {
val duplicateManga = presenter.getDuplicateLibraryManga(manga)
withUIContext {
if (duplicateManga != null) {
AddDuplicateMangaDialog(this@MangaController, duplicateManga) { addToLibrary(manga) }
.showDialog(router)
} else {
addToLibrary(manga)
}
}
}
}
}
@ -534,39 +536,47 @@ class MangaController :
}
private fun addToLibrary(newManga: Manga) {
val categories = presenter.getCategories()
val defaultCategoryId = preferences.defaultCategory()
val defaultCategory = categories.find { it.id == defaultCategoryId }
launchIO {
val categories = presenter.getCategories()
val defaultCategoryId = preferences.defaultCategory()
val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
when {
// Default category set
defaultCategory != null -> {
toggleFavorite()
presenter.moveMangaToCategory(newManga, defaultCategory)
activity?.toast(activity?.getString(R.string.manga_added_library))
activity?.invalidateOptionsMenu()
}
// Automatic 'Default' or no categories
defaultCategoryId == 0 || categories.isEmpty() -> {
toggleFavorite()
presenter.moveMangaToCategory(newManga, null)
activity?.toast(activity?.getString(R.string.manga_added_library))
activity?.invalidateOptionsMenu()
}
// Choose a category
else -> {
val ids = presenter.getMangaCategoryIds(newManga)
val preselected = categories.map {
if (it.id!! in ids) {
QuadStateTextView.State.CHECKED.ordinal
} else {
QuadStateTextView.State.UNCHECKED.ordinal
withUIContext {
when {
// Default category set
defaultCategory != null -> {
toggleFavorite()
presenter.moveMangaToCategory(newManga, defaultCategory.toDbCategory())
activity?.toast(activity?.getString(R.string.manga_added_library))
activity?.invalidateOptionsMenu()
}
}.toIntArray()
showChangeCategoryDialog(newManga, categories, preselected)
// Automatic 'Default' or no categories
defaultCategoryId == 0 || categories.isEmpty() -> {
toggleFavorite()
presenter.moveMangaToCategory(newManga, null)
activity?.toast(activity?.getString(R.string.manga_added_library))
activity?.invalidateOptionsMenu()
}
// Choose a category
else -> {
val ids = presenter.getMangaCategoryIds(newManga)
val preselected = categories.map {
if (it.id in ids) {
QuadStateTextView.State.CHECKED.ordinal
} else {
QuadStateTextView.State.UNCHECKED.ordinal
}
}.toTypedArray()
showChangeCategoryDialog(
newManga,
categories.map { it.toDbCategory() },
preselected,
)
}
}
}
}
@ -610,32 +620,32 @@ class MangaController :
}
fun onCategoriesClick() {
val manga = presenter.manga
val categories = presenter.getCategories()
launchIO {
val manga = presenter.manga
val categories = presenter.getCategories()
val ids = presenter.getMangaCategoryIds(manga)
val preselected = categories.map {
if (it.id!! in ids) {
QuadStateTextView.State.CHECKED.ordinal
} else {
QuadStateTextView.State.UNCHECKED.ordinal
if (categories.isEmpty()) {
return@launchIO
}
}.toIntArray()
showChangeCategoryDialog(manga, categories, preselected)
val ids = presenter.getMangaCategoryIds(manga)
val preselected = categories.map {
if (it.id in ids) {
QuadStateTextView.State.CHECKED.ordinal
} else {
QuadStateTextView.State.UNCHECKED.ordinal
}
}.toTypedArray()
withUIContext {
showChangeCategoryDialog(manga, categories.map { it.toDbCategory() }, preselected)
}
}
}
private fun showChangeCategoryDialog(manga: Manga, categories: List<Category>, preselected: IntArray) {
if (dialog != null) return
dialog = ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
dialog?.addLifecycleListener(
object : LifecycleListener() {
override fun postDestroy(controller: Controller) {
super.postDestroy(controller)
dialog = null
}
},
)
dialog?.showDialog(router)
private fun showChangeCategoryDialog(manga: Manga, categories: List<Category>, preselected: Array<Int>) {
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected.toIntArray())
.showDialog(router)
}
override fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
@ -735,18 +745,9 @@ class MangaController :
}
fun showFullCoverDialog() {
if (dialog != null) return
val manga = manga ?: return
dialog = MangaFullCoverDialog(this, manga)
dialog?.addLifecycleListener(
object : LifecycleListener() {
override fun postDestroy(controller: Controller) {
super.postDestroy(controller)
dialog = null
}
},
)
dialog?.showDialog(router)
MangaFullCoverDialog(this, manga)
.showDialog(router)
}
fun shareCover() {
@ -836,13 +837,13 @@ class MangaController :
val dataUri = data?.data
if (dataUri == null || resultCode != Activity.RESULT_OK) return
val activity = activity ?: return
presenter.editCover(manga!!, activity, dataUri)
presenter.editCover(activity, dataUri)
}
}
fun onSetCoverSuccess() {
mangaInfoAdapter?.notifyItemChanged(0, this)
(dialog as? MangaFullCoverDialog)?.setImage(manga)
(router.backstack.lastOrNull()?.controller as? MangaFullCoverDialog)?.setImage(manga)
activity?.toast(R.string.cover_updated)
}
@ -890,6 +891,7 @@ class MangaController :
}
updateFabVisibility()
updateFilterIconState()
}
private fun fetchChaptersFromSource(manualFetch: Boolean = false) {

View file

@ -4,6 +4,11 @@ import android.content.Context
import android.net.Uri
import android.os.Bundle
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
import eu.kanade.domain.chapter.model.toDbChapter
import eu.kanade.domain.manga.interactor.GetDuplicateLibraryManga
import eu.kanade.domain.manga.model.toDbManga
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
@ -11,6 +16,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.toDomainManga
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download
@ -21,7 +27,6 @@ import eu.kanade.tachiyomi.data.track.AnimeTrackService
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.toSChapter
import eu.kanade.tachiyomi.source.model.toSManga
@ -32,8 +37,10 @@ import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper
import eu.kanade.tachiyomi.util.chapter.getChapterSort
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay
import eu.kanade.tachiyomi.util.editCover
import eu.kanade.tachiyomi.util.isLocal
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.prepUpdateCover
import eu.kanade.tachiyomi.util.removeCovers
@ -45,6 +52,8 @@ import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Stat
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.supervisorScope
import logcat.LogPriority
import rx.Observable
@ -55,6 +64,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.Date
import eu.kanade.domain.category.model.Category as DomainCategory
class MangaPresenter(
val manga: Manga,
@ -64,7 +74,9 @@ class MangaPresenter(
private val trackManager: TrackManager = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(),
// private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(),
private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(),
private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
) : BasePresenter<MangaController>() {
/**
@ -145,32 +157,26 @@ class MangaPresenter(
// Chapters list - start
// Keeps subscribed to changes and sends the list of chapters to the relay.
add(
db.getChapters(manga).asRxObservable()
.map { chapters ->
// Convert every chapter to a model.
chapters.map { it.toModel() }
}
.doOnNext { episodes ->
// Find downloaded chapters
setDownloadedChapters(episodes)
// Store the last emission
this.allChapters = episodes
// Listen for download status changes
observeDownloads()
}
.subscribe { chaptersRelay.call(it) },
)
presenterScope.launchIO {
manga.id?.let { mangaId ->
getChapterByMangaId.subscribe(mangaId)
.collectLatest { domainChapters ->
val chapterItems = domainChapters.map { it.toDbChapter().toModel() }
setDownloadedChapters(chapterItems)
this@MangaPresenter.allChapters = chapterItems
observeDownloads()
chaptersRelay.call(chapterItems)
}
}
}
// Chapters list - end
fetchTrackers()
}
fun getDuplicateLibraryManga(manga: Manga): Manga? {
return db.getDuplicateLibraryManga(manga).executeAsBlocking()
suspend fun getDuplicateLibraryManga(manga: Manga): Manga? {
return getDuplicateLibraryManga.await(manga.title, manga.source)?.toDbManga()
}
// Manga info - start
@ -254,8 +260,8 @@ class MangaPresenter(
*
* @return List of categories, not including the default category
*/
fun getCategories(): List<Category> {
return db.getCategories().executeAsBlocking()
suspend fun getCategories(): List<DomainCategory> {
return getCategories.subscribe().firstOrNull() ?: emptyList()
}
/**
@ -264,9 +270,9 @@ class MangaPresenter(
* @param manga the manga to get categories from.
* @return Array of category ids the manga is in, if none returns default id
*/
fun getMangaCategoryIds(manga: Manga): IntArray {
fun getMangaCategoryIds(manga: Manga): Array<Long> {
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
return categories.mapNotNull { it.id }.toIntArray()
return categories.mapNotNull { it?.id?.toLong() }.toTypedArray()
}
/**
@ -303,31 +309,20 @@ class MangaPresenter(
/**
* Update cover with local file.
*
* @param manga the manga edited.
* @param context Context.
* @param data uri of the cover resource.
*/
fun editCover(manga: Manga, context: Context, data: Uri) {
Observable
.fromCallable {
context.contentResolver.openInputStream(data)?.use {
if (manga.isLocal()) {
LocalSource.updateCover(context, manga, it)
manga.updateCoverLastModified(db)
db.insertManga(manga).executeAsBlocking()
} else if (manga.favorite) {
coverCache.setCustomCoverToCache(manga, it)
manga.updateCoverLastModified(db)
}
true
fun editCover(context: Context, data: Uri) {
presenterScope.launchIO {
context.contentResolver.openInputStream(data)?.use {
try {
val result = manga.toDomainManga()!!.editCover(context, it)
launchUI { if (result) view?.onSetCoverSuccess() }
} catch (e: Exception) {
launchUI { view?.onSetCoverError(e) }
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeFirst(
{ view, _ -> view.onSetCoverSuccess() },
{ view, e -> view.onSetCoverError(e) },
)
}
}
fun deleteCustomCover(manga: Manga) {
@ -664,14 +659,14 @@ class MangaPresenter(
/**
* Whether downloaded only mode is enabled.
*/
fun forceDownloaded(): Boolean {
private fun forceDownloaded(): Boolean {
return manga.favorite && preferences.downloadedOnly().get()
}
/**
* Whether the display only downloaded filter is enabled.
*/
fun onlyDownloaded(): State {
private fun onlyDownloaded(): State {
if (forceDownloaded()) {
return State.INCLUDE
}
@ -685,7 +680,7 @@ class MangaPresenter(
/**
* Whether the display only downloaded filter is enabled.
*/
fun onlyBookmarked(): State {
private fun onlyBookmarked(): State {
return when (manga.bookmarkedFilter) {
Manga.CHAPTER_SHOW_BOOKMARKED -> State.INCLUDE
Manga.CHAPTER_SHOW_NOT_BOOKMARKED -> State.EXCLUDE
@ -696,7 +691,7 @@ class MangaPresenter(
/**
* Whether the display only unread filter is enabled.
*/
fun onlyUnread(): State {
private fun onlyUnread(): State {
return when (manga.readFilter) {
Manga.CHAPTER_SHOW_UNREAD -> State.INCLUDE
Manga.CHAPTER_SHOW_READ -> State.EXCLUDE

View file

@ -6,35 +6,51 @@ import android.util.AttributeSet
import android.view.View
import androidx.core.view.isVisible
import com.bluelinelabs.conductor.Router
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.manga.model.toTriStateGroupState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toDomainManga
import eu.kanade.tachiyomi.ui.manga.MangaPresenter
import eu.kanade.tachiyomi.util.view.popupMenu
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
import eu.kanade.tachiyomi.widget.sheet.TabbedBottomSheetDialog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
class ChaptersSettingsSheet(
private val router: Router,
private val presenter: MangaPresenter,
private val onGroupClickListener: (ExtendedNavigationView.Group) -> Unit,
) : TabbedBottomSheetDialog(router.activity!!) {
val filters = Filter(router.activity!!)
private val sort = Sort(router.activity!!)
private val display = Display(router.activity!!)
private lateinit var scope: CoroutineScope
private var manga: Manga? = null
val filters = Filter(context)
private val sort = Sort(context)
private val display = Display(context)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
filters.onGroupClicked = onGroupClickListener
sort.onGroupClicked = onGroupClickListener
display.onGroupClicked = onGroupClickListener
binding.menu.isVisible = true
binding.menu.setOnClickListener { it.post { showPopupMenu(it) } }
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
scope = MainScope()
// TODO: Listen to changes
updateManga()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
scope.cancel()
}
override fun getTabViews(): List<View> = listOf(
filters,
sort,
@ -47,6 +63,10 @@ class ChaptersSettingsSheet(
R.string.action_display,
)
private fun updateManga() {
manga = presenter.manga.toDomainManga()
}
private fun showPopupMenu(view: View) {
view.popupMenu(
menuRes = R.menu.default_chapter_filter,
@ -79,6 +99,10 @@ class ChaptersSettingsSheet(
return filterGroup.items.any { it.state != State.IGNORE.value }
}
override fun updateView() {
filterGroup.updateModels()
}
inner class FilterGroup : Group {
private val downloaded = Item.TriStateGroup(R.string.action_filter_downloaded, this)
@ -90,14 +114,20 @@ class ChaptersSettingsSheet(
override val footer: Item? = null
override fun initModels() {
if (presenter.forceDownloaded()) {
val manga = manga ?: return
if (manga.forceDownloaded()) {
downloaded.state = State.INCLUDE.value
downloaded.enabled = false
} else {
downloaded.state = presenter.onlyDownloaded().value
downloaded.state = manga.downloadedFilter.toTriStateGroupState().value
}
unread.state = presenter.onlyUnread().value
bookmarked.state = presenter.onlyBookmarked().value
unread.state = manga.unreadFilter.toTriStateGroupState().value
bookmarked.state = manga.bookmarkedFilter.toTriStateGroupState().value
}
fun updateModels() {
initModels()
adapter.notifyItemRangeChanged(0, 3)
}
override fun onItemClicked(item: Item) {
@ -108,7 +138,6 @@ class ChaptersSettingsSheet(
State.EXCLUDE.value -> State.IGNORE
else -> throw Exception("Unknown State")
}
item.state = newState.value
when (item) {
downloaded -> presenter.setDownloadedFilter(newState)
unread -> presenter.setUnreadFilter(newState)
@ -116,8 +145,9 @@ class ChaptersSettingsSheet(
else -> {}
}
initModels()
adapter.notifyItemChanged(items.indexOf(item), item)
// TODO: Remove
updateManga()
updateView()
}
}
}
@ -128,8 +158,14 @@ class ChaptersSettingsSheet(
inner class Sort @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
Settings(context, attrs) {
private val group = SortGroup()
init {
setGroups(listOf(SortGroup()))
setGroups(listOf(group))
}
override fun updateView() {
group.updateModels()
}
inner class SortGroup : Group {
@ -143,8 +179,9 @@ class ChaptersSettingsSheet(
override val footer: Item? = null
override fun initModels() {
val sorting = presenter.manga.sorting
val order = if (presenter.manga.sortDescending()) {
val manga = manga ?: return
val sorting = manga.sorting
val order = if (manga.sortDescending()) {
Item.MultiSort.SORT_DESC
} else {
Item.MultiSort.SORT_ASC
@ -158,29 +195,23 @@ class ChaptersSettingsSheet(
if (sorting == Manga.CHAPTER_SORTING_UPLOAD_DATE) order else Item.MultiSort.SORT_NONE
}
override fun onItemClicked(item: Item) {
items.forEachIndexed { i, multiSort ->
multiSort.state = if (multiSort == item) {
when (item.state) {
Item.MultiSort.SORT_NONE -> Item.MultiSort.SORT_ASC
Item.MultiSort.SORT_ASC -> Item.MultiSort.SORT_DESC
Item.MultiSort.SORT_DESC -> Item.MultiSort.SORT_ASC
else -> throw Exception("Unknown state")
}
} else {
Item.MultiSort.SORT_NONE
}
adapter.notifyItemChanged(i, multiSort)
}
fun updateModels() {
initModels()
adapter.notifyItemRangeChanged(0, 3)
}
override fun onItemClicked(item: Item) {
when (item) {
source -> presenter.setSorting(Manga.CHAPTER_SORTING_SOURCE)
chapterNum -> presenter.setSorting(Manga.CHAPTER_SORTING_NUMBER)
uploadDate -> presenter.setSorting(Manga.CHAPTER_SORTING_UPLOAD_DATE)
source -> presenter.setSorting(Manga.CHAPTER_SORTING_SOURCE.toInt())
chapterNum -> presenter.setSorting(Manga.CHAPTER_SORTING_NUMBER.toInt())
uploadDate -> presenter.setSorting(Manga.CHAPTER_SORTING_UPLOAD_DATE.toInt())
else -> throw Exception("Unknown sorting")
}
// TODO: Remove
presenter.reverseSortOrder()
updateManga()
updateView()
}
}
}
@ -191,8 +222,14 @@ class ChaptersSettingsSheet(
inner class Display @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
Settings(context, attrs) {
private val group = DisplayGroup()
init {
setGroups(listOf(DisplayGroup()))
setGroups(listOf(group))
}
override fun updateView() {
group.updateModels()
}
inner class DisplayGroup : Group {
@ -205,25 +242,29 @@ class ChaptersSettingsSheet(
override val footer: Item? = null
override fun initModels() {
val mode = presenter.manga.displayMode
val mode = manga?.displayMode ?: return
displayTitle.checked = mode == Manga.CHAPTER_DISPLAY_NAME
displayChapterNum.checked = mode == Manga.CHAPTER_DISPLAY_NUMBER
}
fun updateModels() {
initModels()
adapter.notifyItemRangeChanged(0, 2)
}
override fun onItemClicked(item: Item) {
item as Item.Radio
if (item.checked) return
items.forEachIndexed { index, radio ->
radio.checked = item == radio
adapter.notifyItemChanged(index, radio)
}
when (item) {
displayTitle -> presenter.setDisplayMode(Manga.CHAPTER_DISPLAY_NAME)
displayChapterNum -> presenter.setDisplayMode(Manga.CHAPTER_DISPLAY_NUMBER)
displayTitle -> presenter.setDisplayMode(Manga.CHAPTER_DISPLAY_NAME.toInt())
displayChapterNum -> presenter.setDisplayMode(Manga.CHAPTER_DISPLAY_NUMBER.toInt())
else -> throw NotImplementedError("Unknown display mode")
}
// TODO: Remove
updateManga()
updateView()
}
}
}
@ -246,6 +287,9 @@ class ChaptersSettingsSheet(
addView(recycler)
}
open fun updateView() {
}
/**
* Adapter of the recycler view.
*/

View file

@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.databinding.MangaInfoHeaderBinding
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.getNameForMangaInfo
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.getMainAppBarHeight
@ -100,7 +101,7 @@ class MangaInfoHeaderAdapter(
.onEach { controller.onFavoriteClick() }
.launchIn(controller.viewScope)
if (controller.presenter.manga.favorite && controller.presenter.getCategories().isNotEmpty()) {
if (controller.presenter.manga.favorite) {
binding.btnFavorite.longClicks()
.onEach { controller.onCategoriesClick() }
.launchIn(controller.viewScope)
@ -247,20 +248,8 @@ class MangaInfoHeaderAdapter(
// If manga source is known update source TextView.
binding.mangaMissingSourceIcon.isVisible = source is SourceManager.StubSource
val mangaSource = source.toString()
with(binding.mangaSource) {
val enabledLanguages = preferences.enabledLanguages().get()
.filterNot { it in listOf("all", "other") }
val hasOneActiveLanguages = enabledLanguages.size == 1
val isInEnabledLanguages = source.lang in enabledLanguages
text = when {
// For edge cases where user disables a source they got manga of in their library.
hasOneActiveLanguages && !isInEnabledLanguages -> mangaSource
// Hide the language tag when only one language is used.
hasOneActiveLanguages && isInEnabledLanguages -> source.name
else -> mangaSource
}
text = source.getNameForMangaInfo()
setOnClickListener {
controller.performSearch(sourceManager.getOrStub(source.id).name)

View file

@ -11,10 +11,8 @@ import com.google.android.material.datepicker.DateValidatorPointBackward
import com.google.android.material.datepicker.DateValidatorPointForward
import com.google.android.material.datepicker.MaterialDatePicker
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
import eu.kanade.tachiyomi.databinding.TrackControllerBinding
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.lang.launchIO
@ -24,14 +22,10 @@ import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class TrackSheet(
val controller: MangaController,
val manga: Manga,
val fragmentManager: FragmentManager,
private val sourceManager: SourceManager = Injekt.get(),
) : BaseBottomSheetDialog(controller.activity!!),
TrackAdapter.OnClickListener,
SetTrackStatusDialog.Listener,
@ -80,6 +74,8 @@ class TrackSheet(
override fun onSetClick(position: Int) {
val item = adapter.getItem(position) ?: return
val manga = controller.presenter.manga
val source = controller.presenter.source
if (item.service is EnhancedTrackService) {
if (item.track != null) {
@ -87,7 +83,7 @@ class TrackSheet(
return
}
if (!item.service.accept(sourceManager.getOrStub(manga.source))) {
if (!item.service.accept(source)) {
controller.presenter.view?.applicationContext?.toast(R.string.source_unsupported)
return
}

View file

@ -877,7 +877,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
* cover to the presenter.
*/
fun setAsCover(page: ReaderPage) {
presenter.setAsCover(page)
presenter.setAsCover(this, page)
}
/**

View file

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.reader
import android.app.Application
import android.content.Context
import android.net.Uri
import android.os.Bundle
import com.jakewharton.rxrelay.BehaviorRelay
@ -8,9 +9,11 @@ import eu.kanade.domain.chapter.interactor.UpdateChapter
import eu.kanade.domain.chapter.model.ChapterUpdate
import eu.kanade.domain.history.interactor.UpsertHistory
import eu.kanade.domain.history.model.HistoryUpdate
import eu.kanade.domain.manga.model.isLocal
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toDomainManga
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.saver.Image
@ -19,7 +22,6 @@ import eu.kanade.tachiyomi.data.saver.Location
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingUpdateJob
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
@ -32,7 +34,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.util.chapter.getChapterSort
import eu.kanade.tachiyomi.util.isLocal
import eu.kanade.tachiyomi.util.editCover
import eu.kanade.tachiyomi.util.lang.byteSize
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
@ -41,7 +43,6 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.cacheImageDir
import eu.kanade.tachiyomi.util.system.isOnline
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.updateCoverLastModified
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import logcat.LogPriority
@ -692,36 +693,28 @@ class ReaderPresenter(
/**
* Sets the image of this [page] as cover and notifies the UI of the result.
*/
fun setAsCover(page: ReaderPage) {
fun setAsCover(context: Context, page: ReaderPage) {
if (page.status != Page.READY) return
val manga = manga ?: return
val manga = manga?.toDomainManga() ?: return
val stream = page.stream ?: return
Observable
.fromCallable {
stream().use {
if (manga.isLocal()) {
val context = Injekt.get<Application>()
LocalSource.updateCover(context, manga, it)
manga.updateCoverLastModified(db)
SetAsCoverResult.Success
} else {
if (manga.favorite) {
coverCache.setCustomCoverToCache(manga, it)
manga.updateCoverLastModified(db)
SetAsCoverResult.Success
} else {
SetAsCoverResult.AddToLibraryFirst
}
}
}
presenterScope.launchIO {
val result = try {
manga.editCover(context, stream())
} catch (e: Exception) {
false
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeFirst(
{ view, result -> view.onSetAsCoverResult(result) },
{ view, _ -> view.onSetAsCoverResult(SetAsCoverResult.Error) },
)
launchUI {
val resultResult = if (!result) {
SetAsCoverResult.Error
} else if (manga.isLocal() || manga.favorite) {
SetAsCoverResult.Success
} else {
SetAsCoverResult.AddToLibraryFirst
}
view?.onSetAsCoverResult(resultResult)
}
}
}
/**

View file

@ -29,7 +29,7 @@ import eu.kanade.tachiyomi.ui.anime.episode.EpisodeItem
import eu.kanade.tachiyomi.ui.anime.episode.base.BaseEpisodesAdapter
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.base.controller.pushController
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.player.EpisodeLoader
import eu.kanade.tachiyomi.ui.player.ExternalIntents
@ -383,7 +383,7 @@ class AnimeUpdatesController :
}
private fun openAnime(episode: AnimeUpdatesItem) {
parentController!!.router.pushController(AnimeController(episode.anime).withFadeTransaction())
parentController!!.router.pushController(AnimeController(episode.anime.id!!))
}
/**

View file

@ -282,7 +282,7 @@ class UpdatesController :
}
private fun openManga(chapter: UpdatesItem) {
parentController!!.router.pushController(MangaController(chapter.manga))
parentController!!.router.pushController(MangaController(chapter.manga.id!!))
}
/**

View file

@ -1,5 +1,9 @@
package eu.kanade.tachiyomi.util
import android.content.Context
import eu.kanade.domain.anime.interactor.UpdateAnime
import eu.kanade.domain.anime.model.isLocal
import eu.kanade.domain.anime.model.toDbAnime
import eu.kanade.tachiyomi.animesource.LocalAnimeSource
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
@ -8,7 +12,9 @@ import eu.kanade.tachiyomi.data.database.models.Anime
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.InputStream
import java.util.Date
import eu.kanade.domain.anime.model.Anime as DomainAnime
fun Anime.isLocal() = source == LocalAnimeSource.ID
@ -81,3 +87,21 @@ fun Anime.shouldDownloadNewEpisodes(db: AnimeDatabaseHelper, prefs: PreferencesH
// In included category
return categoriesForAnime.any { it in includedCategories }
}
suspend fun DomainAnime.editCover(
context: Context,
stream: InputStream,
updateAnime: UpdateAnime = Injekt.get(),
coverCache: AnimeCoverCache = Injekt.get(),
): Boolean {
return if (isLocal()) {
LocalAnimeSource.updateCover(context, toDbAnime(), stream)
updateAnime.awaitUpdateCoverLastModified(id)
} else if (favorite) {
coverCache.setCustomCoverToCache(toDbAnime(), stream)
updateAnime.awaitUpdateCoverLastModified(id)
} else {
// We should never reach this block
false
}
}

View file

@ -1,5 +1,9 @@
package eu.kanade.tachiyomi.util
import android.content.Context
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.isLocal
import eu.kanade.domain.manga.model.toDbManga
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
@ -8,7 +12,9 @@ import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.model.SManga
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.InputStream
import java.util.Date
import eu.kanade.domain.manga.model.Manga as DomainManga
fun Manga.isLocal() = source == LocalSource.ID
@ -82,3 +88,21 @@ fun Manga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: PreferencesHelper
// In included category
return categoriesForManga.any { it in includedCategories }
}
suspend fun DomainManga.editCover(
context: Context,
stream: InputStream,
updateManga: UpdateManga = Injekt.get(),
coverCache: CoverCache = Injekt.get(),
): Boolean {
return if (isLocal()) {
LocalSource.updateCover(context, toDbManga(), stream)
updateManga.awaitUpdateCoverLastModified(id)
} else if (favorite) {
coverCache.setCustomCoverToCache(toDbManga(), stream)
updateManga.awaitUpdateCoverLastModified(id)
} else {
// We should never reach this block
false
}
}

View file

@ -26,49 +26,40 @@
android:layout_height="?attr/actionBarSize"
android:theme="?attr/actionBarTheme" />
</com.google.android.material.appbar.TachiyomiAppBarLayout>
<FrameLayout
android:id="@+id/downloaded_only"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?attr/colorTertiary"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/side_nav"
app:layout_constraintTop_toBottomOf="@+id/appbar"
tools:visibility="visible">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:layout_width="wrap_content"
android:id="@+id/downloaded_only"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?attr/colorTertiary"
android:gravity="center"
android:padding="4dp"
android:text="@string/label_downloaded_only"
android:textColor="?attr/colorOnTertiary" />
</FrameLayout>
<FrameLayout
android:id="@+id/incognito_mode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/side_nav"
app:layout_constraintTop_toBottomOf="@+id/downloaded_only"
tools:visibility="visible">
android:textAlignment="center"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorOnTertiary"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:layout_width="wrap_content"
android:id="@+id/incognito_mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?attr/colorPrimary"
android:gravity="center"
android:padding="4dp"
android:text="@string/pref_incognito_mode"
android:textColor="?attr/colorOnPrimary" />
android:textAlignment="center"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorOnPrimary"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>
</com.google.android.material.appbar.TachiyomiAppBarLayout>
<eu.kanade.tachiyomi.widget.TachiyomiNavigationRailView
android:id="@+id/side_nav"
@ -79,14 +70,6 @@
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/main_nav" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/side_nav"
app:layout_constraintTop_toBottomOf="@+id/incognito_mode" />
<eu.kanade.tachiyomi.widget.TachiyomiChangeHandlerFrameLayout
android:id="@+id/controller_container"
android:layout_width="0dp"
@ -94,7 +77,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/side_nav"
app:layout_constraintTop_toBottomOf="@+id/tabs" />
app:layout_constraintTop_toBottomOf="@+id/appbar" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -32,41 +32,33 @@
android:layout_height="wrap_content"
android:visibility="gone" />
<FrameLayout
<TextView
android:id="@+id/downloaded_only"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorTertiary"
android:gravity="center"
android:padding="4dp"
android:text="@string/label_downloaded_only"
android:textAlignment="center"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorOnTertiary"
android:visibility="gone"
tools:visibility="visible">
tools:visibility="visible" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="4dp"
android:text="@string/label_downloaded_only"
android:textColor="?attr/colorOnTertiary" />
</FrameLayout>
<FrameLayout
<TextView
android:id="@+id/incognito_mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:gravity="center"
android:padding="4dp"
android:text="@string/pref_incognito_mode"
android:textAlignment="center"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorOnPrimary"
android:visibility="gone"
tools:visibility="visible">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="4dp"
android:text="@string/pref_incognito_mode"
android:textColor="?attr/colorOnPrimary" />
</FrameLayout>
tools:visibility="visible" />
</com.google.android.material.appbar.TachiyomiAppBarLayout>

View file

@ -742,7 +742,7 @@
\nهل مازلت تريد أن تكمل؟</string>
<string name="pref_update_only_started">لم تقرأ أي فصول</string>
<string name="skipped_reason_completed">تم تخطيها لإنتهاء السلسلة</string>
<string name="pref_navigate_pan">قود للتحرك</string>
<string name="pref_navigate_pan">قُدْ للتحرك</string>
<string name="pref_landscape_zoom">كبر الصوره الأفقية</string>
<string name="channel_skipped">متجاوز</string>
<string name="error_saving_picture">خطأ أثناء حفظ الصورة</string>
@ -785,4 +785,5 @@
<string name="complete_list">قائمة الكاملة</string>
<string name="on_hold_list">قائمة المعلقة</string>
<string name="custom_cover">غلاف مخصص</string>
<string name="cant_open_last_read_chapter">غير قادر على فتح آخر فصل تم قراءته</string>
</resources>

View file

@ -736,4 +736,6 @@
<string name="on_hold_list">Lista de espera</string>
<string name="reading_list">Lista de lectura</string>
<string name="network_not_metered">Solo en red sin medidor</string>
<string name="custom_cover">Portada Personalizada</string>
<string name="cant_open_last_read_chapter">Imposible abrir el último capítulo leído</string>
</resources>

Some files were not shown because too many files have changed in this diff Show more