mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-28 09:15:12 +03:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
640cadb70b
130 changed files with 2452 additions and 1409 deletions
2
.github/workflows/build_pull_request.yml
vendored
2
.github/workflows/build_pull_request.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
72
app/src/main/java/eu/kanade/data/AnimeQueryPagingSource.kt
Normal file
72
app/src/main/java/eu/kanade/data/AnimeQueryPagingSource.kt
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
}
|
||||
|
|
72
app/src/main/java/eu/kanade/data/QueryPagingSource.kt
Normal file
72
app/src/main/java/eu/kanade/data/QueryPagingSource.kt
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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() }
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
|
|
|
@ -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() }
|
||||
|
|
|
@ -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()) }
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import eu.kanade.domain.manga.repository.MangaRepository
|
|||
class ResetViewerFlags(
|
||||
private val mangaRepository: MangaRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(): Boolean {
|
||||
return mangaRepository.resetViewerFlags()
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
12
app/src/main/java/eu/kanade/domain/manga/model/MangaCover.kt
Normal file
12
app/src/main/java/eu/kanade/domain/manga/model/MangaCover.kt
Normal 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,
|
||||
)
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 },
|
||||
) {
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
|
@ -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 = '.' },
|
||||
)
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
9
app/src/main/java/eu/kanade/presentation/util/Shimmer.kt
Normal file
9
app/src/main/java/eu/kanade/presentation/util/Shimmer.kt
Normal 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),
|
||||
)
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -3,6 +3,5 @@ package eu.kanade.tachiyomi.data.database
|
|||
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
||||
|
||||
interface DbProvider {
|
||||
|
||||
val db: DefaultStorIOSQLite
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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!!))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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!!))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -877,7 +877,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
* cover to the presenter.
|
||||
*/
|
||||
fun setAsCover(page: ReaderPage) {
|
||||
presenter.setAsCover(page)
|
||||
presenter.setAsCover(this, page)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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!!))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -282,7 +282,7 @@ class UpdatesController :
|
|||
}
|
||||
|
||||
private fun openManga(chapter: UpdatesItem) {
|
||||
parentController!!.router.pushController(MangaController(chapter.manga))
|
||||
parentController!!.router.pushController(MangaController(chapter.manga.id!!))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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
Loading…
Reference in a new issue