mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-25 14:19:27 +03:00
Merge upstream
This commit is contained in:
parent
9b4b4da7ce
commit
ff08f5fa8f
415 changed files with 13815 additions and 13866 deletions
|
@ -12,7 +12,6 @@ plugins {
|
|||
|
||||
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
||||
apply<com.google.gms.googleservices.GoogleServicesPlugin>()
|
||||
apply<com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsPlugin>()
|
||||
}
|
||||
|
||||
shortcutHelper.setFilePath("./shortcuts.xml")
|
||||
|
@ -29,7 +28,7 @@ android {
|
|||
minSdk = AndroidConfig.minSdk
|
||||
targetSdk = AndroidConfig.targetSdk
|
||||
versionCode = 90
|
||||
versionName = "0.13.5.0"
|
||||
versionName = "0.14.0.0"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
|
@ -296,7 +295,6 @@ dependencies {
|
|||
// Crash reports/analytics
|
||||
implementation(libs.acra.http)
|
||||
implementation(libs.firebase.analytics)
|
||||
implementation(libs.firebase.crashlytics)
|
||||
|
||||
// Shizuku
|
||||
implementation(libs.bundles.shizuku)
|
||||
|
|
|
@ -141,7 +141,7 @@
|
|||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".extension.util.AnimeExtensionInstallActivity"
|
||||
android:name=".animeextension.util.AnimeExtensionInstallActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:exported="false" />
|
||||
|
||||
|
@ -225,12 +225,6 @@
|
|||
android:name=".data.notification.NotificationReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver android:name="androidx.media.session.MediaButtonReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".glance.UpdatesGridGlanceReceiver"
|
||||
android:enabled="@bool/glance_appwidget_available"
|
||||
|
@ -258,7 +252,7 @@
|
|||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".data.download.AnimeDownloadService"
|
||||
android:name=".data.animedownload.AnimeDownloadService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
|
@ -272,7 +266,7 @@
|
|||
<service android:name=".extension.util.ExtensionInstallService"
|
||||
android:exported="false" />
|
||||
|
||||
<service android:name=".extension.util.AnimeExtensionInstallService"
|
||||
<service android:name=".animeextension.util.AnimeExtensionInstallService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
|
|
|
@ -1,40 +1,17 @@
|
|||
package eu.kanade.data.anime
|
||||
|
||||
import eu.kanade.domain.anime.model.Anime
|
||||
import eu.kanade.domain.episode.model.Episode
|
||||
import eu.kanade.tachiyomi.data.database.models.AnimelibAnime
|
||||
import eu.kanade.domain.animelib.model.AnimelibAnime
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
|
||||
val animeMapper: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long) -> Anime =
|
||||
{ id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, _, initialized, viewer, episodeFlags, coverLastModified, dateAdded ->
|
||||
val animeMapper: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, UpdateStrategy) -> Anime =
|
||||
{ id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, _, initialized, viewerFlags, episodeFlags, coverLastModified, dateAdded, updateStrategy ->
|
||||
Anime(
|
||||
id = id,
|
||||
source = source,
|
||||
favorite = favorite,
|
||||
lastUpdate = lastUpdate ?: 0,
|
||||
dateAdded = dateAdded,
|
||||
viewerFlags = viewer,
|
||||
episodeFlags = episodeFlags,
|
||||
coverLastModified = coverLastModified,
|
||||
url = url,
|
||||
title = title,
|
||||
artist = artist,
|
||||
author = author,
|
||||
description = description,
|
||||
genre = genre,
|
||||
status = status,
|
||||
thumbnailUrl = thumbnailUrl,
|
||||
initialized = initialized,
|
||||
)
|
||||
}
|
||||
|
||||
val animeEpisodeMapper: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, Long, Long, String, String, String?, Boolean, Boolean, Long, Long, Float, Long, Long, Long) -> Pair<Anime, Episode> =
|
||||
{ _id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, next_update, initialized, viewerFlags, episodeFlags, coverLastModified, dateAdded, episodeId, animeId, chapterUrl, name, scanlator, seen, bookmark, lastSecondSeen, totalSeconds, episodeNumber, sourceOrder, dateFetch, dateUpload ->
|
||||
Anime(
|
||||
id = _id,
|
||||
source = source,
|
||||
favorite = favorite,
|
||||
lastUpdate = lastUpdate ?: 0,
|
||||
dateAdded = dateAdded,
|
||||
viewerFlags = viewerFlags,
|
||||
episodeFlags = episodeFlags,
|
||||
coverLastModified = coverLastModified,
|
||||
|
@ -46,46 +23,40 @@ val animeEpisodeMapper: (Long, Long, String, String?, String?, String?, List<Str
|
|||
genre = genre,
|
||||
status = status,
|
||||
thumbnailUrl = thumbnailUrl,
|
||||
updateStrategy = updateStrategy,
|
||||
initialized = initialized,
|
||||
) to Episode(
|
||||
id = episodeId,
|
||||
animeId = animeId,
|
||||
seen = seen,
|
||||
bookmark = bookmark,
|
||||
lastSecondSeen = lastSecondSeen,
|
||||
totalSeconds = totalSeconds,
|
||||
dateFetch = dateFetch,
|
||||
sourceOrder = sourceOrder,
|
||||
url = chapterUrl,
|
||||
name = name,
|
||||
dateUpload = dateUpload,
|
||||
episodeNumber = episodeNumber,
|
||||
scanlator = scanlator,
|
||||
)
|
||||
}
|
||||
|
||||
val animelibAnime: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, Long, Long, Long) -> AnimelibAnime =
|
||||
{ _id, source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update, next_update, initialized, viewer, episode_flags, cover_last_modified, date_added, unseen_count, seen_count, category ->
|
||||
AnimelibAnime().apply {
|
||||
this.id = _id
|
||||
this.source = source
|
||||
this.url = url
|
||||
this.artist = artist
|
||||
this.author = author
|
||||
this.description = description
|
||||
this.genre = genre?.joinToString()
|
||||
this.title = title
|
||||
this.status = status.toInt()
|
||||
this.thumbnail_url = thumbnail_url
|
||||
this.favorite = favorite
|
||||
this.last_update = last_update ?: 0
|
||||
this.initialized = initialized
|
||||
this.viewer_flags = viewer.toInt()
|
||||
this.episode_flags = episode_flags.toInt()
|
||||
this.cover_last_modified = cover_last_modified
|
||||
this.date_added = date_added
|
||||
this.unseenCount = unseen_count.toInt()
|
||||
this.seenCount = seen_count.toInt()
|
||||
this.category = category.toInt()
|
||||
}
|
||||
val animelibAnime: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, UpdateStrategy, Long, Long, Long, Long, Long, Long, Long) -> AnimelibAnime =
|
||||
{ id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, nextUpdate, initialized, viewerFlags, episodeFlags, coverLastModified, dateAdded, updateStrategy, totalCount, seenCount, latestUpload, episodeFetchedAt, lastSeen, bookmarkCount, category ->
|
||||
AnimelibAnime(
|
||||
anime = animeMapper(
|
||||
id,
|
||||
source,
|
||||
url,
|
||||
artist,
|
||||
author,
|
||||
description,
|
||||
genre,
|
||||
title,
|
||||
status,
|
||||
thumbnailUrl,
|
||||
favorite,
|
||||
lastUpdate,
|
||||
nextUpdate,
|
||||
initialized,
|
||||
viewerFlags,
|
||||
episodeFlags,
|
||||
coverLastModified,
|
||||
dateAdded,
|
||||
updateStrategy,
|
||||
),
|
||||
category = category,
|
||||
totalEpisodes = totalCount,
|
||||
seenCount = seenCount,
|
||||
bookmarkCount = bookmarkCount,
|
||||
latestUpload = latestUpload,
|
||||
episodeFetchedAt = episodeFetchedAt,
|
||||
lastSeen = lastSeen,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,12 +2,13 @@ package eu.kanade.data.anime
|
|||
|
||||
import eu.kanade.data.AnimeDatabaseHandler
|
||||
import eu.kanade.data.listOfStringsAdapter
|
||||
import eu.kanade.data.toLong
|
||||
import eu.kanade.data.updateStrategyAdapter
|
||||
import eu.kanade.domain.anime.model.Anime
|
||||
import eu.kanade.domain.anime.model.AnimeUpdate
|
||||
import eu.kanade.domain.anime.repository.AnimeRepository
|
||||
import eu.kanade.tachiyomi.data.database.models.AnimelibAnime
|
||||
import eu.kanade.domain.animelib.model.AnimelibAnime
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import eu.kanade.tachiyomi.util.system.toLong
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import logcat.LogPriority
|
||||
|
||||
|
@ -24,7 +25,11 @@ class AnimeRepositoryImpl(
|
|||
}
|
||||
|
||||
override suspend fun getAnimeByUrlAndSourceId(url: String, sourceId: Long): Anime? {
|
||||
return handler.awaitOneOrNull { animesQueries.getAnimeByUrlAndSource(url, sourceId, animeMapper) }
|
||||
return handler.awaitOneOrNull(inTransaction = true) { animesQueries.getAnimeByUrlAndSource(url, sourceId, animeMapper) }
|
||||
}
|
||||
|
||||
override fun getAnimeByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow<Anime?> {
|
||||
return handler.subscribeToOneOrNull { animesQueries.getAnimeByUrlAndSource(url, sourceId, animeMapper) }
|
||||
}
|
||||
|
||||
override suspend fun getFavorites(): List<Anime> {
|
||||
|
@ -32,11 +37,11 @@ class AnimeRepositoryImpl(
|
|||
}
|
||||
|
||||
override suspend fun getAnimelibAnime(): List<AnimelibAnime> {
|
||||
return handler.awaitList { animesQueries.getAnimelib(animelibAnime) }
|
||||
return handler.awaitList { animelibViewQueries.animelib(animelibAnime) }
|
||||
}
|
||||
|
||||
override fun getAnimelibAnimeAsFlow(): Flow<List<AnimelibAnime>> {
|
||||
return handler.subscribeToList { animesQueries.getAnimelib(animelibAnime) }
|
||||
return handler.subscribeToList { animelibViewQueries.animelib(animelibAnime) }
|
||||
}
|
||||
|
||||
override fun getFavoritesBySourceId(sourceId: Long): Flow<List<Anime>> {
|
||||
|
@ -69,7 +74,7 @@ class AnimeRepositoryImpl(
|
|||
}
|
||||
|
||||
override suspend fun insert(anime: Anime): Long? {
|
||||
return handler.awaitOneOrNull {
|
||||
return handler.awaitOneOrNull(inTransaction = true) {
|
||||
animesQueries.insert(
|
||||
source = anime.source,
|
||||
url = anime.url,
|
||||
|
@ -88,6 +93,7 @@ class AnimeRepositoryImpl(
|
|||
episodeFlags = anime.episodeFlags,
|
||||
coverLastModified = anime.coverLastModified,
|
||||
dateAdded = anime.dateAdded,
|
||||
updateStrategy = anime.updateStrategy,
|
||||
)
|
||||
animesQueries.selectLastInsertedRowId()
|
||||
}
|
||||
|
@ -103,9 +109,9 @@ class AnimeRepositoryImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun updateAll(values: List<AnimeUpdate>): Boolean {
|
||||
override suspend fun updateAll(animeUpdates: List<AnimeUpdate>): Boolean {
|
||||
return try {
|
||||
partialUpdate(*values.toTypedArray())
|
||||
partialUpdate(*animeUpdates.toTypedArray())
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
|
@ -113,9 +119,9 @@ class AnimeRepositoryImpl(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun partialUpdate(vararg values: AnimeUpdate) {
|
||||
private suspend fun partialUpdate(vararg animeUpdates: AnimeUpdate) {
|
||||
handler.await(inTransaction = true) {
|
||||
values.forEach { value ->
|
||||
animeUpdates.forEach { value ->
|
||||
animesQueries.update(
|
||||
source = value.source,
|
||||
url = value.url,
|
||||
|
@ -134,6 +140,7 @@ class AnimeRepositoryImpl(
|
|||
coverLastModified = value.coverLastModified,
|
||||
dateAdded = value.dateAdded,
|
||||
animeId = value.id,
|
||||
updateStrategy = value.updateStrategy?.let(updateStrategyAdapter::encode),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,21 @@
|
|||
package eu.kanade.data.animehistory
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import eu.kanade.data.AnimeDatabaseHandler
|
||||
import eu.kanade.data.anime.animeMapper
|
||||
import eu.kanade.data.episode.episodeMapper
|
||||
import eu.kanade.domain.anime.model.Anime
|
||||
import eu.kanade.domain.animehistory.model.AnimeHistoryUpdate
|
||||
import eu.kanade.domain.animehistory.model.AnimeHistoryWithRelations
|
||||
import eu.kanade.domain.animehistory.repository.AnimeHistoryRepository
|
||||
import eu.kanade.domain.episode.model.Episode
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import logcat.LogPriority
|
||||
|
||||
class AnimeHistoryRepositoryImpl(
|
||||
private val handler: AnimeDatabaseHandler,
|
||||
) : AnimeHistoryRepository {
|
||||
|
||||
override fun getHistory(query: String): PagingSource<Long, AnimeHistoryWithRelations> {
|
||||
return handler.subscribeToPagingSource(
|
||||
countQuery = { animehistoryViewQueries.countHistory(query) },
|
||||
queryProvider = { limit, offset ->
|
||||
animehistoryViewQueries.animehistory(query, limit, offset, animehistoryWithRelationsMapper)
|
||||
},
|
||||
)
|
||||
override fun getHistory(query: String): Flow<List<AnimeHistoryWithRelations>> {
|
||||
return handler.subscribeToList {
|
||||
animehistoryViewQueries.animehistory(query, animehistoryWithRelationsMapper)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getLastHistory(): AnimeHistoryWithRelations? {
|
||||
|
@ -31,45 +24,6 @@ class AnimeHistoryRepositoryImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getNextEpisode(animeId: Long, episodeId: Long): Episode? {
|
||||
val episode = handler.awaitOne { episodesQueries.getEpisodeById(episodeId, episodeMapper) }
|
||||
val anime = handler.awaitOne { animesQueries.getAnimeById(animeId, animeMapper) }
|
||||
|
||||
if (!episode.seen) {
|
||||
return episode
|
||||
}
|
||||
|
||||
val sortFunction: (Episode, Episode) -> Int = when (anime.sorting) {
|
||||
Anime.EPISODE_SORTING_SOURCE -> { c1, c2 -> c2.sourceOrder.compareTo(c1.sourceOrder) }
|
||||
Anime.EPISODE_SORTING_NUMBER -> { c1, c2 -> c1.episodeNumber.compareTo(c2.episodeNumber) }
|
||||
Anime.EPISODE_SORTING_UPLOAD_DATE -> { c1, c2 -> c1.dateUpload.compareTo(c2.dateUpload) }
|
||||
else -> throw NotImplementedError("Unknown sorting method")
|
||||
}
|
||||
|
||||
val episodes = handler.awaitList { episodesQueries.getEpisodesByAnimeId(animeId, episodeMapper) }
|
||||
.sortedWith(sortFunction)
|
||||
|
||||
val currEpisodeIndex = episodes.indexOfFirst { episode.id == it.id }
|
||||
return when (anime.sorting) {
|
||||
Anime.EPISODE_SORTING_SOURCE -> episodes.getOrNull(currEpisodeIndex + 1)
|
||||
Anime.EPISODE_SORTING_NUMBER -> {
|
||||
val episodeNumber = episode.episodeNumber
|
||||
|
||||
((currEpisodeIndex + 1) until episodes.size)
|
||||
.map { episodes[it] }
|
||||
.firstOrNull {
|
||||
it.episodeNumber > episodeNumber &&
|
||||
it.episodeNumber <= episodeNumber + 1
|
||||
}
|
||||
}
|
||||
Anime.EPISODE_SORTING_UPLOAD_DATE -> {
|
||||
episodes.drop(currEpisodeIndex + 1)
|
||||
.firstOrNull { it.dateUpload >= episode.dateUpload }
|
||||
}
|
||||
else -> throw NotImplementedError("Unknown sorting method")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun resetHistory(historyId: Long) {
|
||||
try {
|
||||
handler.await { animehistoryQueries.resetAnimeHistoryById(historyId) }
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package eu.kanade.data.animesource
|
||||
|
||||
import eu.kanade.data.AnimeDatabaseHandler
|
||||
import eu.kanade.domain.animesource.model.AnimeSourceData
|
||||
import eu.kanade.domain.animesource.repository.AnimeSourceDataRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class AnimeSourceDataRepositoryImpl(
|
||||
private val handler: AnimeDatabaseHandler,
|
||||
) : AnimeSourceDataRepository {
|
||||
|
||||
override fun subscribeAll(): Flow<List<AnimeSourceData>> {
|
||||
return handler.subscribeToList { animesourcesQueries.findAll(animesourceDataMapper) }
|
||||
}
|
||||
|
||||
override suspend fun getSourceData(id: Long): AnimeSourceData? {
|
||||
return handler.awaitOneOrNull { animesourcesQueries.findOne(id, animesourceDataMapper) }
|
||||
}
|
||||
|
||||
override suspend fun upsertSourceData(id: Long, lang: String, name: String) {
|
||||
handler.await { animesourcesQueries.upsert(id, lang, name) }
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ val animesourceMapper: (eu.kanade.tachiyomi.animesource.AnimeSource) -> AnimeSou
|
|||
source.lang,
|
||||
source.name,
|
||||
supportsLatest = false,
|
||||
isStub = source is AnimeSourceManager.StubSource,
|
||||
isStub = source is AnimeSourceManager.StubAnimeSource,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package eu.kanade.data.animesource
|
||||
|
||||
import androidx.paging.PagingState
|
||||
import eu.kanade.data.episode.NoEpisodesException
|
||||
import eu.kanade.domain.animesource.model.AnimeSourcePagingSourceType
|
||||
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
|
||||
abstract class AnimeSourcePagingSource(
|
||||
protected val source: AnimeCatalogueSource,
|
||||
) : AnimeSourcePagingSourceType() {
|
||||
|
||||
abstract suspend fun requestNextPage(currentPage: Int): AnimesPage
|
||||
|
||||
override suspend fun load(params: LoadParams<Long>): LoadResult<Long, SAnime> {
|
||||
val page = params.key ?: 1
|
||||
|
||||
val animesPage = try {
|
||||
withIOContext {
|
||||
requestNextPage(page.toInt())
|
||||
.takeIf { it.animes.isNotEmpty() }
|
||||
?: throw NoEpisodesException()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return LoadResult.Error(e)
|
||||
}
|
||||
|
||||
return LoadResult.Page(
|
||||
data = animesPage.animes,
|
||||
prevKey = null,
|
||||
nextKey = if (animesPage.hasNextPage) page + 1 else null,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Long, SAnime>): Long? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
val anchorPage = state.closestPageToPosition(anchorPosition)
|
||||
anchorPage?.prevKey ?: anchorPage?.nextKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AnimeSourceSearchPagingSource(source: AnimeCatalogueSource, val query: String, val filters: AnimeFilterList) : AnimeSourcePagingSource(source) {
|
||||
override suspend fun requestNextPage(currentPage: Int): AnimesPage {
|
||||
return source.fetchSearchAnime(currentPage, query, filters).awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
class AnimeSourcePopularPagingSource(source: AnimeCatalogueSource) : AnimeSourcePagingSource(source) {
|
||||
override suspend fun requestNextPage(currentPage: Int): AnimesPage {
|
||||
return source.fetchPopularAnime(currentPage).awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
class AnimeSourceLatestPagingSource(source: AnimeCatalogueSource) : AnimeSourcePagingSource(source) {
|
||||
override suspend fun requestNextPage(currentPage: Int): AnimesPage {
|
||||
return source.fetchLatestUpdates(currentPage).awaitSingle()
|
||||
}
|
||||
}
|
|
@ -2,13 +2,15 @@ package eu.kanade.data.animesource
|
|||
|
||||
import eu.kanade.data.AnimeDatabaseHandler
|
||||
import eu.kanade.domain.animesource.model.AnimeSource
|
||||
import eu.kanade.domain.animesource.model.AnimeSourceData
|
||||
import eu.kanade.domain.animesource.model.AnimeSourcePagingSourceType
|
||||
import eu.kanade.domain.animesource.model.AnimeSourceWithCount
|
||||
import eu.kanade.domain.animesource.repository.AnimeSourceRepository
|
||||
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
|
||||
import eu.kanade.tachiyomi.animesource.LocalAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSource as LoadedAnimeSource
|
||||
|
||||
class AnimeSourceRepositoryImpl(
|
||||
private val sourceManager: AnimeSourceManager,
|
||||
|
@ -41,21 +43,32 @@ class AnimeSourceRepositoryImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getSourcesWithNonLibraryAnime(): Flow<List<Pair<LoadedAnimeSource, Long>>> {
|
||||
override fun getSourcesWithNonLibraryAnime(): Flow<List<AnimeSourceWithCount>> {
|
||||
val sourceIdWithNonLibraryAnime = handler.subscribeToList { animesQueries.getSourceIdsWithNonLibraryAnime() }
|
||||
return sourceIdWithNonLibraryAnime.map { sourceId ->
|
||||
sourceId.map { (sourceId, count) ->
|
||||
val source = sourceManager.getOrStub(sourceId)
|
||||
source to count
|
||||
AnimeSourceWithCount(animesourceMapper(source), count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAnimeSourceData(id: Long): AnimeSourceData? {
|
||||
return handler.awaitOneOrNull { animesourcesQueries.getAnimeSourceData(id, animesourceDataMapper) }
|
||||
override fun search(
|
||||
sourceId: Long,
|
||||
query: String,
|
||||
filterList: AnimeFilterList,
|
||||
): AnimeSourcePagingSourceType {
|
||||
val source = sourceManager.get(sourceId) as AnimeCatalogueSource
|
||||
return AnimeSourceSearchPagingSource(source, query, filterList)
|
||||
}
|
||||
|
||||
override suspend fun upsertAnimeSourceData(id: Long, lang: String, name: String) {
|
||||
handler.await { animesourcesQueries.upsert(id, lang, name) }
|
||||
override fun getPopular(sourceId: Long): AnimeSourcePagingSourceType {
|
||||
val source = sourceManager.get(sourceId) as AnimeCatalogueSource
|
||||
return AnimeSourcePopularPagingSource(source)
|
||||
}
|
||||
|
||||
override fun getLatest(sourceId: Long): AnimeSourcePagingSourceType {
|
||||
val source = sourceManager.get(sourceId) as AnimeCatalogueSource
|
||||
return AnimeSourceLatestPagingSource(source)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,9 +44,9 @@ class AnimeTrackRepositoryImpl(
|
|||
insertValues(*tracks.toTypedArray())
|
||||
}
|
||||
|
||||
private suspend fun insertValues(vararg values: AnimeTrack) {
|
||||
private suspend fun insertValues(vararg tracks: AnimeTrack) {
|
||||
handler.await(inTransaction = true) {
|
||||
values.forEach { animeTrack ->
|
||||
tracks.forEach { animeTrack ->
|
||||
anime_syncQueries.insert(
|
||||
animeId = animeTrack.animeId,
|
||||
syncId = animeTrack.syncId,
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package eu.kanade.data.animeupdates
|
||||
|
||||
import eu.kanade.domain.animeupdates.model.AnimeUpdatesWithRelations
|
||||
import eu.kanade.domain.manga.model.MangaCover
|
||||
|
||||
val updateWithRelationMapper: (Long, String, Long, String, String?, Boolean, Boolean, Long, Boolean, String?, Long, Long, Long) -> AnimeUpdatesWithRelations = {
|
||||
animeId, animeTitle, episodeId, episodeName, scanlator, seen, bookmark, sourceId, favorite, thumbnailUrl, coverLastModified, _, dateFetch ->
|
||||
AnimeUpdatesWithRelations(
|
||||
animeId = animeId,
|
||||
animeTitle = animeTitle,
|
||||
episodeId = episodeId,
|
||||
episodeName = episodeName,
|
||||
scanlator = scanlator,
|
||||
seen = seen,
|
||||
bookmark = bookmark,
|
||||
sourceId = sourceId,
|
||||
dateFetch = dateFetch,
|
||||
coverData = MangaCover(
|
||||
mangaId = animeId,
|
||||
sourceId = sourceId,
|
||||
isMangaFavorite = favorite,
|
||||
url = thumbnailUrl,
|
||||
lastModified = coverLastModified,
|
||||
),
|
||||
)
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package eu.kanade.data.animeupdates
|
||||
|
||||
import eu.kanade.data.AnimeDatabaseHandler
|
||||
import eu.kanade.domain.animeupdates.model.AnimeUpdatesWithRelations
|
||||
import eu.kanade.domain.animeupdates.repository.AnimeUpdatesRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class AnimeUpdatesRepositoryImpl(
|
||||
val databaseHandler: AnimeDatabaseHandler,
|
||||
) : AnimeUpdatesRepository {
|
||||
|
||||
override fun subscribeAll(after: Long): Flow<List<AnimeUpdatesWithRelations>> {
|
||||
return databaseHandler.subscribeToList {
|
||||
animeupdatesViewQueries.animeupdates(after, updateWithRelationMapper)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,13 +4,17 @@ import eu.kanade.data.AnimeDatabaseHandler
|
|||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
|
||||
import eu.kanade.domain.category.repository.DuplicateNameException
|
||||
import eu.kanade.tachiyomi.mi.AnimeDatabase
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class CategoryRepositoryImplAnime(
|
||||
private val handler: AnimeDatabaseHandler,
|
||||
) : CategoryRepositoryAnime {
|
||||
|
||||
override suspend fun get(id: Long): Category? {
|
||||
return handler.awaitOneOrNull { categoriesQueries.getCategory(id, categoryMapper) }
|
||||
}
|
||||
|
||||
override suspend fun getAll(): List<Category> {
|
||||
return handler.awaitList { categoriesQueries.getCategories(categoryMapper) }
|
||||
}
|
||||
|
@ -31,29 +35,43 @@ class CategoryRepositoryImplAnime(
|
|||
}
|
||||
}
|
||||
|
||||
@Throws(DuplicateNameException::class)
|
||||
override suspend fun insert(name: String, order: Long) {
|
||||
if (checkDuplicateName(name)) throw DuplicateNameException(name)
|
||||
override suspend fun insert(category: Category) {
|
||||
handler.await {
|
||||
categoriesQueries.insert(
|
||||
name = name,
|
||||
order = order,
|
||||
flags = 0L,
|
||||
name = category.name,
|
||||
order = category.order,
|
||||
flags = category.flags,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(DuplicateNameException::class)
|
||||
override suspend fun update(payload: CategoryUpdate) {
|
||||
if (payload.name != null && checkDuplicateName(payload.name)) throw DuplicateNameException(payload.name)
|
||||
override suspend fun updatePartial(update: CategoryUpdate) {
|
||||
handler.await {
|
||||
updatePartialBlocking(update)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updatePartial(updates: List<CategoryUpdate>) {
|
||||
handler.await(inTransaction = true) {
|
||||
for (update in updates) {
|
||||
updatePartialBlocking(update)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun AnimeDatabase.updatePartialBlocking(update: CategoryUpdate) {
|
||||
categoriesQueries.update(
|
||||
name = payload.name,
|
||||
order = payload.order,
|
||||
flags = payload.flags,
|
||||
categoryId = payload.id,
|
||||
name = update.name,
|
||||
order = update.order,
|
||||
flags = update.flags,
|
||||
categoryId = update.id,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun updateAllFlags(flags: Long?) {
|
||||
handler.await {
|
||||
categoriesQueries.updateAllFlags(flags)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(categoryId: Long) {
|
||||
|
@ -63,10 +81,4 @@ class CategoryRepositoryImplAnime(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun checkDuplicateName(name: String): Boolean {
|
||||
return handler
|
||||
.awaitList { categoriesQueries.getCategories() }
|
||||
.any { it.name == name }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package eu.kanade.data.episode
|
||||
|
||||
object CleanupEpisodeName {
|
||||
|
||||
fun await(episodeName: String, animeTitle: String): String {
|
||||
return episodeName
|
||||
.trim()
|
||||
.removePrefix(animeTitle)
|
||||
.trim(*EPISODE_TRIM_CHARS)
|
||||
}
|
||||
|
||||
private val EPISODE_TRIM_CHARS = arrayOf(
|
||||
// Whitespace
|
||||
' ',
|
||||
'\u0009',
|
||||
'\u000A',
|
||||
'\u000B',
|
||||
'\u000C',
|
||||
'\u000D',
|
||||
'\u0020',
|
||||
'\u0085',
|
||||
'\u00A0',
|
||||
'\u1680',
|
||||
'\u2000',
|
||||
'\u2001',
|
||||
'\u2002',
|
||||
'\u2003',
|
||||
'\u2004',
|
||||
'\u2005',
|
||||
'\u2006',
|
||||
'\u2007',
|
||||
'\u2008',
|
||||
'\u2009',
|
||||
'\u200A',
|
||||
'\u2028',
|
||||
'\u2029',
|
||||
'\u202F',
|
||||
'\u205F',
|
||||
'\u3000',
|
||||
|
||||
// Separators
|
||||
'-',
|
||||
'_',
|
||||
',',
|
||||
':',
|
||||
).toCharArray()
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
package eu.kanade.data.episode
|
||||
|
||||
import eu.kanade.data.AnimeDatabaseHandler
|
||||
import eu.kanade.data.toLong
|
||||
import eu.kanade.domain.episode.model.Episode
|
||||
import eu.kanade.domain.episode.model.EpisodeUpdate
|
||||
import eu.kanade.domain.episode.repository.EpisodeRepository
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import eu.kanade.tachiyomi.util.system.toLong
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import logcat.LogPriority
|
||||
|
||||
|
@ -83,6 +83,10 @@ class EpisodeRepositoryImpl(
|
|||
return handler.awaitList { episodesQueries.getEpisodesByAnimeId(animeId, episodeMapper) }
|
||||
}
|
||||
|
||||
override suspend fun getBookmarkedEpisodesByAnimeId(animeId: Long): List<Episode> {
|
||||
return handler.awaitList { episodesQueries.getBookmarkedEpisodesByAnimeId(animeId, episodeMapper) }
|
||||
}
|
||||
|
||||
override suspend fun getEpisodeById(id: Long): Episode? {
|
||||
return handler.awaitOneOrNull { episodesQueries.getEpisodeById(id, episodeMapper) }
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
package eu.kanade.data.source
|
||||
|
||||
class NoResultsException : Exception()
|
|
@ -1,6 +1,7 @@
|
|||
package eu.kanade.data.source
|
||||
|
||||
import androidx.paging.PagingState
|
||||
import eu.kanade.data.chapter.NoChaptersException
|
||||
import eu.kanade.domain.source.model.SourcePagingSourceType
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
|
@ -22,7 +23,7 @@ abstract class SourcePagingSource(
|
|||
withIOContext {
|
||||
requestNextPage(page.toInt())
|
||||
.takeIf { it.mangas.isNotEmpty() }
|
||||
?: throw NoResultsException()
|
||||
?: throw NoChaptersException()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return LoadResult.Error(e)
|
||||
|
|
|
@ -2,8 +2,10 @@ package eu.kanade.domain
|
|||
|
||||
import eu.kanade.data.anime.AnimeRepositoryImpl
|
||||
import eu.kanade.data.animehistory.AnimeHistoryRepositoryImpl
|
||||
import eu.kanade.data.animesource.AnimeSourceDataRepositoryImpl
|
||||
import eu.kanade.data.animesource.AnimeSourceRepositoryImpl
|
||||
import eu.kanade.data.animetrack.AnimeTrackRepositoryImpl
|
||||
import eu.kanade.data.animeupdates.AnimeUpdatesRepositoryImpl
|
||||
import eu.kanade.data.category.CategoryRepositoryImpl
|
||||
import eu.kanade.data.category.CategoryRepositoryImplAnime
|
||||
import eu.kanade.data.chapter.ChapterRepositoryImpl
|
||||
|
@ -13,10 +15,12 @@ import eu.kanade.data.manga.MangaRepositoryImpl
|
|||
import eu.kanade.data.source.SourceDataRepositoryImpl
|
||||
import eu.kanade.data.source.SourceRepositoryImpl
|
||||
import eu.kanade.data.track.TrackRepositoryImpl
|
||||
import eu.kanade.data.updates.UpdatesRepositoryImpl
|
||||
import eu.kanade.domain.anime.interactor.GetAnime
|
||||
import eu.kanade.domain.anime.interactor.GetAnimeWithEpisodes
|
||||
import eu.kanade.domain.anime.interactor.GetAnimelibAnime
|
||||
import eu.kanade.domain.anime.interactor.GetDuplicateLibraryAnime
|
||||
import eu.kanade.domain.anime.interactor.NetworkToLocalAnime
|
||||
import eu.kanade.domain.anime.interactor.SetAnimeEpisodeFlags
|
||||
import eu.kanade.domain.anime.interactor.SetAnimeViewerFlags
|
||||
import eu.kanade.domain.anime.interactor.UpdateAnime
|
||||
|
@ -24,47 +28,50 @@ import eu.kanade.domain.anime.repository.AnimeRepository
|
|||
import eu.kanade.domain.animedownload.interactor.DeleteAnimeDownload
|
||||
import eu.kanade.domain.animeextension.interactor.GetAnimeExtensionLanguages
|
||||
import eu.kanade.domain.animeextension.interactor.GetAnimeExtensionSources
|
||||
import eu.kanade.domain.animeextension.interactor.GetAnimeExtensionUpdates
|
||||
import eu.kanade.domain.animeextension.interactor.GetAnimeExtensions
|
||||
import eu.kanade.domain.animehistory.interactor.DeleteAnimeHistoryTable
|
||||
import eu.kanade.domain.animeextension.interactor.GetAnimeExtensionsByType
|
||||
import eu.kanade.domain.animehistory.interactor.DeleteAllAnimeHistory
|
||||
import eu.kanade.domain.animehistory.interactor.GetAnimeHistory
|
||||
import eu.kanade.domain.animehistory.interactor.GetNextEpisode
|
||||
import eu.kanade.domain.animehistory.interactor.RemoveAnimeHistoryByAnimeId
|
||||
import eu.kanade.domain.animehistory.interactor.RemoveAnimeHistoryById
|
||||
import eu.kanade.domain.animehistory.interactor.UpsertAnimeHistory
|
||||
import eu.kanade.domain.animehistory.repository.AnimeHistoryRepository
|
||||
import eu.kanade.domain.animesource.interactor.GetAnimeSourceData
|
||||
import eu.kanade.domain.animesource.interactor.GetAnimeSourcesWithFavoriteCount
|
||||
import eu.kanade.domain.animesource.interactor.GetAnimeSourcesWithNonLibraryAnime
|
||||
import eu.kanade.domain.animesource.interactor.GetEnabledAnimeSources
|
||||
import eu.kanade.domain.animesource.interactor.GetLanguagesWithAnimeSources
|
||||
import eu.kanade.domain.animesource.interactor.GetRemoteAnime
|
||||
import eu.kanade.domain.animesource.interactor.ToggleAnimeSource
|
||||
import eu.kanade.domain.animesource.interactor.ToggleAnimeSourcePin
|
||||
import eu.kanade.domain.animesource.interactor.UpsertAnimeSourceData
|
||||
import eu.kanade.domain.animesource.repository.AnimeSourceDataRepository
|
||||
import eu.kanade.domain.animesource.repository.AnimeSourceRepository
|
||||
import eu.kanade.domain.animetrack.interactor.DeleteAnimeTrack
|
||||
import eu.kanade.domain.animetrack.interactor.GetAnimeTracks
|
||||
import eu.kanade.domain.animetrack.interactor.GetTracksPerAnime
|
||||
import eu.kanade.domain.animetrack.interactor.InsertAnimeTrack
|
||||
import eu.kanade.domain.animetrack.repository.AnimeTrackRepository
|
||||
import eu.kanade.data.updates.UpdatesRepositoryImpl
|
||||
import eu.kanade.domain.animeupdates.interactor.GetAnimeUpdates
|
||||
import eu.kanade.domain.animeupdates.repository.AnimeUpdatesRepository
|
||||
import eu.kanade.domain.category.interactor.CreateAnimeCategoryWithName
|
||||
import eu.kanade.domain.category.interactor.CreateCategoryWithName
|
||||
import eu.kanade.domain.category.interactor.DeleteAnimeCategory
|
||||
import eu.kanade.domain.category.interactor.DeleteCategory
|
||||
import eu.kanade.domain.category.interactor.DeleteCategoryAnime
|
||||
import eu.kanade.domain.category.interactor.GetAnimeCategories
|
||||
import eu.kanade.domain.category.interactor.GetCategories
|
||||
import eu.kanade.domain.category.interactor.GetCategoriesAnime
|
||||
import eu.kanade.domain.category.interactor.SetAnimeCategories
|
||||
import eu.kanade.domain.category.interactor.RenameCategory
|
||||
import eu.kanade.domain.category.interactor.RenameAnimeCategory
|
||||
import eu.kanade.domain.category.interactor.ReorderCategory
|
||||
import eu.kanade.domain.category.interactor.RenameCategory
|
||||
import eu.kanade.domain.category.interactor.ReorderAnimeCategory
|
||||
import eu.kanade.domain.category.interactor.ResetCategoryFlags
|
||||
import eu.kanade.domain.category.interactor.ReorderCategory
|
||||
import eu.kanade.domain.category.interactor.ResetAnimeCategoryFlags
|
||||
import eu.kanade.domain.category.interactor.SetDisplayModeForCategory
|
||||
import eu.kanade.domain.category.interactor.ResetCategoryFlags
|
||||
import eu.kanade.domain.category.interactor.SetAnimeCategories
|
||||
import eu.kanade.domain.category.interactor.SetDisplayModeForAnimeCategory
|
||||
import eu.kanade.domain.category.interactor.SetDisplayModeForCategory
|
||||
import eu.kanade.domain.category.interactor.SetMangaCategories
|
||||
import eu.kanade.domain.category.interactor.SetSortModeForAnimeCategory
|
||||
import eu.kanade.domain.category.interactor.SetSortModeForCategory
|
||||
import eu.kanade.domain.category.interactor.UpdateAnimeCategory
|
||||
import eu.kanade.domain.category.interactor.UpdateCategory
|
||||
import eu.kanade.domain.category.interactor.UpdateCategoryAnime
|
||||
import eu.kanade.domain.category.repository.CategoryRepository
|
||||
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
|
||||
import eu.kanade.domain.chapter.interactor.GetChapter
|
||||
|
@ -79,6 +86,7 @@ import eu.kanade.domain.chapter.repository.ChapterRepository
|
|||
import eu.kanade.domain.download.interactor.DeleteDownload
|
||||
import eu.kanade.domain.episode.interactor.GetEpisode
|
||||
import eu.kanade.domain.episode.interactor.GetEpisodeByAnimeId
|
||||
import eu.kanade.domain.episode.interactor.SetAnimeDefaultEpisodeFlags
|
||||
import eu.kanade.domain.episode.interactor.SetSeenStatus
|
||||
import eu.kanade.domain.episode.interactor.ShouldUpdateDbEpisode
|
||||
import eu.kanade.domain.episode.interactor.SyncEpisodesWithSource
|
||||
|
@ -129,15 +137,14 @@ import uy.kohesive.injekt.api.InjektRegistrar
|
|||
import uy.kohesive.injekt.api.addFactory
|
||||
import uy.kohesive.injekt.api.addSingletonFactory
|
||||
import uy.kohesive.injekt.api.get
|
||||
import eu.kanade.domain.anime.interactor.GetFavorites as GetFavoritesAnime
|
||||
import eu.kanade.domain.anime.interactor.GetAnimeFavorites as GetFavoritesAnime
|
||||
import eu.kanade.domain.anime.interactor.ResetViewerFlags as ResetViewerFlagsAnime
|
||||
|
||||
class DomainModule : InjektModule {
|
||||
|
||||
override fun InjektRegistrar.registerInjectables() {
|
||||
|
||||
addSingletonFactory<CategoryRepositoryAnime> { CategoryRepositoryImplAnime(get()) }
|
||||
addFactory { GetCategoriesAnime(get()) }
|
||||
addFactory { GetAnimeCategories(get()) }
|
||||
addFactory { ResetAnimeCategoryFlags(get(), get()) }
|
||||
addFactory { SetDisplayModeForAnimeCategory(get(), get()) }
|
||||
addFactory { SetSortModeForAnimeCategory(get(), get()) }
|
||||
|
@ -190,7 +197,7 @@ class DomainModule : InjektModule {
|
|||
|
||||
addSingletonFactory<AnimeTrackRepository> { AnimeTrackRepositoryImpl(get()) }
|
||||
addFactory { DeleteAnimeTrack(get()) }
|
||||
addFactory { GetAnimeTracksPerAnime(get())
|
||||
addFactory { GetTracksPerAnime(get()) }
|
||||
addFactory { GetAnimeTracks(get()) }
|
||||
addFactory { InsertAnimeTrack(get()) }
|
||||
|
||||
|
@ -200,15 +207,6 @@ class DomainModule : InjektModule {
|
|||
addFactory { GetTracks(get()) }
|
||||
addFactory { InsertTrack(get()) }
|
||||
|
||||
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
|
||||
addFactory { GetChapter(get()) }
|
||||
addFactory { GetChapterByMangaId(get()) }
|
||||
addFactory { UpdateChapter(get()) }
|
||||
addFactory { SetReadStatus(get(), get(), get(), get()) }
|
||||
addFactory { ShouldUpdateDbChapter() }
|
||||
addFactory { SyncChaptersWithSource(get(), get(), get(), get()) }
|
||||
addFactory { SyncChaptersWithTrackServiceTwoWay(get(), get()) }
|
||||
|
||||
addSingletonFactory<EpisodeRepository> { EpisodeRepositoryImpl(get()) }
|
||||
addFactory { GetEpisode(get()) }
|
||||
addFactory { GetEpisodeByAnimeId(get()) }
|
||||
|
@ -218,6 +216,15 @@ class DomainModule : InjektModule {
|
|||
addFactory { SyncEpisodesWithSource(get(), get(), get(), get()) }
|
||||
addFactory { SyncEpisodesWithTrackServiceTwoWay(get(), get()) }
|
||||
|
||||
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
|
||||
addFactory { GetChapter(get()) }
|
||||
addFactory { GetChapterByMangaId(get()) }
|
||||
addFactory { UpdateChapter(get()) }
|
||||
addFactory { SetReadStatus(get(), get(), get(), get()) }
|
||||
addFactory { ShouldUpdateDbChapter() }
|
||||
addFactory { SyncChaptersWithSource(get(), get(), get(), get()) }
|
||||
addFactory { SyncChaptersWithTrackServiceTwoWay(get(), get()) }
|
||||
|
||||
addSingletonFactory<AnimeHistoryRepository> { AnimeHistoryRepositoryImpl(get()) }
|
||||
addFactory { DeleteAllAnimeHistory(get()) }
|
||||
addFactory { GetAnimeHistory(get()) }
|
||||
|
@ -257,8 +264,6 @@ class DomainModule : InjektModule {
|
|||
addFactory { GetRemoteAnime(get()) }
|
||||
addFactory { GetAnimeSourcesWithFavoriteCount(get(), get()) }
|
||||
addFactory { GetAnimeSourcesWithNonLibraryAnime(get()) }
|
||||
addFactory { SetAnimeMigrateSorting(get()) }
|
||||
addFactory { ToggleAnimeLanguage(get()) }
|
||||
addFactory { ToggleAnimeSource(get()) }
|
||||
addFactory { ToggleAnimeSourcePin(get()) }
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ class GetAnime(
|
|||
return animeRepository.getAnimeByIdAsFlow(id)
|
||||
}
|
||||
|
||||
suspend fun await(url: String, sourceId: Long): Anime? {
|
||||
return animeRepository.getAnimeByUrlAndSourceId(url, sourceId)
|
||||
fun subscribe(url: String, sourceId: Long): Flow<Anime?> {
|
||||
return animeRepository.getAnimeByUrlAndSourceIdAsFlow(url, sourceId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import eu.kanade.domain.anime.model.Anime
|
|||
import eu.kanade.domain.anime.repository.AnimeRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class GetFavorites(
|
||||
class GetAnimeFavorites(
|
||||
private val animeRepository: AnimeRepository,
|
||||
) {
|
||||
|
|
@ -24,4 +24,8 @@ class GetAnimeWithEpisodes(
|
|||
suspend fun awaitAnime(id: Long): Anime {
|
||||
return animeRepository.getAnimeById(id)
|
||||
}
|
||||
|
||||
suspend fun awaitEpisodes(id: Long): List<Episode> {
|
||||
return episodeRepository.getEpisodeByAnimeId(id)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package eu.kanade.domain.anime.interactor
|
||||
|
||||
import eu.kanade.domain.anime.repository.AnimeRepository
|
||||
import eu.kanade.tachiyomi.data.database.models.AnimelibAnime
|
||||
import eu.kanade.domain.animelib.model.AnimelibAnime
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class GetAnimelibAnime(
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
package eu.kanade.domain.anime.interactor
|
||||
|
||||
import eu.kanade.domain.anime.model.Anime
|
||||
import eu.kanade.domain.anime.repository.AnimeRepository
|
||||
|
||||
class InsertAnime(
|
||||
private val animeRepository: AnimeRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(anime: Anime): Long? {
|
||||
return animeRepository.insert(anime)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package eu.kanade.domain.anime.interactor
|
||||
|
||||
import eu.kanade.domain.anime.model.Anime
|
||||
import eu.kanade.domain.anime.repository.AnimeRepository
|
||||
|
||||
class NetworkToLocalAnime(
|
||||
private val animeRepository: AnimeRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(anime: Anime, sourceId: Long): Anime {
|
||||
val localAnime = getAnime(anime.url, sourceId)
|
||||
return when {
|
||||
localAnime == null -> {
|
||||
val id = insertAnime(anime.copy(source = sourceId))
|
||||
anime.copy(id = id!!)
|
||||
}
|
||||
!localAnime.favorite -> {
|
||||
// if the anime isn't a favorite, set its display title from source
|
||||
// if it later becomes a favorite, updated title will go to db
|
||||
localAnime.copy(title = anime.title)
|
||||
}
|
||||
else -> {
|
||||
localAnime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getAnime(url: String, sourceId: Long): Anime? {
|
||||
return animeRepository.getAnimeByUrlAndSourceId(url, sourceId)
|
||||
}
|
||||
|
||||
private suspend fun insertAnime(anime: Anime): Long? {
|
||||
return animeRepository.insert(anime)
|
||||
}
|
||||
}
|
|
@ -6,8 +6,8 @@ import eu.kanade.domain.anime.model.hasCustomCover
|
|||
import eu.kanade.domain.anime.model.isLocal
|
||||
import eu.kanade.domain.anime.model.toDbAnime
|
||||
import eu.kanade.domain.anime.repository.AnimeRepository
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
|
||||
import tachiyomi.animesource.model.AnimeInfo
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
|
@ -20,23 +20,30 @@ class UpdateAnime(
|
|||
return animeRepository.update(animeUpdate)
|
||||
}
|
||||
|
||||
suspend fun awaitAll(values: List<AnimeUpdate>): Boolean {
|
||||
return animeRepository.updateAll(values)
|
||||
suspend fun awaitAll(animeUpdates: List<AnimeUpdate>): Boolean {
|
||||
return animeRepository.updateAll(animeUpdates)
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateFromSource(
|
||||
localAnime: Anime,
|
||||
remoteAnime: AnimeInfo,
|
||||
remoteAnime: SAnime,
|
||||
manualFetch: Boolean,
|
||||
coverCache: AnimeCoverCache = Injekt.get(),
|
||||
): Boolean {
|
||||
// if the anime isn't a favorite, set its title from source and update in db
|
||||
val title = if (!localAnime.favorite) remoteAnime.title else null
|
||||
val remoteTitle = try {
|
||||
remoteAnime.title
|
||||
} catch (_: UninitializedPropertyAccessException) {
|
||||
""
|
||||
}
|
||||
|
||||
// Never refresh covers if the url is empty to avoid "losing" existing covers
|
||||
val updateCover = remoteAnime.cover.isNotEmpty() && (manualFetch || localAnime.thumbnailUrl != remoteAnime.cover)
|
||||
val coverLastModified = if (updateCover) {
|
||||
// if the anime isn't a favorite, set its title from source and update in db
|
||||
val title = if (remoteTitle.isEmpty() || localAnime.favorite) null else remoteTitle
|
||||
|
||||
val coverLastModified =
|
||||
when {
|
||||
// Never refresh covers if the url is empty to avoid "losing" existing covers
|
||||
remoteAnime.thumbnail_url.isNullOrEmpty() -> null
|
||||
!manualFetch && localAnime.thumbnailUrl == remoteAnime.thumbnail_url -> null
|
||||
localAnime.isLocal() -> Date().time
|
||||
localAnime.hasCustomCover(coverCache) -> {
|
||||
coverCache.deleteFromCache(localAnime.toDbAnime(), false)
|
||||
|
@ -47,19 +54,21 @@ class UpdateAnime(
|
|||
Date().time
|
||||
}
|
||||
}
|
||||
} else null
|
||||
|
||||
val thumbnailUrl = remoteAnime.thumbnail_url?.takeIf { it.isNotEmpty() }
|
||||
|
||||
return animeRepository.update(
|
||||
AnimeUpdate(
|
||||
id = localAnime.id,
|
||||
title = title?.takeIf { it.isNotEmpty() },
|
||||
title = title,
|
||||
coverLastModified = coverLastModified,
|
||||
author = remoteAnime.author,
|
||||
artist = remoteAnime.artist,
|
||||
description = remoteAnime.description,
|
||||
genre = remoteAnime.genres,
|
||||
thumbnailUrl = remoteAnime.cover.takeIf { it.isNotEmpty() },
|
||||
genre = remoteAnime.getGenres(),
|
||||
thumbnailUrl = thumbnailUrl,
|
||||
status = remoteAnime.status.toLong(),
|
||||
updateStrategy = remoteAnime.update_strategy,
|
||||
initialized = true,
|
||||
),
|
||||
)
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
package eu.kanade.domain.anime.model
|
||||
|
||||
import eu.kanade.data.listOfStringsAdapter
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
|
||||
import eu.kanade.tachiyomi.data.database.models.AnimeImpl
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import tachiyomi.animesource.model.AnimeInfo
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.Serializable
|
||||
|
@ -30,6 +30,7 @@ data class Anime(
|
|||
val genre: List<String>?,
|
||||
val status: Long,
|
||||
val thumbnailUrl: String?,
|
||||
val updateStrategy: UpdateStrategy,
|
||||
val initialized: Boolean,
|
||||
) : Serializable {
|
||||
|
||||
|
@ -79,7 +80,7 @@ data class Anime(
|
|||
}
|
||||
|
||||
fun forceDownloaded(): Boolean {
|
||||
return favorite && Injekt.get<PreferencesHelper>().downloadedOnly().get()
|
||||
return favorite && Injekt.get<BasePreferences>().downloadedOnly().get()
|
||||
}
|
||||
|
||||
fun sortDescending(): Boolean {
|
||||
|
@ -98,6 +99,28 @@ data class Anime(
|
|||
it.initialized = initialized
|
||||
}
|
||||
|
||||
fun copyFrom(other: SAnime): Anime {
|
||||
val author = other.author ?: author
|
||||
val artist = other.artist ?: artist
|
||||
val description = other.description ?: description
|
||||
val genres = if (other.genre != null) {
|
||||
other.getGenres()
|
||||
} else {
|
||||
genre
|
||||
}
|
||||
val thumbnailUrl = other.thumbnail_url ?: thumbnailUrl
|
||||
return this.copy(
|
||||
author = author,
|
||||
artist = artist,
|
||||
description = description,
|
||||
genre = genres,
|
||||
thumbnailUrl = thumbnailUrl,
|
||||
status = other.status.toLong(),
|
||||
updateStrategy = other.update_strategy,
|
||||
initialized = other.initialized && initialized,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Generic filter that does not filter anything
|
||||
const val SHOW_ALL = 0x00000000L
|
||||
|
@ -133,17 +156,18 @@ data class Anime(
|
|||
title = "",
|
||||
source = -1L,
|
||||
favorite = false,
|
||||
lastUpdate = -1L,
|
||||
dateAdded = -1L,
|
||||
viewerFlags = -1L,
|
||||
episodeFlags = -1L,
|
||||
coverLastModified = -1L,
|
||||
lastUpdate = 0L,
|
||||
dateAdded = 0L,
|
||||
viewerFlags = 0L,
|
||||
episodeFlags = 0L,
|
||||
coverLastModified = 0L,
|
||||
artist = null,
|
||||
author = null,
|
||||
description = null,
|
||||
genre = null,
|
||||
status = 0L,
|
||||
thumbnailUrl = null,
|
||||
updateStrategy = UpdateStrategy.ALWAYS_UPDATE,
|
||||
initialized = false,
|
||||
)
|
||||
}
|
||||
|
@ -181,20 +205,10 @@ fun Anime.toDbAnime(): DbAnime = AnimeImpl().also {
|
|||
it.genre = genre?.let(listOfStringsAdapter::encode)
|
||||
it.status = status.toInt()
|
||||
it.thumbnail_url = thumbnailUrl
|
||||
it.update_strategy = updateStrategy
|
||||
it.initialized = initialized
|
||||
}
|
||||
|
||||
fun Anime.toAnimeInfo(): AnimeInfo = AnimeInfo(
|
||||
artist = artist ?: "",
|
||||
author = author ?: "",
|
||||
cover = thumbnailUrl ?: "",
|
||||
description = description ?: "",
|
||||
genres = genre ?: emptyList(),
|
||||
key = url,
|
||||
status = status.toInt(),
|
||||
title = title,
|
||||
)
|
||||
|
||||
fun Anime.toAnimeUpdate(): AnimeUpdate {
|
||||
return AnimeUpdate(
|
||||
id = id,
|
||||
|
@ -213,6 +227,22 @@ fun Anime.toAnimeUpdate(): AnimeUpdate {
|
|||
genre = genre,
|
||||
status = status,
|
||||
thumbnailUrl = thumbnailUrl,
|
||||
updateStrategy = updateStrategy,
|
||||
initialized = initialized,
|
||||
)
|
||||
}
|
||||
|
||||
fun SAnime.toDomainAnime(): Anime {
|
||||
return Anime.create().copy(
|
||||
url = url,
|
||||
title = title,
|
||||
artist = artist,
|
||||
author = author,
|
||||
description = description,
|
||||
genre = getGenres(),
|
||||
status = status.toLong(),
|
||||
thumbnailUrl = thumbnail_url,
|
||||
updateStrategy = update_strategy,
|
||||
initialized = initialized,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package eu.kanade.domain.anime.model
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
|
||||
data class AnimeUpdate(
|
||||
val id: Long,
|
||||
val source: Long? = null,
|
||||
|
@ -17,5 +19,6 @@ data class AnimeUpdate(
|
|||
val genre: List<String>? = null,
|
||||
val status: Long? = null,
|
||||
val thumbnailUrl: String? = null,
|
||||
val updateStrategy: UpdateStrategy? = null,
|
||||
val initialized: Boolean? = null,
|
||||
)
|
||||
|
|
|
@ -2,7 +2,7 @@ package eu.kanade.domain.anime.repository
|
|||
|
||||
import eu.kanade.domain.anime.model.Anime
|
||||
import eu.kanade.domain.anime.model.AnimeUpdate
|
||||
import eu.kanade.tachiyomi.data.database.models.AnimelibAnime
|
||||
import eu.kanade.domain.animelib.model.AnimelibAnime
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AnimeRepository {
|
||||
|
@ -13,6 +13,8 @@ interface AnimeRepository {
|
|||
|
||||
suspend fun getAnimeByUrlAndSourceId(url: String, sourceId: Long): Anime?
|
||||
|
||||
fun getAnimeByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow<Anime?>
|
||||
|
||||
suspend fun getFavorites(): List<Anime>
|
||||
|
||||
suspend fun getAnimelibAnime(): List<AnimelibAnime>
|
||||
|
@ -31,5 +33,5 @@ interface AnimeRepository {
|
|||
|
||||
suspend fun update(update: AnimeUpdate): Boolean
|
||||
|
||||
suspend fun updateAll(values: List<AnimeUpdate>): Boolean
|
||||
suspend fun updateAll(animeUpdates: List<AnimeUpdate>): Boolean
|
||||
}
|
||||
|
|
|
@ -2,19 +2,19 @@ package eu.kanade.domain.animedownload.interactor
|
|||
|
||||
import eu.kanade.domain.anime.model.Anime
|
||||
import eu.kanade.domain.episode.model.Episode
|
||||
import eu.kanade.domain.episode.model.toDbEpisode
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
|
||||
import eu.kanade.tachiyomi.data.download.AnimeDownloadManager
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.withContext
|
||||
import eu.kanade.tachiyomi.data.animedownload.AnimeDownloadManager
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
|
||||
class DeleteAnimeDownload(
|
||||
private val sourceManager: AnimeSourceManager,
|
||||
private val downloadManager: AnimeDownloadManager,
|
||||
) {
|
||||
|
||||
suspend fun awaitAll(anime: Anime, vararg values: Episode) = withContext(NonCancellable) {
|
||||
suspend fun awaitAll(anime: Anime, vararg episodes: Episode) = withNonCancellableContext {
|
||||
sourceManager.get(anime.source)?.let { source ->
|
||||
downloadManager.deleteEpisodes(values.toList(), anime, source)
|
||||
downloadManager.deleteEpisodes(episodes.map { it.toDbEpisode() }, anime, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
package eu.kanade.domain.animeextension.interactor
|
||||
|
||||
import eu.kanade.core.util.asFlow
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.AnimeExtensionManager
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.animeextension.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
||||
class GetAnimeExtensionLanguages(
|
||||
private val preferences: PreferencesHelper,
|
||||
private val preferences: SourcePreferences,
|
||||
private val extensionManager: AnimeExtensionManager,
|
||||
) {
|
||||
fun subscribe(): Flow<List<String>> {
|
||||
return combine(
|
||||
preferences.enabledLanguages().asFlow(),
|
||||
extensionManager.getAvailableExtensionsObservable().asFlow(),
|
||||
preferences.enabledLanguages().changes(),
|
||||
extensionManager.availableExtensionsFlow,
|
||||
) { enabledLanguage, availableExtensions ->
|
||||
availableExtensions
|
||||
.map { it.lang }
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
package eu.kanade.domain.animeextension.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.animeextension.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.ui.browse.animeextension.details.AnimeExtensionSourceItem
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class GetAnimeExtensionSources(
|
||||
private val preferences: PreferencesHelper,
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
fun subscribe(extension: AnimeExtension.Installed): Flow<List<AnimeExtensionSourceItem>> {
|
||||
|
@ -16,7 +16,7 @@ class GetAnimeExtensionSources(
|
|||
val isMultiLangSingleSource =
|
||||
isMultiSource && extension.sources.map { it.name }.distinct().size == 1
|
||||
|
||||
return preferences.disabledSources().asFlow().map { disabledSources ->
|
||||
return preferences.disabledAnimeSources().changes().map { disabledSources ->
|
||||
fun AnimeSource.isEnabled() = id.toString() !in disabledSources
|
||||
|
||||
extension.sources
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
package eu.kanade.domain.animeextension.interactor
|
||||
|
||||
import eu.kanade.core.util.asFlow
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.model.AnimeExtension
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class GetAnimeExtensionUpdates(
|
||||
private val preferences: PreferencesHelper,
|
||||
private val extensionManager: AnimeExtensionManager,
|
||||
) {
|
||||
|
||||
fun subscribe(): Flow<List<AnimeExtension.Installed>> {
|
||||
val showNsfwSources = preferences.showNsfwSource().get()
|
||||
|
||||
return extensionManager.getInstalledExtensionsObservable().asFlow()
|
||||
.map { installed ->
|
||||
installed
|
||||
.filter { it.hasUpdate && (showNsfwSources || it.isNsfw.not()) }
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,35 +1,33 @@
|
|||
package eu.kanade.domain.animeextension.interactor
|
||||
|
||||
import eu.kanade.core.util.asFlow
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.model.AnimeExtension
|
||||
import eu.kanade.domain.animeextension.model.AnimeExtensions
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.animeextension.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.animeextension.model.AnimeExtension
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
||||
typealias ExtensionSegregation = Triple<List<AnimeExtension.Installed>, List<AnimeExtension.Untrusted>, List<AnimeExtension.Available>>
|
||||
|
||||
class GetAnimeExtensions(
|
||||
private val preferences: PreferencesHelper,
|
||||
class GetAnimeExtensionsByType(
|
||||
private val preferences: SourcePreferences,
|
||||
private val extensionManager: AnimeExtensionManager,
|
||||
) {
|
||||
|
||||
fun subscribe(): Flow<ExtensionSegregation> {
|
||||
fun subscribe(): Flow<AnimeExtensions> {
|
||||
val showNsfwSources = preferences.showNsfwSource().get()
|
||||
|
||||
return combine(
|
||||
preferences.enabledLanguages().asFlow(),
|
||||
extensionManager.getInstalledExtensionsObservable().asFlow(),
|
||||
extensionManager.getUntrustedExtensionsObservable().asFlow(),
|
||||
extensionManager.getAvailableExtensionsObservable().asFlow(),
|
||||
preferences.enabledLanguages().changes(),
|
||||
extensionManager.installedExtensionsFlow,
|
||||
extensionManager.untrustedExtensionsFlow,
|
||||
extensionManager.availableExtensionsFlow,
|
||||
) { _activeLanguages, _installed, _untrusted, _available ->
|
||||
|
||||
val installed = _installed
|
||||
.filter { it.hasUpdate.not() && (showNsfwSources || it.isNsfw.not()) }
|
||||
val (updates, installed) = _installed
|
||||
.filter { (showNsfwSources || it.isNsfw.not()) }
|
||||
.sortedWith(
|
||||
compareBy<AnimeExtension.Installed> { it.isObsolete.not() }
|
||||
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
|
||||
)
|
||||
.partition { it.hasUpdate }
|
||||
|
||||
val untrusted = _untrusted
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
|
@ -38,11 +36,11 @@ class GetAnimeExtensions(
|
|||
.filter { extension ->
|
||||
_installed.none { it.pkgName == extension.pkgName } &&
|
||||
_untrusted.none { it.pkgName == extension.pkgName } &&
|
||||
extension.lang in _activeLanguages &&
|
||||
(showNsfwSources || extension.isNsfw.not())
|
||||
}
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
|
||||
Triple(installed, untrusted, available)
|
||||
AnimeExtensions(updates, installed, available, untrusted)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package eu.kanade.domain.animeextension.model
|
||||
|
||||
import eu.kanade.tachiyomi.animeextension.model.AnimeExtension
|
||||
|
||||
data class AnimeExtensions(
|
||||
val updates: List<AnimeExtension.Installed>,
|
||||
val installed: List<AnimeExtension.Installed>,
|
||||
val available: List<AnimeExtension.Available>,
|
||||
val untrusted: List<AnimeExtension.Untrusted>,
|
||||
)
|
|
@ -2,7 +2,7 @@ package eu.kanade.domain.animehistory.interactor
|
|||
|
||||
import eu.kanade.domain.animehistory.repository.AnimeHistoryRepository
|
||||
|
||||
class DeleteAnimeHistoryTable(
|
||||
class DeleteAllAnimeHistory(
|
||||
private val repository: AnimeHistoryRepository,
|
||||
) {
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
package eu.kanade.domain.animehistory.interactor
|
||||
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import eu.kanade.domain.animehistory.model.AnimeHistoryWithRelations
|
||||
import eu.kanade.domain.animehistory.repository.AnimeHistoryRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
@ -11,11 +8,7 @@ class GetAnimeHistory(
|
|||
private val repository: AnimeHistoryRepository,
|
||||
) {
|
||||
|
||||
fun subscribe(query: String): Flow<PagingData<AnimeHistoryWithRelations>> {
|
||||
return Pager(
|
||||
PagingConfig(pageSize = 25),
|
||||
) {
|
||||
repository.getHistory(query)
|
||||
}.flow
|
||||
fun subscribe(query: String): Flow<List<AnimeHistoryWithRelations>> {
|
||||
return repository.getHistory(query)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,51 @@
|
|||
package eu.kanade.domain.animehistory.interactor
|
||||
|
||||
import eu.kanade.domain.anime.interactor.GetAnime
|
||||
import eu.kanade.domain.anime.model.Anime
|
||||
import eu.kanade.domain.animehistory.repository.AnimeHistoryRepository
|
||||
import eu.kanade.domain.episode.interactor.GetEpisode
|
||||
import eu.kanade.domain.episode.interactor.GetEpisodeByAnimeId
|
||||
import eu.kanade.domain.episode.model.Episode
|
||||
import eu.kanade.tachiyomi.util.episode.getEpisodeSort
|
||||
|
||||
class GetNextEpisode(
|
||||
private val repository: AnimeHistoryRepository,
|
||||
private val getEpisode: GetEpisode,
|
||||
private val getEpisodeByAnimeId: GetEpisodeByAnimeId,
|
||||
private val getAnime: GetAnime,
|
||||
private val historyRepository: AnimeHistoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(animeId: Long, episodeId: Long): Episode? {
|
||||
return repository.getNextEpisode(animeId, episodeId)
|
||||
suspend fun await(): Episode? {
|
||||
val history = historyRepository.getLastHistory() ?: return null
|
||||
return await(history.animeId, history.episodeId)
|
||||
}
|
||||
|
||||
suspend fun await(): Episode? {
|
||||
val history = repository.getLastHistory() ?: return null
|
||||
return repository.getNextEpisode(history.animeId, history.episodeId)
|
||||
suspend fun await(animeId: Long, episodeId: Long): Episode? {
|
||||
val episode = getEpisode.await(episodeId) ?: return null
|
||||
val anime = getAnime.await(animeId) ?: return null
|
||||
|
||||
if (!episode.seen) return episode
|
||||
|
||||
val episodes = getEpisodeByAnimeId.await(animeId)
|
||||
.sortedWith(getEpisodeSort(anime, sortDescending = false))
|
||||
|
||||
val currEpisodeIndex = episodes.indexOfFirst { episode.id == it.id }
|
||||
return when (anime.sorting) {
|
||||
Anime.EPISODE_SORTING_SOURCE -> episodes.getOrNull(currEpisodeIndex + 1)
|
||||
Anime.EPISODE_SORTING_NUMBER -> {
|
||||
val episodeNumber = episode.episodeNumber
|
||||
|
||||
((currEpisodeIndex + 1) until episodes.size)
|
||||
.map { episodes[it] }
|
||||
.firstOrNull {
|
||||
it.episodeNumber > episodeNumber && it.episodeNumber <= episodeNumber + 1
|
||||
}
|
||||
}
|
||||
Anime.EPISODE_SORTING_UPLOAD_DATE -> {
|
||||
episodes.drop(currEpisodeIndex + 1)
|
||||
.firstOrNull { it.dateUpload >= episode.dateUpload }
|
||||
}
|
||||
else -> throw NotImplementedError("Invalid episode sorting method: ${anime.sorting}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
package eu.kanade.domain.animehistory.repository
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import eu.kanade.domain.animehistory.model.AnimeHistoryUpdate
|
||||
import eu.kanade.domain.animehistory.model.AnimeHistoryWithRelations
|
||||
import eu.kanade.domain.episode.model.Episode
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AnimeHistoryRepository {
|
||||
|
||||
fun getHistory(query: String): PagingSource<Long, AnimeHistoryWithRelations>
|
||||
fun getHistory(query: String): Flow<List<AnimeHistoryWithRelations>>
|
||||
|
||||
suspend fun getLastHistory(): AnimeHistoryWithRelations?
|
||||
|
||||
suspend fun getNextEpisode(animeId: Long, episodeId: Long): Episode?
|
||||
|
||||
suspend fun resetHistory(historyId: Long)
|
||||
|
||||
suspend fun resetHistoryByAnimeId(animeId: Long)
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package eu.kanade.domain.animelib.model
|
||||
|
||||
import eu.kanade.domain.anime.model.Anime
|
||||
|
||||
data class AnimelibAnime(
|
||||
val anime: Anime,
|
||||
val category: Long,
|
||||
val totalEpisodes: Long,
|
||||
val seenCount: Long,
|
||||
val bookmarkCount: Long,
|
||||
val latestUpload: Long,
|
||||
val episodeFetchedAt: Long,
|
||||
val lastSeen: Long,
|
||||
) {
|
||||
val id: Long = anime.id
|
||||
|
||||
val unseenCount
|
||||
get() = totalEpisodes - seenCount
|
||||
|
||||
val hasBookmarks
|
||||
get() = bookmarkCount > 0
|
||||
|
||||
val hasStarted = seenCount > 0
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package eu.kanade.domain.animesource.interactor
|
||||
|
||||
import eu.kanade.domain.animesource.model.AnimeSourceData
|
||||
import eu.kanade.domain.animesource.repository.AnimeSourceRepository
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
|
||||
class GetAnimeSourceData(
|
||||
private val repository: AnimeSourceRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(id: Long): AnimeSourceData? {
|
||||
return try {
|
||||
repository.getAnimeSourceData(id)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ package eu.kanade.domain.animesource.interactor
|
|||
import eu.kanade.domain.animesource.model.AnimeSource
|
||||
import eu.kanade.domain.animesource.repository.AnimeSourceRepository
|
||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import java.text.Collator
|
||||
|
@ -12,13 +12,13 @@ import java.util.Locale
|
|||
|
||||
class GetAnimeSourcesWithFavoriteCount(
|
||||
private val repository: AnimeSourceRepository,
|
||||
private val preferences: PreferencesHelper,
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
fun subscribe(): Flow<List<Pair<AnimeSource, Long>>> {
|
||||
return combine(
|
||||
preferences.migrationSortingDirection().asFlow(),
|
||||
preferences.migrationSortingMode().asFlow(),
|
||||
preferences.migrationSortingDirection().changes(),
|
||||
preferences.migrationSortingMode().changes(),
|
||||
repository.getSourcesWithFavoriteCount(),
|
||||
) { direction, mode, list ->
|
||||
list.sortedWith(sortFn(direction, mode))
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
package eu.kanade.domain.animesource.interactor
|
||||
|
||||
import eu.kanade.domain.animesource.model.AnimeSourceWithCount
|
||||
import eu.kanade.domain.animesource.repository.AnimeSourceRepository
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class GetAnimeSourcesWithNonLibraryAnime(
|
||||
private val repository: AnimeSourceRepository,
|
||||
) {
|
||||
|
||||
fun subscribe(): Flow<List<Pair<AnimeSource, Long>>> {
|
||||
fun subscribe(): Flow<List<AnimeSourceWithCount>> {
|
||||
return repository.getSourcesWithNonLibraryAnime()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,23 +4,23 @@ import eu.kanade.domain.animesource.model.AnimeSource
|
|||
import eu.kanade.domain.animesource.model.Pin
|
||||
import eu.kanade.domain.animesource.model.Pins
|
||||
import eu.kanade.domain.animesource.repository.AnimeSourceRepository
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.animesource.LocalAnimeSource
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
class GetEnabledAnimeSources(
|
||||
private val repository: AnimeSourceRepository,
|
||||
private val preferences: PreferencesHelper,
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
fun subscribe(): Flow<List<AnimeSource>> {
|
||||
return combine(
|
||||
preferences.pinnedAnimeSources().asFlow(),
|
||||
preferences.enabledLanguages().asFlow(),
|
||||
preferences.disabledAnimeSources().asFlow(),
|
||||
preferences.lastUsedAnimeSource().asFlow(),
|
||||
preferences.pinnedAnimeSources().changes(),
|
||||
preferences.enabledLanguages().changes(),
|
||||
preferences.disabledAnimeSources().changes(),
|
||||
preferences.lastUsedAnimeSource().changes(),
|
||||
repository.getSources(),
|
||||
) { pinnedSourceIds, enabledLanguages, disabledSources, lastUsedSource, sources ->
|
||||
val duplicatePins = preferences.duplicatePinnedSources().get()
|
||||
|
|
|
@ -2,20 +2,20 @@ package eu.kanade.domain.animesource.interactor
|
|||
|
||||
import eu.kanade.domain.animesource.model.AnimeSource
|
||||
import eu.kanade.domain.animesource.repository.AnimeSourceRepository
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
||||
class GetLanguagesWithAnimeSources(
|
||||
private val repository: AnimeSourceRepository,
|
||||
private val preferences: PreferencesHelper,
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
fun subscribe(): Flow<Map<String, List<AnimeSource>>> {
|
||||
return combine(
|
||||
preferences.enabledLanguages().asFlow(),
|
||||
preferences.disabledSources().asFlow(),
|
||||
preferences.enabledLanguages().changes(),
|
||||
preferences.disabledSources().changes(),
|
||||
repository.getOnlineSources(),
|
||||
) { enabledLanguage, disabledSource, onlineSources ->
|
||||
val sortedSources = onlineSources.sortedWith(
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package eu.kanade.domain.animesource.interactor
|
||||
|
||||
import eu.kanade.domain.animesource.model.AnimeSourcePagingSourceType
|
||||
import eu.kanade.domain.animesource.repository.AnimeSourceRepository
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
|
||||
class GetRemoteAnime(
|
||||
private val repository: AnimeSourceRepository,
|
||||
) {
|
||||
|
||||
fun subscribe(sourceId: Long, query: String, filterList: AnimeFilterList): AnimeSourcePagingSourceType {
|
||||
return when (query) {
|
||||
QUERY_POPULAR -> repository.getPopular(sourceId)
|
||||
QUERY_LATEST -> repository.getLatest(sourceId)
|
||||
else -> repository.search(sourceId, query, filterList)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val QUERY_POPULAR = "eu.kanade.domain.animesource.interactor.POPULAR"
|
||||
const val QUERY_LATEST = "eu.kanade.domain.animesource.interactor.LATEST"
|
||||
}
|
||||
}
|
|
@ -1,23 +1,24 @@
|
|||
package eu.kanade.domain.animesource.interactor
|
||||
|
||||
import eu.kanade.domain.animesource.model.AnimeSource
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.util.preference.minusAssign
|
||||
import eu.kanade.tachiyomi.util.preference.plusAssign
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.core.preference.getAndSet
|
||||
|
||||
class ToggleAnimeSource(
|
||||
private val preferences: PreferencesHelper,
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
fun await(source: AnimeSource, enable: Boolean = source.id.toString() in preferences.disabledAnimeSources().get()) {
|
||||
fun await(source: AnimeSource, enable: Boolean = isEnabled(source.id)) {
|
||||
await(source.id, enable)
|
||||
}
|
||||
|
||||
fun await(sourceId: Long, enable: Boolean = sourceId.toString() in preferences.disabledAnimeSources().get()) {
|
||||
if (enable) {
|
||||
preferences.disabledAnimeSources() -= sourceId.toString()
|
||||
} else {
|
||||
preferences.disabledAnimeSources() += sourceId.toString()
|
||||
fun await(sourceId: Long, enable: Boolean = isEnabled(sourceId)) {
|
||||
preferences.disabledAnimeSources().getAndSet { disabled ->
|
||||
if (enable) disabled.minus("$sourceId") else disabled.plus("$sourceId")
|
||||
}
|
||||
}
|
||||
|
||||
private fun isEnabled(sourceId: Long): Boolean {
|
||||
return sourceId.toString() in preferences.disabledAnimeSources().get()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
package eu.kanade.domain.animesource.interactor
|
||||
|
||||
import eu.kanade.domain.animesource.model.AnimeSource
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.util.preference.minusAssign
|
||||
import eu.kanade.tachiyomi.util.preference.plusAssign
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.core.preference.getAndSet
|
||||
|
||||
class ToggleAnimeSourcePin(
|
||||
private val preferences: PreferencesHelper,
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
fun await(source: AnimeSource) {
|
||||
val isPinned = source.id.toString() in preferences.pinnedAnimeSources().get()
|
||||
if (isPinned) {
|
||||
preferences.pinnedAnimeSources() -= source.id.toString()
|
||||
} else {
|
||||
preferences.pinnedAnimeSources() += source.id.toString()
|
||||
preferences.pinnedAnimeSources().getAndSet { pinned ->
|
||||
if (isPinned) pinned.minus("${source.id}") else pinned.plus("${source.id}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
package eu.kanade.domain.animesource.interactor
|
||||
|
||||
import eu.kanade.domain.animesource.model.AnimeSourceData
|
||||
import eu.kanade.domain.animesource.repository.AnimeSourceRepository
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
|
||||
class UpsertAnimeSourceData(
|
||||
private val repository: AnimeSourceRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(sourceData: AnimeSourceData) {
|
||||
try {
|
||||
repository.upsertAnimeSourceData(sourceData.id, sourceData.lang, sourceData.name)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,4 +4,7 @@ data class AnimeSourceData(
|
|||
val id: Long,
|
||||
val lang: String,
|
||||
val name: String,
|
||||
)
|
||||
) {
|
||||
|
||||
val isMissingInfo: Boolean = name.isBlank() || lang.isBlank()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package eu.kanade.domain.animesource.model
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
|
||||
typealias AnimeSourcePagingSourceType = PagingSource<Long, SAnime>
|
|
@ -0,0 +1,13 @@
|
|||
package eu.kanade.domain.animesource.model
|
||||
|
||||
data class AnimeSourceWithCount(
|
||||
val source: AnimeSource,
|
||||
val count: Long,
|
||||
) {
|
||||
|
||||
val id: Long
|
||||
get() = source.id
|
||||
|
||||
val name: String
|
||||
get() = source.name
|
||||
}
|
|
@ -3,7 +3,7 @@ package eu.kanade.domain.animesource.model
|
|||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import eu.kanade.tachiyomi.extension.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.animeextension.AnimeExtensionManager
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
|
@ -17,8 +17,11 @@ data class AnimeSource(
|
|||
val isUsedLast: Boolean = false,
|
||||
) {
|
||||
|
||||
val nameWithLanguage: String
|
||||
get() = "$name (${lang.uppercase()})"
|
||||
val visualName: String
|
||||
get() = when {
|
||||
lang.isEmpty() -> name
|
||||
else -> "$name (${lang.uppercase()})"
|
||||
}
|
||||
|
||||
val icon: ImageBitmap?
|
||||
get() {
|
|
@ -0,0 +1,12 @@
|
|||
package eu.kanade.domain.animesource.repository
|
||||
|
||||
import eu.kanade.domain.animesource.model.AnimeSourceData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AnimeSourceDataRepository {
|
||||
fun subscribeAll(): Flow<List<AnimeSourceData>>
|
||||
|
||||
suspend fun getSourceData(id: Long): AnimeSourceData?
|
||||
|
||||
suspend fun upsertSourceData(id: Long, lang: String, name: String)
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
package eu.kanade.domain.animesource.repository
|
||||
|
||||
import eu.kanade.domain.animesource.model.AnimeSource
|
||||
import eu.kanade.domain.animesource.model.AnimeSourceData
|
||||
import eu.kanade.domain.animesource.model.AnimeSourcePagingSourceType
|
||||
import eu.kanade.domain.animesource.model.AnimeSourceWithCount
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSource as LoadedAnimeSource
|
||||
|
||||
interface AnimeSourceRepository {
|
||||
|
||||
|
@ -13,9 +14,11 @@ interface AnimeSourceRepository {
|
|||
|
||||
fun getSourcesWithFavoriteCount(): Flow<List<Pair<AnimeSource, Long>>>
|
||||
|
||||
fun getSourcesWithNonLibraryAnime(): Flow<List<Pair<LoadedAnimeSource, Long>>>
|
||||
fun getSourcesWithNonLibraryAnime(): Flow<List<AnimeSourceWithCount>>
|
||||
|
||||
suspend fun getAnimeSourceData(id: Long): AnimeSourceData?
|
||||
fun search(sourceId: Long, query: String, filterList: AnimeFilterList): AnimeSourcePagingSourceType
|
||||
|
||||
suspend fun upsertAnimeSourceData(id: Long, lang: String, name: String)
|
||||
fun getPopular(sourceId: Long): AnimeSourcePagingSourceType
|
||||
|
||||
fun getLatest(sourceId: Long): AnimeSourcePagingSourceType
|
||||
}
|
||||
|
|
|
@ -19,10 +19,6 @@ class GetAnimeTracks(
|
|||
}
|
||||
}
|
||||
|
||||
fun subscribe(): Flow<List<AnimeTrack>> {
|
||||
return animetrackRepository.getAnimeTracksAsFlow()
|
||||
}
|
||||
|
||||
fun subscribe(animeId: Long): Flow<List<AnimeTrack>> {
|
||||
return animetrackRepository.getAnimeTracksByAnimeIdAsFlow(animeId)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package eu.kanade.domain.animetrack.interactor
|
||||
|
||||
import eu.kanade.domain.animetrack.repository.AnimeTrackRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class GetTracksPerAnime(
|
||||
private val trackRepository: AnimeTrackRepository,
|
||||
) {
|
||||
|
||||
fun subscribe(): Flow<Map<Long, List<Long>>> {
|
||||
return trackRepository.getAnimeTracksAsFlow().map { tracks ->
|
||||
tracks
|
||||
.groupBy { it.animeId }
|
||||
.mapValues { entry ->
|
||||
entry.value.map { it.syncId }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package eu.kanade.domain.animeupdates.interactor
|
||||
|
||||
import eu.kanade.domain.animeupdates.model.AnimeUpdatesWithRelations
|
||||
import eu.kanade.domain.animeupdates.repository.AnimeUpdatesRepository
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import java.util.Calendar
|
||||
|
||||
class GetAnimeUpdates(
|
||||
private val repository: AnimeUpdatesRepository,
|
||||
private val preferences: LibraryPreferences,
|
||||
) {
|
||||
|
||||
fun subscribe(calendar: Calendar): Flow<List<AnimeUpdatesWithRelations>> = subscribe(calendar.time.time)
|
||||
|
||||
fun subscribe(after: Long): Flow<List<AnimeUpdatesWithRelations>> {
|
||||
return repository.subscribeAll(after)
|
||||
.onEach { updates ->
|
||||
// Set unread chapter count for bottom bar badge
|
||||
preferences.unseenUpdatesCount().set(updates.count { !it.seen })
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package eu.kanade.domain.animeupdates.model
|
||||
|
||||
import eu.kanade.domain.manga.model.MangaCover
|
||||
|
||||
data class AnimeUpdatesWithRelations(
|
||||
val animeId: Long,
|
||||
val animeTitle: String,
|
||||
val episodeId: Long,
|
||||
val episodeName: String,
|
||||
val scanlator: String?,
|
||||
val seen: Boolean,
|
||||
val bookmark: Boolean,
|
||||
val sourceId: Long,
|
||||
val dateFetch: Long,
|
||||
val coverData: MangaCover,
|
||||
)
|
|
@ -0,0 +1,9 @@
|
|||
package eu.kanade.domain.animeupdates.repository
|
||||
|
||||
import eu.kanade.domain.animeupdates.model.AnimeUpdatesWithRelations
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AnimeUpdatesRepository {
|
||||
|
||||
fun subscribeAll(after: Long): Flow<List<AnimeUpdatesWithRelations>>
|
||||
}
|
|
@ -2,6 +2,10 @@ package eu.kanade.domain.backup.service
|
|||
|
||||
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||
import eu.kanade.tachiyomi.core.provider.FolderProvider
|
||||
import eu.kanade.tachiyomi.data.preference.FLAG_CATEGORIES
|
||||
import eu.kanade.tachiyomi.data.preference.FLAG_CHAPTERS
|
||||
import eu.kanade.tachiyomi.data.preference.FLAG_HISTORY
|
||||
import eu.kanade.tachiyomi.data.preference.FLAG_TRACK
|
||||
|
||||
class BackupPreferences(
|
||||
private val folderProvider: FolderProvider,
|
||||
|
@ -13,4 +17,6 @@ class BackupPreferences(
|
|||
fun numberOfBackups() = preferenceStore.getInt("backup_slots", 2)
|
||||
|
||||
fun backupInterval() = preferenceStore.getInt("backup_interval", 12)
|
||||
|
||||
fun backupFlags() = preferenceStore.getStringSet("backup_flags", setOf(FLAG_CATEGORIES, FLAG_CHAPTERS, FLAG_HISTORY, FLAG_TRACK))
|
||||
}
|
||||
|
|
|
@ -5,8 +5,6 @@ import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
|||
import eu.kanade.tachiyomi.core.preference.getEnum
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
|
||||
class BasePreferences(
|
||||
val context: Context,
|
||||
|
@ -26,5 +24,6 @@ class BasePreferences(
|
|||
if (DeviceUtil.isMiui) PreferenceValues.ExtensionInstaller.LEGACY else PreferenceValues.ExtensionInstaller.PACKAGEINSTALLER,
|
||||
)
|
||||
|
||||
fun acraEnabled() = preferenceStore.getBoolean("acra.enable", isPreviewBuildType || isReleaseBuildType)
|
||||
// acra is disabled
|
||||
fun acraEnabled() = preferenceStore.getBoolean("acra.enable", false)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.category.model.anyWithName
|
||||
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
|
||||
class CreateAnimeCategoryWithName(
|
||||
private val categoryRepository: CategoryRepositoryAnime,
|
||||
private val preferences: LibraryPreferences,
|
||||
) {
|
||||
|
||||
private val initialFlags: Long
|
||||
get() {
|
||||
val sort = preferences.librarySortingMode().get()
|
||||
return preferences.libraryDisplayMode().get().flag or
|
||||
sort.type.flag or
|
||||
sort.direction.flag
|
||||
}
|
||||
|
||||
suspend fun await(name: String): Result = withNonCancellableContext {
|
||||
val categories = categoryRepository.getAll()
|
||||
if (categories.anyWithName(name)) {
|
||||
return@withNonCancellableContext Result.NameAlreadyExistsError
|
||||
}
|
||||
|
||||
val nextOrder = categories.maxOfOrNull { it.order }?.plus(1) ?: 0
|
||||
val newCategory = Category(
|
||||
id = 0,
|
||||
name = name,
|
||||
order = nextOrder,
|
||||
flags = initialFlags,
|
||||
)
|
||||
|
||||
try {
|
||||
categoryRepository.insert(newCategory)
|
||||
Result.Success
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
Result.InternalError(e)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
object NameAlreadyExistsError : Result()
|
||||
data class InternalError(val error: Throwable) : Result()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
|
||||
class DeleteAnimeCategory(
|
||||
private val categoryRepository: CategoryRepositoryAnime,
|
||||
) {
|
||||
|
||||
suspend fun await(categoryId: Long) = withNonCancellableContext {
|
||||
try {
|
||||
categoryRepository.delete(categoryId)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
return@withNonCancellableContext Result.InternalError(e)
|
||||
}
|
||||
|
||||
val categories = categoryRepository.getAll()
|
||||
val updates = categories.mapIndexed { index, category ->
|
||||
CategoryUpdate(
|
||||
id = category.id,
|
||||
order = index.toLong(),
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
categoryRepository.updatePartial(updates)
|
||||
Result.Success
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
Result.InternalError(e)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
data class InternalError(val error: Throwable) : Result()
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
|
||||
|
||||
class DeleteCategoryAnime(
|
||||
private val categoryRepository: CategoryRepositoryAnime,
|
||||
) {
|
||||
|
||||
suspend fun await(categoryId: Long) {
|
||||
categoryRepository.delete(categoryId)
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import eu.kanade.domain.category.model.Category
|
|||
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class GetCategoriesAnime(
|
||||
class GetAnimeCategories(
|
||||
private val categoryRepository: CategoryRepositoryAnime,
|
||||
) {
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
|
||||
|
||||
class InsertCategoryAnime(
|
||||
private val categoryRepository: CategoryRepositoryAnime,
|
||||
) {
|
||||
|
||||
suspend fun await(name: String, order: Long): Result {
|
||||
return try {
|
||||
categoryRepository.insert(name, order)
|
||||
Result.Success
|
||||
} catch (e: Exception) {
|
||||
Result.Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
data class Error(val error: Exception) : Result()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.domain.category.model.anyWithName
|
||||
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
|
||||
class RenameAnimeCategory(
|
||||
private val categoryRepository: CategoryRepositoryAnime,
|
||||
) {
|
||||
|
||||
suspend fun await(categoryId: Long, name: String) = withNonCancellableContext {
|
||||
val categories = categoryRepository.getAll()
|
||||
if (categories.anyWithName(name)) {
|
||||
return@withNonCancellableContext Result.NameAlreadyExistsError
|
||||
}
|
||||
|
||||
val update = CategoryUpdate(
|
||||
id = categoryId,
|
||||
name = name,
|
||||
)
|
||||
|
||||
try {
|
||||
categoryRepository.updatePartial(update)
|
||||
Result.Success
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
Result.InternalError(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun await(category: Category, name: String) = await(category.id, name)
|
||||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
object NameAlreadyExistsError : Result()
|
||||
data class InternalError(val error: Throwable) : Result()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
|
||||
class ReorderAnimeCategory(
|
||||
private val categoryRepository: CategoryRepositoryAnime,
|
||||
) {
|
||||
|
||||
suspend fun await(categoryId: Long, newPosition: Int) = withNonCancellableContext {
|
||||
val categories = categoryRepository.getAll().filterNot(Category::isSystemCategory)
|
||||
|
||||
val currentIndex = categories.indexOfFirst { it.id == categoryId }
|
||||
if (currentIndex == newPosition) {
|
||||
return@withNonCancellableContext Result.Unchanged
|
||||
}
|
||||
|
||||
val reorderedCategories = categories.toMutableList()
|
||||
val reorderedCategory = reorderedCategories.removeAt(currentIndex)
|
||||
reorderedCategories.add(newPosition, reorderedCategory)
|
||||
|
||||
val updates = reorderedCategories.mapIndexed { index, category ->
|
||||
CategoryUpdate(
|
||||
id = category.id,
|
||||
order = index.toLong(),
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
categoryRepository.updatePartial(updates)
|
||||
Result.Success
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
Result.InternalError(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun await(category: Category, newPosition: Long): Result =
|
||||
await(category.id, newPosition.toInt())
|
||||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
object Unchanged : Result()
|
||||
data class InternalError(val error: Throwable) : Result()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
|
||||
import eu.kanade.domain.library.model.plus
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
|
||||
class ResetAnimeCategoryFlags(
|
||||
private val preferences: LibraryPreferences,
|
||||
private val categoryRepository: CategoryRepositoryAnime,
|
||||
) {
|
||||
|
||||
suspend fun await() {
|
||||
val display = preferences.libraryDisplayMode().get()
|
||||
val sort = preferences.librarySortingMode().get()
|
||||
categoryRepository.updateAllFlags(display + sort.type + sort.direction)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
|
||||
import eu.kanade.domain.library.model.LibraryDisplayMode
|
||||
import eu.kanade.domain.library.model.plus
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
|
||||
class SetDisplayModeForAnimeCategory(
|
||||
private val preferences: LibraryPreferences,
|
||||
private val categoryRepository: CategoryRepositoryAnime,
|
||||
) {
|
||||
|
||||
suspend fun await(categoryId: Long, display: LibraryDisplayMode) {
|
||||
val category = categoryRepository.get(categoryId) ?: return
|
||||
val flags = category.flags + display
|
||||
if (preferences.categorizedDisplaySettings().get()) {
|
||||
categoryRepository.updatePartial(
|
||||
CategoryUpdate(
|
||||
id = category.id,
|
||||
flags = flags,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
preferences.libraryDisplayMode().set(display)
|
||||
categoryRepository.updateAllFlags(flags)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun await(category: Category, display: LibraryDisplayMode) {
|
||||
await(category.id, display)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
|
||||
import eu.kanade.domain.library.model.LibrarySort
|
||||
import eu.kanade.domain.library.model.plus
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
|
||||
class SetSortModeForAnimeCategory(
|
||||
private val preferences: LibraryPreferences,
|
||||
private val categoryRepository: CategoryRepositoryAnime,
|
||||
) {
|
||||
|
||||
suspend fun await(categoryId: Long, type: LibrarySort.Type, direction: LibrarySort.Direction) {
|
||||
val category = categoryRepository.get(categoryId) ?: return
|
||||
val flags = category.flags + type + direction
|
||||
if (preferences.categorizedDisplaySettings().get()) {
|
||||
categoryRepository.updatePartial(
|
||||
CategoryUpdate(
|
||||
id = category.id,
|
||||
flags = flags,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
preferences.librarySortingMode().set(LibrarySort(type, direction))
|
||||
categoryRepository.updateAllFlags(flags)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun await(category: Category, type: LibrarySort.Type, direction: LibrarySort.Direction) {
|
||||
await(category.id, type, direction)
|
||||
}
|
||||
}
|
|
@ -2,14 +2,15 @@ package eu.kanade.domain.category.interactor
|
|||
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
|
||||
class UpdateCategoryAnime(
|
||||
class UpdateAnimeCategory(
|
||||
private val categoryRepository: CategoryRepositoryAnime,
|
||||
) {
|
||||
|
||||
suspend fun await(payload: CategoryUpdate): Result {
|
||||
return try {
|
||||
categoryRepository.update(payload)
|
||||
suspend fun await(payload: CategoryUpdate): Result = withNonCancellableContext {
|
||||
try {
|
||||
categoryRepository.updatePartial(payload)
|
||||
Result.Success
|
||||
} catch (e: Exception) {
|
||||
Result.Error(e)
|
|
@ -6,6 +6,8 @@ import kotlinx.coroutines.flow.Flow
|
|||
|
||||
interface CategoryRepositoryAnime {
|
||||
|
||||
suspend fun get(id: Long): Category?
|
||||
|
||||
suspend fun getAll(): List<Category>
|
||||
|
||||
fun getAllAsFlow(): Flow<List<Category>>
|
||||
|
@ -14,13 +16,13 @@ interface CategoryRepositoryAnime {
|
|||
|
||||
fun getCategoriesByAnimeIdAsFlow(animeId: Long): Flow<List<Category>>
|
||||
|
||||
@Throws(DuplicateNameException::class)
|
||||
suspend fun insert(name: String, order: Long)
|
||||
suspend fun insert(category: Category)
|
||||
|
||||
@Throws(DuplicateNameException::class)
|
||||
suspend fun update(payload: CategoryUpdate)
|
||||
suspend fun updatePartial(update: CategoryUpdate)
|
||||
|
||||
suspend fun updatePartial(updates: List<CategoryUpdate>)
|
||||
|
||||
suspend fun updateAllFlags(flags: Long?)
|
||||
|
||||
suspend fun delete(categoryId: Long)
|
||||
|
||||
suspend fun checkDuplicateName(name: String): Boolean
|
||||
}
|
||||
|
|
|
@ -12,12 +12,18 @@ class DownloadPreferences(
|
|||
|
||||
fun downloadOnlyOverWifi() = preferenceStore.getBoolean("pref_download_only_over_wifi_key", true)
|
||||
|
||||
fun useExternalDownloader() = preferenceStore.getBoolean("use_external_downloader", false)
|
||||
|
||||
fun externalDownloaderSelection() = preferenceStore.getString("external_downloader_selection", "")
|
||||
|
||||
fun saveChaptersAsCBZ() = preferenceStore.getBoolean("save_chapter_as_cbz", true)
|
||||
|
||||
fun splitTallImages() = preferenceStore.getBoolean("split_tall_images", false)
|
||||
|
||||
fun autoDownloadWhileReading() = preferenceStore.getInt("auto_download_while_reading", 0)
|
||||
|
||||
fun autoDownloadWhileWatching() = preferenceStore.getInt("auto_download_while_watching", 0)
|
||||
|
||||
fun removeAfterReadSlots() = preferenceStore.getInt("remove_after_read_slots", -1)
|
||||
|
||||
fun removeAfterMarkedAsRead() = preferenceStore.getBoolean("pref_remove_after_marked_as_read_key", false)
|
||||
|
@ -25,10 +31,14 @@ class DownloadPreferences(
|
|||
fun removeBookmarkedChapters() = preferenceStore.getBoolean("pref_remove_bookmarked", false)
|
||||
|
||||
fun removeExcludeCategories() = preferenceStore.getStringSet("remove_exclude_categories", emptySet())
|
||||
fun removeExcludeAnimeCategories() = preferenceStore.getStringSet("remove_exclude_anime_categories", emptySet())
|
||||
|
||||
fun downloadNewChapters() = preferenceStore.getBoolean("download_new", false)
|
||||
fun downloadNewEpisodes() = preferenceStore.getBoolean("download_new_episode", false)
|
||||
|
||||
fun downloadNewChapterCategories() = preferenceStore.getStringSet("download_new_categories", emptySet())
|
||||
fun downloadNewEpisodeCategories() = preferenceStore.getStringSet("download_new_anime_categories", emptySet())
|
||||
|
||||
fun downloadNewChapterCategoriesExclude() = preferenceStore.getStringSet("download_new_categories_exclude", emptySet())
|
||||
fun downloadNewEpisodeCategoriesExclude() = preferenceStore.getStringSet("download_new_anime_categories_exclude", emptySet())
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
package eu.kanade.domain.episode.interactor
|
||||
|
||||
import eu.kanade.domain.anime.interactor.GetAnimeFavorites
|
||||
import eu.kanade.domain.anime.interactor.SetAnimeEpisodeFlags
|
||||
import eu.kanade.domain.anime.model.Anime
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
|
||||
class SetAnimeDefaultEpisodeFlags(
|
||||
private val libraryPreferences: LibraryPreferences,
|
||||
private val setAnimeEpisodeFlags: SetAnimeEpisodeFlags,
|
||||
private val getFavorites: GetAnimeFavorites,
|
||||
) {
|
||||
|
||||
suspend fun await(anime: Anime) {
|
||||
withNonCancellableContext {
|
||||
with(libraryPreferences) {
|
||||
setAnimeEpisodeFlags.awaitSetAllFlags(
|
||||
animeId = anime.id,
|
||||
unseenFilter = filterEpisodeBySeen().get(),
|
||||
downloadedFilter = filterEpisodeByDownloaded().get(),
|
||||
bookmarkedFilter = filterEpisodeByBookmarked().get(),
|
||||
sortingMode = sortEpisodeBySourceOrNumber().get(),
|
||||
sortingDirection = sortEpisodeByAscendingOrDescending().get(),
|
||||
displayMode = displayEpisodeByNameOrNumber().get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun awaitAll() {
|
||||
withNonCancellableContext {
|
||||
getFavorites.await().forEach { await(it) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,17 +3,16 @@ package eu.kanade.domain.episode.interactor
|
|||
import eu.kanade.domain.anime.model.Anime
|
||||
import eu.kanade.domain.anime.repository.AnimeRepository
|
||||
import eu.kanade.domain.animedownload.interactor.DeleteAnimeDownload
|
||||
import eu.kanade.domain.download.service.DownloadPreferences
|
||||
import eu.kanade.domain.episode.model.Episode
|
||||
import eu.kanade.domain.episode.model.EpisodeUpdate
|
||||
import eu.kanade.domain.episode.repository.EpisodeRepository
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.withContext
|
||||
import logcat.LogPriority
|
||||
|
||||
class SetSeenStatus(
|
||||
private val preferences: PreferencesHelper,
|
||||
private val downloadPreferences: DownloadPreferences,
|
||||
private val deleteDownload: DeleteAnimeDownload,
|
||||
private val animeRepository: AnimeRepository,
|
||||
private val episodeRepository: EpisodeRepository,
|
||||
|
@ -27,38 +26,33 @@ class SetSeenStatus(
|
|||
)
|
||||
}
|
||||
|
||||
suspend fun await(seen: Boolean, vararg values: Episode): Result = withContext(NonCancellable) f@{
|
||||
val episodes = values.filterNot { it.seen == seen }
|
||||
|
||||
if (episodes.isEmpty()) {
|
||||
return@f Result.NoEpisodes
|
||||
suspend fun await(seen: Boolean, vararg episodes: Episode): Result = withNonCancellableContext {
|
||||
val episodesToUpdate = episodes.filter {
|
||||
when (seen) {
|
||||
true -> !it.seen
|
||||
false -> it.seen || it.lastSecondSeen > 0
|
||||
}
|
||||
|
||||
val anime = episodes.fold(mutableSetOf<Anime>()) { acc, episode ->
|
||||
if (acc.all { it.id != episode.animeId }) {
|
||||
acc += animeRepository.getAnimeById(episode.animeId)
|
||||
}
|
||||
acc
|
||||
if (episodesToUpdate.isEmpty()) {
|
||||
return@withNonCancellableContext Result.NoEpisodes
|
||||
}
|
||||
|
||||
try {
|
||||
episodeRepository.updateAll(
|
||||
episodes.map { episode ->
|
||||
mapper(episode, seen)
|
||||
},
|
||||
episodesToUpdate.map { mapper(it, seen) },
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
return@f Result.InternalError(e)
|
||||
return@withNonCancellableContext Result.InternalError(e)
|
||||
}
|
||||
|
||||
if (seen && preferences.removeAfterMarkedAsRead()) {
|
||||
anime.forEach { anime ->
|
||||
if (seen && downloadPreferences.removeAfterMarkedAsRead().get()) {
|
||||
episodesToUpdate
|
||||
.groupBy { it.animeId }
|
||||
.forEach { (animeId, episodes) ->
|
||||
deleteDownload.awaitAll(
|
||||
anime = anime,
|
||||
values = episodes
|
||||
.filter { anime.id == it.animeId }
|
||||
.toTypedArray(),
|
||||
anime = animeRepository.getAnimeById(animeId),
|
||||
episodes = episodes.toTypedArray(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -66,10 +60,10 @@ class SetSeenStatus(
|
|||
Result.Success
|
||||
}
|
||||
|
||||
suspend fun await(animeId: Long, seen: Boolean): Result = withContext(NonCancellable) f@{
|
||||
return@f await(
|
||||
suspend fun await(animeId: Long, seen: Boolean): Result = withNonCancellableContext {
|
||||
await(
|
||||
seen = seen,
|
||||
values = episodeRepository
|
||||
episodes = episodeRepository
|
||||
.getEpisodeByAnimeId(animeId)
|
||||
.toTypedArray(),
|
||||
)
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
package eu.kanade.domain.episode.interactor
|
||||
|
||||
import eu.kanade.data.episode.CleanupEpisodeName
|
||||
import eu.kanade.data.episode.NoEpisodesException
|
||||
import eu.kanade.domain.anime.interactor.UpdateAnime
|
||||
import eu.kanade.domain.anime.model.Anime
|
||||
import eu.kanade.domain.episode.model.Episode
|
||||
import eu.kanade.domain.episode.model.toDbEpisode
|
||||
import eu.kanade.domain.episode.model.toEpisodeUpdate
|
||||
import eu.kanade.domain.episode.repository.EpisodeRepository
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.LocalAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.isLocal
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.data.download.AnimeDownloadManager
|
||||
import eu.kanade.tachiyomi.data.animedownload.AnimeDownloadManager
|
||||
import eu.kanade.tachiyomi.data.animedownload.AnimeDownloadProvider
|
||||
import eu.kanade.tachiyomi.util.episode.EpisodeRecognition
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
@ -20,6 +23,7 @@ import java.util.TreeSet
|
|||
|
||||
class SyncEpisodesWithSource(
|
||||
private val downloadManager: AnimeDownloadManager = Injekt.get(),
|
||||
private val downloadProvider: AnimeDownloadProvider = Injekt.get(),
|
||||
private val episodeRepository: EpisodeRepository = Injekt.get(),
|
||||
private val shouldUpdateDbEpisode: ShouldUpdateDbEpisode = Injekt.get(),
|
||||
private val updateAnime: UpdateAnime = Injekt.get(),
|
||||
|
@ -27,12 +31,20 @@ class SyncEpisodesWithSource(
|
|||
private val getEpisodeByAnimeId: GetEpisodeByAnimeId = Injekt.get(),
|
||||
) {
|
||||
|
||||
/**
|
||||
* Method to synchronize db episodes with source ones
|
||||
*
|
||||
* @param rawSourceEpisodes the episodes from the source.
|
||||
* @param anime the anime the episodes belong to.
|
||||
* @param source the source the anime belongs to.
|
||||
* @return Newly added episodes
|
||||
*/
|
||||
suspend fun await(
|
||||
rawSourceEpisodes: List<SEpisode>,
|
||||
anime: Anime,
|
||||
source: AnimeSource,
|
||||
): Pair<List<Episode>, List<Episode>> {
|
||||
if (rawSourceEpisodes.isEmpty() && source.id != LocalAnimeSource.ID) {
|
||||
): List<Episode> {
|
||||
if (rawSourceEpisodes.isEmpty() && !source.isLocal()) {
|
||||
throw NoEpisodesException()
|
||||
}
|
||||
|
||||
|
@ -41,6 +53,7 @@ class SyncEpisodesWithSource(
|
|||
.mapIndexed { i, sEpisode ->
|
||||
Episode.create()
|
||||
.copyFromSEpisode(sEpisode)
|
||||
.copy(name = CleanupEpisodeName.await(sEpisode.name, anime.title))
|
||||
.copy(animeId = anime.id, sourceOrder = i.toLong())
|
||||
}
|
||||
|
||||
|
@ -94,8 +107,11 @@ class SyncEpisodesWithSource(
|
|||
toAdd.add(toAddEpisode)
|
||||
} else {
|
||||
if (shouldUpdateDbEpisode.await(dbEpisode, episode)) {
|
||||
if (dbEpisode.name != episode.name && downloadManager.isEpisodeDownloaded(dbEpisode.name, dbEpisode.scanlator, anime.title, anime.source)) {
|
||||
downloadManager.renameEpisode(source, anime, dbEpisode, episode)
|
||||
val shouldRenameEpisode = downloadProvider.isEpisodeDirNameChanged(dbEpisode, episode) &&
|
||||
downloadManager.isEpisodeDownloaded(dbEpisode.name, dbEpisode.scanlator, anime.title, anime.source)
|
||||
|
||||
if (shouldRenameEpisode) {
|
||||
downloadManager.renameEpisode(source, anime, dbEpisode.toDbEpisode(), episode.toDbEpisode())
|
||||
}
|
||||
var toChangeEpisode = dbEpisode.copy(
|
||||
name = episode.name,
|
||||
|
@ -113,18 +129,18 @@ class SyncEpisodesWithSource(
|
|||
|
||||
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
||||
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
||||
return Pair(emptyList(), emptyList())
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val reAdded = mutableListOf<Episode>()
|
||||
|
||||
val deletedEpisodeNumbers = TreeSet<Float>()
|
||||
val deletedSeenEpisodeNumbers = TreeSet<Float>()
|
||||
val deletedBookmarkedEpisodeNumbers = TreeSet<Float>()
|
||||
|
||||
toDelete.forEach { episode ->
|
||||
if (episode.seen) {
|
||||
deletedSeenEpisodeNumbers.add(episode.episodeNumber)
|
||||
}
|
||||
if (episode.seen) deletedSeenEpisodeNumbers.add(episode.episodeNumber)
|
||||
if (episode.bookmark) deletedBookmarkedEpisodeNumbers.add(episode.episodeNumber)
|
||||
deletedEpisodeNumbers.add(episode.episodeNumber)
|
||||
}
|
||||
|
||||
|
@ -133,20 +149,19 @@ class SyncEpisodesWithSource(
|
|||
|
||||
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
|
||||
// Sources MUST return the episodes from most to less recent, which is common.
|
||||
|
||||
var itemCount = toAdd.size
|
||||
var updatedToAdd = toAdd.map { toAddItem ->
|
||||
var episode = toAddItem.copy(dateFetch = rightNow + itemCount--)
|
||||
|
||||
if (episode.isRecognizedNumber.not() && episode.episodeNumber !in deletedEpisodeNumbers) return@map episode
|
||||
if (episode.isRecognizedNumber.not() || episode.episodeNumber !in deletedEpisodeNumbers) return@map episode
|
||||
|
||||
if (episode.episodeNumber in deletedSeenEpisodeNumbers) {
|
||||
episode = episode.copy(seen = true)
|
||||
}
|
||||
episode = episode.copy(
|
||||
seen = episode.episodeNumber in deletedSeenEpisodeNumbers,
|
||||
bookmark = episode.episodeNumber in deletedBookmarkedEpisodeNumbers,
|
||||
)
|
||||
|
||||
// Try to to use the fetch date of the original entry to not pollute 'Updates' tab
|
||||
val oldDateFetch = deletedEpisodeNumberDateFetchMap[episode.episodeNumber]
|
||||
oldDateFetch?.let {
|
||||
deletedEpisodeNumberDateFetchMap[episode.episodeNumber]?.let {
|
||||
episode = episode.copy(dateFetch = it)
|
||||
}
|
||||
|
||||
|
@ -173,7 +188,8 @@ class SyncEpisodesWithSource(
|
|||
// Note that last_update actually represents last time the episode list changed at all
|
||||
updateAnime.awaitUpdateLastUpdate(anime.id)
|
||||
|
||||
@Suppress("ConvertArgumentToSet") // See tachiyomiorg/tachiyomi#6372.
|
||||
return Pair(updatedToAdd.subtract(reAdded).toList(), toDelete.subtract(reAdded).toList())
|
||||
val reAddedUrls = reAdded.map { it.url }.toHashSet()
|
||||
|
||||
return updatedToAdd.filterNot { it.url in reAddedUrls }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ data class Episode(
|
|||
url = sEpisode.url,
|
||||
dateUpload = sEpisode.date_upload,
|
||||
episodeNumber = sEpisode.episode_number,
|
||||
scanlator = sEpisode.scanlator,
|
||||
scanlator = sEpisode.scanlator?.ifBlank { null },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,8 @@ interface EpisodeRepository {
|
|||
|
||||
suspend fun getEpisodeByAnimeId(animeId: Long): List<Episode>
|
||||
|
||||
suspend fun getBookmarkedEpisodesByAnimeId(animeId: Long): List<Episode>
|
||||
|
||||
suspend fun getEpisodeById(id: Long): Episode?
|
||||
|
||||
fun getEpisodeByAnimeIdAsFlow(animeId: Long): Flow<List<Episode>>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package eu.kanade.domain.library.service
|
||||
|
||||
import eu.kanade.domain.anime.model.Anime
|
||||
import eu.kanade.domain.library.model.LibraryDisplayMode
|
||||
import eu.kanade.domain.library.model.LibrarySort
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
|
@ -13,6 +14,7 @@ import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
|||
class LibraryPreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
) {
|
||||
fun bottomNavStyle() = preferenceStore.getInt("bottom_nav_style", 0)
|
||||
|
||||
fun libraryDisplayMode() = preferenceStore.getObject("pref_display_mode_library", LibraryDisplayMode.default, LibraryDisplayMode.Serializer::serialize, LibraryDisplayMode.Serializer::deserialize)
|
||||
|
||||
|
@ -60,41 +62,54 @@ class LibraryPreferences(
|
|||
|
||||
fun showUpdatesNavBadge() = preferenceStore.getBoolean("library_update_show_tab_badge", false)
|
||||
fun unreadUpdatesCount() = preferenceStore.getInt("library_unread_updates_count", 0)
|
||||
fun unseenUpdatesCount() = preferenceStore.getInt("library_unseen_updates_count", 0)
|
||||
|
||||
// endregion
|
||||
|
||||
// region Category
|
||||
|
||||
fun defaultCategory() = preferenceStore.getInt("default_category", -1)
|
||||
fun defaultAnimeCategory() = preferenceStore.getInt("default_anime_category", -1)
|
||||
|
||||
fun lastUsedCategory() = preferenceStore.getInt("last_used_category", 0)
|
||||
fun lastUsedAnimeCategory() = preferenceStore.getInt("last_used_anime_category", 0)
|
||||
|
||||
fun categoryTabs() = preferenceStore.getBoolean("display_category_tabs", true)
|
||||
fun animeCategoryTabs() = preferenceStore.getBoolean("display_anime_category_tabs", true)
|
||||
|
||||
fun categoryNumberOfItems() = preferenceStore.getBoolean("display_number_of_items", false)
|
||||
fun animeCategoryNumberOfItems() = preferenceStore.getBoolean("display_number_of_items_anime", false)
|
||||
|
||||
fun categorizedDisplaySettings() = preferenceStore.getBoolean("categorized_display", false)
|
||||
|
||||
fun libraryUpdateCategories() = preferenceStore.getStringSet("library_update_categories", emptySet())
|
||||
fun animelibUpdateCategories() = preferenceStore.getStringSet("animelib_update_categories", emptySet())
|
||||
|
||||
fun libraryUpdateCategoriesExclude() = preferenceStore.getStringSet("library_update_categories_exclude", emptySet())
|
||||
fun animelibUpdateCategoriesExclude() = preferenceStore.getStringSet("animelib_update_categories_exclude", emptySet())
|
||||
|
||||
// endregion
|
||||
|
||||
// region Chapter
|
||||
|
||||
fun filterChapterByRead() = preferenceStore.getLong("default_chapter_filter_by_read", Manga.SHOW_ALL)
|
||||
fun filterEpisodeBySeen() = preferenceStore.getLong("default_episode_filter_by_seen", Anime.SHOW_ALL)
|
||||
|
||||
fun filterChapterByDownloaded() = preferenceStore.getLong("default_chapter_filter_by_downloaded", Manga.SHOW_ALL)
|
||||
fun filterEpisodeByDownloaded() = preferenceStore.getLong("default_episode_filter_by_downloaded", Anime.SHOW_ALL)
|
||||
|
||||
fun filterChapterByBookmarked() = preferenceStore.getLong("default_chapter_filter_by_bookmarked", Manga.SHOW_ALL)
|
||||
fun filterEpisodeByBookmarked() = preferenceStore.getLong("default_episode_filter_by_bookmarked", Anime.SHOW_ALL)
|
||||
|
||||
// and upload date
|
||||
fun sortChapterBySourceOrNumber() = preferenceStore.getLong("default_chapter_sort_by_source_or_number", Manga.CHAPTER_SORTING_SOURCE)
|
||||
fun sortEpisodeBySourceOrNumber() = preferenceStore.getLong("default_episode_sort_by_source_or_number", Anime.EPISODE_SORTING_SOURCE)
|
||||
|
||||
fun displayChapterByNameOrNumber() = preferenceStore.getLong("default_chapter_display_by_name_or_number", Manga.CHAPTER_DISPLAY_NAME)
|
||||
fun displayEpisodeByNameOrNumber() = preferenceStore.getLong("default_chapter_display_by_name_or_number", Anime.EPISODE_DISPLAY_NAME)
|
||||
|
||||
fun sortChapterByAscendingOrDescending() = preferenceStore.getLong("default_chapter_sort_by_ascending_or_descending", Manga.CHAPTER_SORT_DESC)
|
||||
fun sortEpisodeByAscendingOrDescending() = preferenceStore.getLong("default_chapter_sort_by_ascending_or_descending", Anime.EPISODE_SORT_DESC)
|
||||
|
||||
fun setChapterSettingsDefault(manga: Manga) {
|
||||
filterChapterByRead().set(manga.unreadFilterRaw)
|
||||
|
@ -105,6 +120,15 @@ class LibraryPreferences(
|
|||
sortChapterByAscendingOrDescending().set(if (manga.sortDescending()) Manga.CHAPTER_SORT_DESC else Manga.CHAPTER_SORT_ASC)
|
||||
}
|
||||
|
||||
fun setEpisodeSettingsDefault(anime: Anime) {
|
||||
filterEpisodeBySeen().set(anime.unseenFilterRaw)
|
||||
filterEpisodeByDownloaded().set(anime.downloadedFilterRaw)
|
||||
filterEpisodeByBookmarked().set(anime.bookmarkedFilterRaw)
|
||||
sortEpisodeBySourceOrNumber().set(anime.sorting)
|
||||
displayEpisodeByNameOrNumber().set(anime.displayMode)
|
||||
sortEpisodeByAscendingOrDescending().set(if (anime.sortDescending()) Anime.EPISODE_SORT_DESC else Anime.EPISODE_SORT_ASC)
|
||||
}
|
||||
|
||||
fun autoClearChapterCache() = preferenceStore.getBoolean("auto_clear_chapter_cache", false)
|
||||
|
||||
// endregion
|
||||
|
|
|
@ -16,12 +16,18 @@ class SourcePreferences(
|
|||
|
||||
fun disabledSources() = preferenceStore.getStringSet("hidden_catalogues", emptySet())
|
||||
|
||||
fun disabledAnimeSources() = preferenceStore.getStringSet("hidden_anime_catalogues", emptySet())
|
||||
|
||||
fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet())
|
||||
|
||||
fun pinnedAnimeSources() = preferenceStore.getStringSet("pinned_anime_catalogues", emptySet())
|
||||
|
||||
fun duplicatePinnedSources() = preferenceStore.getBoolean("duplicate_pinned_sources", false)
|
||||
|
||||
fun lastUsedSource() = preferenceStore.getLong("last_catalogue_source", -1)
|
||||
|
||||
fun lastUsedAnimeSource() = preferenceStore.getLong("last_anime_catalogue_source", -1)
|
||||
|
||||
fun showNsfwSource() = preferenceStore.getBoolean("show_nsfw_source", true)
|
||||
|
||||
fun migrationSortingMode() = preferenceStore.getEnum("pref_migration_sorting", SetMigrateSorting.Mode.ALPHABETICAL)
|
||||
|
@ -30,7 +36,11 @@ class SourcePreferences(
|
|||
|
||||
fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)
|
||||
|
||||
fun animeextensionUpdatesCount() = preferenceStore.getInt("animeext_updates_count", 0)
|
||||
|
||||
fun trustedSignatures() = preferenceStore.getStringSet("trusted_signatures", emptySet())
|
||||
|
||||
fun searchPinnedSourcesOnly() = preferenceStore.getBoolean("search_pinned_sources_only", false)
|
||||
|
||||
fun searchAnimePinnedSourcesOnly() = preferenceStore.getBoolean("search_pinned_anime_sources_only", false)
|
||||
}
|
||||
|
|
|
@ -2,17 +2,12 @@ package eu.kanade.presentation.anime
|
|||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.rememberSplineBasedDecay
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.rememberScrollableState
|
||||
import androidx.compose.foundation.gestures.scrollBy
|
||||
import androidx.compose.foundation.gestures.scrollable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
|
@ -24,7 +19,6 @@ import androidx.compose.foundation.layout.navigationBars
|
|||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
|
@ -36,72 +30,54 @@ import androidx.compose.material3.Icon
|
|||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberTopAppBarScrollState
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||
import eu.kanade.domain.anime.model.Anime.Companion.EPISODE_DISPLAY_NUMBER
|
||||
import eu.kanade.domain.episode.model.Episode
|
||||
import eu.kanade.presentation.anime.components.AnimeBottomActionMenu
|
||||
import eu.kanade.presentation.anime.components.AnimeActionRow
|
||||
import eu.kanade.presentation.anime.components.AnimeEpisodeListItem
|
||||
import eu.kanade.presentation.anime.components.AnimeInfoHeader
|
||||
import eu.kanade.presentation.anime.components.AnimeSmallAppBar
|
||||
import eu.kanade.presentation.anime.components.AnimeTopAppBar
|
||||
import eu.kanade.presentation.anime.components.AnimeInfoBox
|
||||
import eu.kanade.presentation.anime.components.EpisodeHeader
|
||||
import eu.kanade.presentation.anime.components.ExpandableAnimeDescription
|
||||
import eu.kanade.presentation.components.AnimeBottomActionMenu
|
||||
import eu.kanade.presentation.components.EpisodeDownloadAction
|
||||
import eu.kanade.presentation.components.ExtendedFloatingActionButton
|
||||
import eu.kanade.presentation.components.LazyColumn
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.components.SwipeRefreshIndicator
|
||||
import eu.kanade.presentation.components.SwipeRefresh
|
||||
import eu.kanade.presentation.components.TwoPanelBox
|
||||
import eu.kanade.presentation.components.VerticalFastScroller
|
||||
import eu.kanade.presentation.manga.DownloadAction
|
||||
import eu.kanade.presentation.manga.EpisodeDownloadAction
|
||||
import eu.kanade.presentation.util.ExitUntilCollapsedScrollBehavior
|
||||
import eu.kanade.presentation.manga.MangaScreenItem
|
||||
import eu.kanade.presentation.manga.components.MangaToolbar
|
||||
import eu.kanade.presentation.util.isScrolledToEnd
|
||||
import eu.kanade.presentation.util.isScrollingUp
|
||||
import eu.kanade.presentation.util.plus
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
|
||||
import eu.kanade.tachiyomi.animesource.getNameForAnimeInfo
|
||||
import eu.kanade.tachiyomi.data.download.model.AnimeDownload
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.data.animedownload.model.AnimeDownload
|
||||
import eu.kanade.tachiyomi.ui.anime.AnimeScreenState
|
||||
import eu.kanade.tachiyomi.ui.anime.EpisodeItem
|
||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import eu.kanade.tachiyomi.ui.player.setting.PlayerPreferences
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private val episodeDecimalFormat = DecimalFormat(
|
||||
"#.###",
|
||||
DecimalFormatSymbols()
|
||||
.apply { decimalSeparator = '.' },
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun AnimeScreen(
|
||||
state: AnimeScreenState.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
windowWidthSizeClass: WindowWidthSizeClass,
|
||||
isTabletUi: Boolean,
|
||||
onBackClicked: () -> Unit,
|
||||
onEpisodeClicked: (Episode, Boolean) -> Unit,
|
||||
onDownloadEpisode: ((List<EpisodeItem>, EpisodeDownloadAction) -> Unit)?,
|
||||
|
@ -130,8 +106,12 @@ fun AnimeScreen(
|
|||
onMarkPreviousAsSeenClicked: (Episode) -> Unit,
|
||||
onMultiDeleteClicked: (List<Episode>) -> Unit,
|
||||
|
||||
// Episode selection
|
||||
onEpisodeSelected: (EpisodeItem, Boolean, Boolean, Boolean) -> Unit,
|
||||
onAllEpisodeSelected: (Boolean) -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
) {
|
||||
if (windowWidthSizeClass == WindowWidthSizeClass.Compact) {
|
||||
if (!isTabletUi) {
|
||||
AnimeScreenSmallImpl(
|
||||
state = state,
|
||||
snackbarHostState = snackbarHostState,
|
||||
|
@ -142,9 +122,9 @@ fun AnimeScreen(
|
|||
onWebViewClicked = onWebViewClicked,
|
||||
onTrackingClicked = onTrackingClicked,
|
||||
onTagClicked = onTagClicked,
|
||||
onFilterButtonClicked = onFilterButtonClicked,
|
||||
onFilterClicked = onFilterButtonClicked,
|
||||
onRefresh = onRefresh,
|
||||
onContinueReading = onContinueWatching,
|
||||
onContinueWatching = onContinueWatching,
|
||||
onSearch = onSearch,
|
||||
onCoverClicked = onCoverClicked,
|
||||
onShareClicked = onShareClicked,
|
||||
|
@ -156,11 +136,13 @@ fun AnimeScreen(
|
|||
onMultiMarkAsSeenClicked = onMultiMarkAsSeenClicked,
|
||||
onMarkPreviousAsSeenClicked = onMarkPreviousAsSeenClicked,
|
||||
onMultiDeleteClicked = onMultiDeleteClicked,
|
||||
onEpisodeSelected = onEpisodeSelected,
|
||||
onAllEpisodeSelected = onAllEpisodeSelected,
|
||||
onInvertSelection = onInvertSelection,
|
||||
)
|
||||
} else {
|
||||
AnimeScreenLargeImpl(
|
||||
state = state,
|
||||
windowWidthSizeClass = windowWidthSizeClass,
|
||||
snackbarHostState = snackbarHostState,
|
||||
onBackClicked = onBackClicked,
|
||||
onEpisodeClicked = onEpisodeClicked,
|
||||
|
@ -171,7 +153,7 @@ fun AnimeScreen(
|
|||
onTagClicked = onTagClicked,
|
||||
onFilterButtonClicked = onFilterButtonClicked,
|
||||
onRefresh = onRefresh,
|
||||
onContinueReading = onContinueWatching,
|
||||
onContinueWatching = onContinueWatching,
|
||||
onSearch = onSearch,
|
||||
onCoverClicked = onCoverClicked,
|
||||
onShareClicked = onShareClicked,
|
||||
|
@ -183,6 +165,9 @@ fun AnimeScreen(
|
|||
onMultiMarkAsSeenClicked = onMultiMarkAsSeenClicked,
|
||||
onMarkPreviousAsSeenClicked = onMarkPreviousAsSeenClicked,
|
||||
onMultiDeleteClicked = onMultiDeleteClicked,
|
||||
onEpisodeSelected = onEpisodeSelected,
|
||||
onAllEpisodeSelected = onAllEpisodeSelected,
|
||||
onInvertSelection = onInvertSelection,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -198,9 +183,9 @@ private fun AnimeScreenSmallImpl(
|
|||
onWebViewClicked: (() -> Unit)?,
|
||||
onTrackingClicked: (() -> Unit)?,
|
||||
onTagClicked: (String) -> Unit,
|
||||
onFilterButtonClicked: () -> Unit,
|
||||
onFilterClicked: () -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
onContinueReading: () -> Unit,
|
||||
onContinueWatching: () -> Unit,
|
||||
onSearch: (query: String, global: Boolean) -> Unit,
|
||||
|
||||
// For cover dialog
|
||||
|
@ -215,121 +200,64 @@ private fun AnimeScreenSmallImpl(
|
|||
|
||||
// For bottom action menu
|
||||
onMultiBookmarkClicked: (List<Episode>, bookmarked: Boolean) -> Unit,
|
||||
onMultiMarkAsSeenClicked: (List<Episode>, markAsRead: Boolean) -> Unit,
|
||||
onMultiMarkAsSeenClicked: (List<Episode>, markAsSeen: Boolean) -> Unit,
|
||||
onMarkPreviousAsSeenClicked: (Episode) -> Unit,
|
||||
onMultiDeleteClicked: (List<Episode>) -> Unit,
|
||||
|
||||
// Episode selection
|
||||
onEpisodeSelected: (EpisodeItem, Boolean, Boolean, Boolean) -> Unit,
|
||||
onAllEpisodeSelected: (Boolean) -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
) {
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
|
||||
val scrollBehavior = ExitUntilCollapsedScrollBehavior(rememberTopAppBarScrollState(), decayAnimationSpec)
|
||||
val episodeListState = rememberLazyListState()
|
||||
SideEffect {
|
||||
if (episodeListState.firstVisibleItemIndex > 0 || episodeListState.firstVisibleItemScrollOffset > 0) {
|
||||
// Should go here after a configuration change
|
||||
// Safe to say that the app bar is fully scrolled
|
||||
scrollBehavior.state.offset = scrollBehavior.state.offsetLimit
|
||||
}
|
||||
}
|
||||
|
||||
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
||||
val (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(1) }
|
||||
SwipeRefresh(
|
||||
state = rememberSwipeRefreshState(state.isRefreshingInfo || state.isRefreshingEpisode),
|
||||
onRefresh = onRefresh,
|
||||
indicatorPadding = PaddingValues(
|
||||
start = insetPadding.calculateStartPadding(layoutDirection),
|
||||
top = with(LocalDensity.current) { topBarHeight.toDp() },
|
||||
end = insetPadding.calculateEndPadding(layoutDirection),
|
||||
),
|
||||
indicator = { s, trigger ->
|
||||
SwipeRefreshIndicator(
|
||||
state = s,
|
||||
refreshTriggerDistance = trigger,
|
||||
)
|
||||
},
|
||||
) {
|
||||
val episodes = remember(state) { state.processedEpisodes.toList() }
|
||||
val selected = remember(episodes) { emptyList<EpisodeItem>().toMutableStateList() }
|
||||
val selectedPositions = remember(episodes) { arrayOf(-1, -1) } // first and last selected index in list
|
||||
|
||||
val internalOnBackPressed = {
|
||||
if (selected.isNotEmpty()) {
|
||||
selected.clear()
|
||||
if (episodes.any { it.selected }) {
|
||||
onAllEpisodeSelected(false)
|
||||
} else {
|
||||
onBackClicked()
|
||||
}
|
||||
}
|
||||
BackHandler(onBack = internalOnBackPressed)
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.padding(insetPadding),
|
||||
.padding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal).asPaddingValues()),
|
||||
topBar = {
|
||||
AnimeTopAppBar(
|
||||
modifier = Modifier
|
||||
.scrollable(
|
||||
state = rememberScrollableState {
|
||||
var consumed = runBlocking { episodeListState.scrollBy(-it) } * -1
|
||||
if (consumed == 0f) {
|
||||
// Pass scroll to app bar if we're on the top of the list
|
||||
val newOffset =
|
||||
(scrollBehavior.state.offset + it).coerceIn(scrollBehavior.state.offsetLimit, 0f)
|
||||
consumed = newOffset - scrollBehavior.state.offset
|
||||
scrollBehavior.state.offset = newOffset
|
||||
val firstVisibleItemIndex by remember {
|
||||
derivedStateOf { episodeListState.firstVisibleItemIndex }
|
||||
}
|
||||
consumed
|
||||
},
|
||||
orientation = Orientation.Vertical,
|
||||
interactionSource = episodeListState.interactionSource as MutableInteractionSource,
|
||||
),
|
||||
val firstVisibleItemScrollOffset by remember {
|
||||
derivedStateOf { episodeListState.firstVisibleItemScrollOffset }
|
||||
}
|
||||
val animatedTitleAlpha by animateFloatAsState(
|
||||
if (firstVisibleItemIndex > 0) 1f else 0f,
|
||||
)
|
||||
val animatedBgAlpha by animateFloatAsState(
|
||||
if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
|
||||
)
|
||||
MangaToolbar(
|
||||
title = state.anime.title,
|
||||
author = state.anime.author,
|
||||
artist = state.anime.artist,
|
||||
description = state.anime.description,
|
||||
tagsProvider = { state.anime.genre },
|
||||
coverDataProvider = { state.anime },
|
||||
sourceName = remember { state.source.getNameForAnimeInfo() },
|
||||
isStubSource = remember { state.source is SourceManager.StubSource },
|
||||
favorite = state.anime.favorite,
|
||||
status = state.anime.status,
|
||||
trackingCount = state.trackingCount,
|
||||
episodeCount = episodes.size,
|
||||
episodeFiltered = state.anime.episodesFiltered(),
|
||||
titleAlphaProvider = { animatedTitleAlpha },
|
||||
backgroundAlphaProvider = { animatedBgAlpha },
|
||||
hasFilters = state.anime.episodesFiltered(),
|
||||
incognitoMode = state.isIncognitoMode,
|
||||
downloadedOnlyMode = state.isDownloadedOnlyMode,
|
||||
fromSource = state.isFromSource,
|
||||
onBackClicked = internalOnBackPressed,
|
||||
onCoverClick = onCoverClicked,
|
||||
onTagClicked = onTagClicked,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
onTrackingClicked = onTrackingClicked,
|
||||
onFilterButtonClicked = onFilterButtonClicked,
|
||||
onShareClicked = onShareClicked,
|
||||
onDownloadClicked = onDownloadActionClicked,
|
||||
onEditCategoryClicked = onEditCategoryClicked,
|
||||
onMigrateClicked = onMigrateClicked,
|
||||
changeAnimeSkipIntro = changeAnimeSkipIntro,
|
||||
doGlobalSearch = onSearch,
|
||||
scrollBehavior = scrollBehavior,
|
||||
actionModeCounter = selected.size,
|
||||
onSelectAll = {
|
||||
selected.clear()
|
||||
selected.addAll(episodes)
|
||||
},
|
||||
onInvertSelection = {
|
||||
val toSelect = episodes - selected
|
||||
selected.clear()
|
||||
selected.addAll(toSelect)
|
||||
},
|
||||
onSmallAppBarHeightChanged = onTopBarHeightChanged,
|
||||
onClickFilter = onFilterClicked,
|
||||
onClickShare = onShareClicked,
|
||||
onClickDownload = onDownloadActionClicked,
|
||||
onClickEditCategory = onEditCategoryClicked,
|
||||
onClickMigrate = onMigrateClicked,
|
||||
actionModeCounter = episodes.count { it.selected },
|
||||
onSelectAll = { onAllEpisodeSelected(true) },
|
||||
onInvertSelection = { onInvertSelection() },
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
SharedAnimeBottomActionMenu(
|
||||
selected = selected,
|
||||
selected = episodes.filter { it.selected },
|
||||
onEpisodeClicked = onEpisodeClicked,
|
||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||
onMultiMarkAsSeenClicked = onMultiMarkAsSeenClicked,
|
||||
|
@ -342,7 +270,7 @@ private fun AnimeScreenSmallImpl(
|
|||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
floatingActionButton = {
|
||||
AnimatedVisibility(
|
||||
visible = episodes.any { !it.episode.seen } && selected.isEmpty(),
|
||||
visible = episodes.any { !it.episode.seen } && episodes.none { it.selected },
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
|
@ -355,35 +283,102 @@ private fun AnimeScreenSmallImpl(
|
|||
}
|
||||
Text(text = stringResource(id))
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PlayArrow,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
onClick = onContinueWatching,
|
||||
expanded = episodeListState.isScrollingUp() || episodeListState.isScrolledToEnd(),
|
||||
modifier = Modifier
|
||||
.padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()),
|
||||
.padding(
|
||||
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom)
|
||||
.asPaddingValues(),
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
) { contentPadding ->
|
||||
val withNavBarContentPadding = contentPadding +
|
||||
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
|
||||
val topPadding = contentPadding.calculateTopPadding()
|
||||
|
||||
SwipeRefresh(
|
||||
refreshing = state.isRefreshingData,
|
||||
onRefresh = onRefresh,
|
||||
enabled = episodes.none { it.selected },
|
||||
indicatorPadding = contentPadding,
|
||||
) {
|
||||
VerticalFastScroller(
|
||||
listState = episodeListState,
|
||||
thumbAllowed = { scrollBehavior.state.offset == scrollBehavior.state.offsetLimit },
|
||||
topContentPadding = withNavBarContentPadding.calculateTopPadding(),
|
||||
endContentPadding = withNavBarContentPadding.calculateEndPadding(LocalLayoutDirection.current),
|
||||
topContentPadding = topPadding,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
state = episodeListState,
|
||||
contentPadding = withNavBarContentPadding,
|
||||
contentPadding = PaddingValues(
|
||||
bottom = contentPadding.calculateBottomPadding(),
|
||||
),
|
||||
) {
|
||||
item(
|
||||
key = MangaScreenItem.INFO_BOX,
|
||||
contentType = MangaScreenItem.INFO_BOX,
|
||||
) {
|
||||
AnimeInfoBox(
|
||||
isTabletUi = false,
|
||||
appBarPadding = topPadding,
|
||||
title = state.anime.title,
|
||||
author = state.anime.author,
|
||||
artist = state.anime.artist,
|
||||
sourceName = remember { state.source.getNameForAnimeInfo() },
|
||||
isStubSource = remember { state.source is AnimeSourceManager.StubAnimeSource },
|
||||
coverDataProvider = { state.anime },
|
||||
status = state.anime.status,
|
||||
onCoverClick = onCoverClicked,
|
||||
doSearch = onSearch,
|
||||
)
|
||||
}
|
||||
|
||||
item(
|
||||
key = MangaScreenItem.ACTION_ROW,
|
||||
contentType = MangaScreenItem.ACTION_ROW,
|
||||
) {
|
||||
AnimeActionRow(
|
||||
favorite = state.anime.favorite,
|
||||
trackingCount = state.trackingCount,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
onTrackingClicked = onTrackingClicked,
|
||||
onEditCategory = onEditCategoryClicked,
|
||||
)
|
||||
}
|
||||
|
||||
item(
|
||||
key = MangaScreenItem.DESCRIPTION_WITH_TAG,
|
||||
contentType = MangaScreenItem.DESCRIPTION_WITH_TAG,
|
||||
) {
|
||||
ExpandableAnimeDescription(
|
||||
defaultExpandState = state.isFromSource,
|
||||
description = state.anime.description,
|
||||
tagsProvider = { state.anime.genre },
|
||||
onTagClicked = onTagClicked,
|
||||
)
|
||||
}
|
||||
|
||||
item(
|
||||
key = MangaScreenItem.CHAPTER_HEADER,
|
||||
contentType = MangaScreenItem.CHAPTER_HEADER,
|
||||
) {
|
||||
EpisodeHeader(
|
||||
episodeCount = episodes.size,
|
||||
onClick = onFilterClicked,
|
||||
)
|
||||
}
|
||||
|
||||
sharedEpisodeItems(
|
||||
episodes = episodes,
|
||||
state = state,
|
||||
selected = selected,
|
||||
selectedPositions = selectedPositions,
|
||||
onEpisodeClicked = onEpisodeClicked,
|
||||
onDownloadEpisode = onDownloadEpisode,
|
||||
onEpisodeSelected = onEpisodeSelected,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -394,7 +389,6 @@ private fun AnimeScreenSmallImpl(
|
|||
@Composable
|
||||
fun AnimeScreenLargeImpl(
|
||||
state: AnimeScreenState.Success,
|
||||
windowWidthSizeClass: WindowWidthSizeClass,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
onBackClicked: () -> Unit,
|
||||
onEpisodeClicked: (Episode, Boolean) -> Unit,
|
||||
|
@ -405,7 +399,7 @@ fun AnimeScreenLargeImpl(
|
|||
onTagClicked: (String) -> Unit,
|
||||
onFilterButtonClicked: () -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
onContinueReading: () -> Unit,
|
||||
onContinueWatching: () -> Unit,
|
||||
onSearch: (query: String, global: Boolean) -> Unit,
|
||||
|
||||
// For cover dialog
|
||||
|
@ -420,40 +414,37 @@ fun AnimeScreenLargeImpl(
|
|||
|
||||
// For bottom action menu
|
||||
onMultiBookmarkClicked: (List<Episode>, bookmarked: Boolean) -> Unit,
|
||||
onMultiMarkAsSeenClicked: (List<Episode>, markAsRead: Boolean) -> Unit,
|
||||
onMultiMarkAsSeenClicked: (List<Episode>, markAsSeen: Boolean) -> Unit,
|
||||
onMarkPreviousAsSeenClicked: (Episode) -> Unit,
|
||||
onMultiDeleteClicked: (List<Episode>) -> Unit,
|
||||
|
||||
// Episode selection
|
||||
onEpisodeSelected: (EpisodeItem, Boolean, Boolean, Boolean) -> Unit,
|
||||
onAllEpisodeSelected: (Boolean) -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
) {
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
val density = LocalDensity.current
|
||||
|
||||
val episodes = remember(state) { state.processedEpisodes.toList() }
|
||||
|
||||
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
||||
val (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(0) }
|
||||
var topBarHeight by remember { mutableStateOf(0) }
|
||||
SwipeRefresh(
|
||||
state = rememberSwipeRefreshState(state.isRefreshingInfo || state.isRefreshingEpisode),
|
||||
refreshing = state.isRefreshingData,
|
||||
onRefresh = onRefresh,
|
||||
enabled = episodes.none { it.selected },
|
||||
indicatorPadding = PaddingValues(
|
||||
start = insetPadding.calculateStartPadding(layoutDirection),
|
||||
top = with(density) { topBarHeight.toDp() },
|
||||
end = insetPadding.calculateEndPadding(layoutDirection),
|
||||
),
|
||||
clipIndicatorToPadding = true,
|
||||
indicator = { s, trigger ->
|
||||
SwipeRefreshIndicator(
|
||||
state = s,
|
||||
refreshTriggerDistance = trigger,
|
||||
)
|
||||
},
|
||||
) {
|
||||
val episodeListState = rememberLazyListState()
|
||||
val episodes = remember(state) { state.processedEpisodes.toList() }
|
||||
val selected = remember(episodes) { emptyList<EpisodeItem>().toMutableStateList() }
|
||||
val selectedPositions = remember(episodes) { arrayOf(-1, -1) } // first and last selected index in list
|
||||
|
||||
val internalOnBackPressed = {
|
||||
if (selected.isNotEmpty()) {
|
||||
selected.clear()
|
||||
if (episodes.any { it.selected }) {
|
||||
onAllEpisodeSelected(false)
|
||||
} else {
|
||||
onBackClicked()
|
||||
}
|
||||
|
@ -463,29 +454,23 @@ fun AnimeScreenLargeImpl(
|
|||
Scaffold(
|
||||
modifier = Modifier.padding(insetPadding),
|
||||
topBar = {
|
||||
AnimeSmallAppBar(
|
||||
modifier = Modifier.onSizeChanged { onTopBarHeightChanged(it.height) },
|
||||
MangaToolbar(
|
||||
modifier = Modifier.onSizeChanged { topBarHeight = (it.height) },
|
||||
title = state.anime.title,
|
||||
titleAlphaProvider = { if (selected.isEmpty()) 0f else 1f },
|
||||
titleAlphaProvider = { if (episodes.any { it.selected }) 1f else 0f },
|
||||
backgroundAlphaProvider = { 1f },
|
||||
hasFilters = state.anime.episodesFiltered(),
|
||||
incognitoMode = state.isIncognitoMode,
|
||||
downloadedOnlyMode = state.isDownloadedOnlyMode,
|
||||
onBackClicked = internalOnBackPressed,
|
||||
onShareClicked = onShareClicked,
|
||||
onDownloadClicked = onDownloadActionClicked,
|
||||
onEditCategoryClicked = onEditCategoryClicked,
|
||||
onMigrateClicked = onMigrateClicked,
|
||||
changeAnimeSkipIntro = changeAnimeSkipIntro,
|
||||
actionModeCounter = selected.size,
|
||||
onSelectAll = {
|
||||
selected.clear()
|
||||
selected.addAll(episodes)
|
||||
},
|
||||
onInvertSelection = {
|
||||
val toSelect = episodes - selected
|
||||
selected.clear()
|
||||
selected.addAll(toSelect)
|
||||
},
|
||||
onClickFilter = onFilterButtonClicked,
|
||||
onClickShare = onShareClicked,
|
||||
onClickDownload = onDownloadActionClicked,
|
||||
onClickEditCategory = onEditCategoryClicked,
|
||||
onClickMigrate = onMigrateClicked,
|
||||
actionModeCounter = episodes.count { it.selected },
|
||||
onSelectAll = { onAllEpisodeSelected(true) },
|
||||
onInvertSelection = { onInvertSelection() },
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
|
@ -494,7 +479,7 @@ fun AnimeScreenLargeImpl(
|
|||
contentAlignment = Alignment.BottomEnd,
|
||||
) {
|
||||
SharedAnimeBottomActionMenu(
|
||||
selected = selected,
|
||||
selected = episodes.filter { it.selected },
|
||||
onEpisodeClicked = onEpisodeClicked,
|
||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||
onMultiMarkAsSeenClicked = onMultiMarkAsSeenClicked,
|
||||
|
@ -508,7 +493,7 @@ fun AnimeScreenLargeImpl(
|
|||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
floatingActionButton = {
|
||||
AnimatedVisibility(
|
||||
visible = episodes.any { !it.episode.seen } && selected.isEmpty(),
|
||||
visible = episodes.any { !it.episode.seen } && episodes.none { it.selected },
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
|
@ -521,8 +506,8 @@ fun AnimeScreenLargeImpl(
|
|||
}
|
||||
Text(text = stringResource(id))
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueReading,
|
||||
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||
onClick = onContinueWatching,
|
||||
expanded = episodeListState.isScrollingUp() || episodeListState.isScrolledToEnd(),
|
||||
modifier = Modifier
|
||||
.padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()),
|
||||
|
@ -530,75 +515,82 @@ fun AnimeScreenLargeImpl(
|
|||
}
|
||||
},
|
||||
) { contentPadding ->
|
||||
Row {
|
||||
val withNavBarContentPadding = contentPadding +
|
||||
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
|
||||
AnimeInfoHeader(
|
||||
TwoPanelBox(
|
||||
startContent = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(bottom = withNavBarContentPadding.calculateBottomPadding()),
|
||||
windowWidthSizeClass = WindowWidthSizeClass.Expanded,
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
AnimeInfoBox(
|
||||
isTabletUi = true,
|
||||
appBarPadding = contentPadding.calculateTopPadding(),
|
||||
title = state.anime.title,
|
||||
author = state.anime.author,
|
||||
artist = state.anime.artist,
|
||||
description = state.anime.description,
|
||||
tagsProvider = { state.anime.genre },
|
||||
sourceName = remember { state.source.getNameForAnimeInfo() },
|
||||
isStubSource = remember { state.source is AnimeSourceManager.StubSource },
|
||||
isStubSource = remember { state.source is AnimeSourceManager.StubAnimeSource },
|
||||
coverDataProvider = { state.anime },
|
||||
favorite = state.anime.favorite,
|
||||
status = state.anime.status,
|
||||
trackingCount = state.trackingCount,
|
||||
fromSource = state.isFromSource,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
onTrackingClicked = onTrackingClicked,
|
||||
onTagClicked = onTagClicked,
|
||||
onEditCategory = onEditCategoryClicked,
|
||||
onCoverClick = onCoverClicked,
|
||||
doSearch = onSearch,
|
||||
)
|
||||
|
||||
val episodesWeight = if (windowWidthSizeClass == WindowWidthSizeClass.Medium) 1f else 2f
|
||||
AnimeActionRow(
|
||||
favorite = state.anime.favorite,
|
||||
trackingCount = state.trackingCount,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
onTrackingClicked = onTrackingClicked,
|
||||
onEditCategory = onEditCategoryClicked,
|
||||
)
|
||||
ExpandableAnimeDescription(
|
||||
defaultExpandState = true,
|
||||
description = state.anime.description,
|
||||
tagsProvider = { state.anime.genre },
|
||||
onTagClicked = onTagClicked,
|
||||
)
|
||||
}
|
||||
},
|
||||
endContent = {
|
||||
VerticalFastScroller(
|
||||
listState = episodeListState,
|
||||
modifier = Modifier.weight(episodesWeight),
|
||||
topContentPadding = withNavBarContentPadding.calculateTopPadding(),
|
||||
endContentPadding = withNavBarContentPadding.calculateEndPadding(layoutDirection),
|
||||
topContentPadding = contentPadding.calculateTopPadding(),
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
state = episodeListState,
|
||||
contentPadding = withNavBarContentPadding,
|
||||
contentPadding = PaddingValues(
|
||||
top = contentPadding.calculateTopPadding(),
|
||||
bottom = contentPadding.calculateBottomPadding(),
|
||||
),
|
||||
) {
|
||||
item(
|
||||
key = MangaScreenItem.CHAPTER_HEADER,
|
||||
contentType = MangaScreenItem.CHAPTER_HEADER,
|
||||
) {
|
||||
item(contentType = "header") {
|
||||
EpisodeHeader(
|
||||
episodeCount = episodes.size,
|
||||
isEpisodeFiltered = state.anime.episodesFiltered(),
|
||||
onFilterButtonClicked = onFilterButtonClicked,
|
||||
onClick = onFilterButtonClicked,
|
||||
)
|
||||
}
|
||||
|
||||
sharedEpisodeItems(
|
||||
episodes = episodes,
|
||||
state = state,
|
||||
selected = selected,
|
||||
selectedPositions = selectedPositions,
|
||||
onEpisodeClicked = onEpisodeClicked,
|
||||
onDownloadEpisode = onDownloadEpisode,
|
||||
onEpisodeSelected = onEpisodeSelected,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SharedAnimeBottomActionMenu(
|
||||
selected: SnapshotStateList<EpisodeItem>,
|
||||
selected: List<EpisodeItem>,
|
||||
modifier: Modifier = Modifier,
|
||||
onEpisodeClicked: (Episode, Boolean) -> Unit,
|
||||
onMultiBookmarkClicked: (List<Episode>, bookmarked: Boolean) -> Unit,
|
||||
onMultiMarkAsSeenClicked: (List<Episode>, markAsSeen: Boolean) -> Unit,
|
||||
|
@ -607,225 +599,97 @@ private fun SharedAnimeBottomActionMenu(
|
|||
onMultiDeleteClicked: (List<Episode>) -> Unit,
|
||||
fillFraction: Float,
|
||||
) {
|
||||
val preferences: PreferencesHelper = Injekt.get()
|
||||
val preferences: PlayerPreferences = Injekt.get()
|
||||
AnimeBottomActionMenu(
|
||||
visible = selected.isNotEmpty(),
|
||||
modifier = Modifier.fillMaxWidth(fillFraction),
|
||||
modifier = modifier.fillMaxWidth(fillFraction),
|
||||
onBookmarkClicked = {
|
||||
onMultiBookmarkClicked.invoke(selected.map { it.episode }, true)
|
||||
selected.clear()
|
||||
}.takeIf { selected.any { !it.episode.bookmark } },
|
||||
onRemoveBookmarkClicked = {
|
||||
onMultiBookmarkClicked.invoke(selected.map { it.episode }, false)
|
||||
selected.clear()
|
||||
}.takeIf { selected.all { it.episode.bookmark } },
|
||||
onMarkAsSeenClicked = {
|
||||
onMultiMarkAsSeenClicked(selected.map { it.episode }, true)
|
||||
selected.clear()
|
||||
}.takeIf { selected.any { !it.episode.seen } },
|
||||
onMarkAsUnseenClicked = {
|
||||
onMultiMarkAsSeenClicked(selected.map { it.episode }, false)
|
||||
selected.clear()
|
||||
}.takeIf { selected.any { it.episode.seen } },
|
||||
}.takeIf { selected.any { it.episode.seen || it.episode.lastSecondSeen > 0L } },
|
||||
onMarkPreviousAsSeenClicked = {
|
||||
onMarkPreviousAsSeenClicked(selected[0].episode)
|
||||
selected.clear()
|
||||
}.takeIf { selected.size == 1 },
|
||||
onDownloadClicked = {
|
||||
onDownloadEpisode!!(selected.toList(), EpisodeDownloadAction.START)
|
||||
selected.clear()
|
||||
}.takeIf {
|
||||
onDownloadEpisode != null && selected.any { it.downloadState != AnimeDownload.State.DOWNLOADED }
|
||||
},
|
||||
onDeleteClicked = {
|
||||
onMultiDeleteClicked(selected.map { it.episode })
|
||||
selected.clear()
|
||||
}.takeIf {
|
||||
onDownloadEpisode != null && selected.any { it.downloadState == AnimeDownload.State.DOWNLOADED }
|
||||
},
|
||||
onExternalClicked = {
|
||||
onEpisodeClicked(selected.map { it.episode }.first(), true)
|
||||
selected.clear()
|
||||
}.takeIf { !preferences.alwaysUseExternalPlayer() && selected.size == 1 },
|
||||
}.takeIf { !preferences.alwaysUseExternalPlayer().get() && selected.size == 1 },
|
||||
onInternalClicked = {
|
||||
onEpisodeClicked(selected.map { it.episode }.first(), true)
|
||||
selected.clear()
|
||||
}.takeIf { preferences.alwaysUseExternalPlayer() && selected.size == 1 },
|
||||
}.takeIf { preferences.alwaysUseExternalPlayer().get() && selected.size == 1 },
|
||||
)
|
||||
}
|
||||
|
||||
private fun LazyListScope.sharedEpisodeItems(
|
||||
episodes: List<EpisodeItem>,
|
||||
state: AnimeScreenState.Success,
|
||||
selected: SnapshotStateList<EpisodeItem>,
|
||||
selectedPositions: Array<Int>,
|
||||
onEpisodeClicked: (Episode, Boolean) -> Unit,
|
||||
onDownloadEpisode: ((List<EpisodeItem>, EpisodeDownloadAction) -> Unit)?,
|
||||
onEpisodeSelected: (EpisodeItem, Boolean, Boolean, Boolean) -> Unit,
|
||||
) {
|
||||
items(items = episodes) { episodeItem ->
|
||||
val context = LocalContext.current
|
||||
items(
|
||||
items = episodes,
|
||||
key = { "episode-${it.episode.id}" },
|
||||
contentType = { MangaScreenItem.CHAPTER },
|
||||
) { episodeItem ->
|
||||
val haptic = LocalHapticFeedback.current
|
||||
|
||||
val (episode, downloadState, downloadProgress) = episodeItem
|
||||
val episodeTitle = if (state.anime.displayMode == EPISODE_DISPLAY_NUMBER) {
|
||||
stringResource(
|
||||
id = R.string.display_mode_episode,
|
||||
episodeDecimalFormat.format(episode.episodeNumber.toDouble()),
|
||||
)
|
||||
} else {
|
||||
episode.name
|
||||
}
|
||||
val date = remember(episode.dateUpload) {
|
||||
episode.dateUpload
|
||||
.takeIf { it > 0 }
|
||||
?.let {
|
||||
Date(it).toRelativeString(
|
||||
context,
|
||||
state.dateRelativeTime,
|
||||
state.dateFormat,
|
||||
)
|
||||
}
|
||||
}
|
||||
val lastSecondSeen = remember(episode.lastSecondSeen, episode.seen) {
|
||||
episode.lastSecondSeen.takeIf { !episode.seen && it > 0 }
|
||||
}
|
||||
val totalSeconds = remember(episode.totalSeconds) {
|
||||
episode.totalSeconds.takeIf { !episode.seen && it > 0 }
|
||||
}
|
||||
val scanlator = remember(episode.scanlator) { episode.scanlator.takeIf { !it.isNullOrBlank() } }
|
||||
|
||||
AnimeEpisodeListItem(
|
||||
title = episodeTitle,
|
||||
date = date,
|
||||
watchProgress = lastSecondSeen?.let {
|
||||
if (totalSeconds != null) {
|
||||
stringResource(
|
||||
id = R.string.episode_progress,
|
||||
formatProgress(lastSecondSeen),
|
||||
formatProgress(totalSeconds),
|
||||
)
|
||||
} else {
|
||||
stringResource(
|
||||
id = R.string.episode_progress_no_total,
|
||||
formatProgress(lastSecondSeen),
|
||||
)
|
||||
}
|
||||
},
|
||||
scanlator = scanlator,
|
||||
seen = episode.seen,
|
||||
bookmark = episode.bookmark,
|
||||
selected = selected.contains(episodeItem),
|
||||
downloadState = downloadState,
|
||||
downloadProgress = downloadProgress,
|
||||
title = episodeItem.episodeTitleString,
|
||||
date = episodeItem.dateUploadString,
|
||||
watchProgress = episodeItem.seenProgressString,
|
||||
scanlator = episodeItem.episode.scanlator.takeIf { !it.isNullOrBlank() },
|
||||
seen = episodeItem.episode.seen,
|
||||
bookmark = episodeItem.episode.bookmark,
|
||||
selected = episodeItem.selected,
|
||||
downloadStateProvider = { episodeItem.downloadState },
|
||||
downloadProgressProvider = { episodeItem.downloadProgress },
|
||||
onLongClick = {
|
||||
val dispatched = onEpisodeItemLongClick(
|
||||
episodeItem = episodeItem,
|
||||
selected = selected,
|
||||
episodes = episodes,
|
||||
selectedPositions = selectedPositions,
|
||||
)
|
||||
if (dispatched) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onEpisodeSelected(episodeItem, !episodeItem.selected, true, true)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
onClick = {
|
||||
onEpisodeItemClick(
|
||||
episodeItem = episodeItem,
|
||||
selected = selected,
|
||||
episodes = episodes,
|
||||
selectedPositions = selectedPositions,
|
||||
onToggleSelection = { onEpisodeSelected(episodeItem, !episodeItem.selected, true, false) },
|
||||
onEpisodeClicked = onEpisodeClicked,
|
||||
)
|
||||
},
|
||||
onDownloadClick = if (onDownloadEpisode != null) {
|
||||
{ onDownloadEpisode(listOf(episodeItem), it) }
|
||||
} else null,
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEpisodeItemLongClick(
|
||||
episodeItem: EpisodeItem,
|
||||
selected: MutableList<EpisodeItem>,
|
||||
episodes: List<EpisodeItem>,
|
||||
selectedPositions: Array<Int>,
|
||||
): Boolean {
|
||||
if (!selected.contains(episodeItem)) {
|
||||
val selectedIndex = episodes.indexOf(episodeItem)
|
||||
if (selected.isEmpty()) {
|
||||
selected.add(episodeItem)
|
||||
selectedPositions[0] = selectedIndex
|
||||
selectedPositions[1] = selectedIndex
|
||||
return true
|
||||
}
|
||||
|
||||
// Try to select the items in-between when possible
|
||||
val range: IntRange
|
||||
if (selectedIndex < selectedPositions[0]) {
|
||||
range = selectedIndex until selectedPositions[0]
|
||||
selectedPositions[0] = selectedIndex
|
||||
} else if (selectedIndex > selectedPositions[1]) {
|
||||
range = (selectedPositions[1] + 1)..selectedIndex
|
||||
selectedPositions[1] = selectedIndex
|
||||
} else {
|
||||
// Just select itself
|
||||
range = selectedIndex..selectedIndex
|
||||
}
|
||||
|
||||
range.forEach {
|
||||
val toAdd = episodes[it]
|
||||
if (!selected.contains(toAdd)) {
|
||||
selected.add(toAdd)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun onEpisodeItemClick(
|
||||
episodeItem: EpisodeItem,
|
||||
selected: MutableList<EpisodeItem>,
|
||||
episodes: List<EpisodeItem>,
|
||||
selectedPositions: Array<Int>,
|
||||
onToggleSelection: (Boolean) -> Unit,
|
||||
onEpisodeClicked: (Episode, Boolean) -> Unit,
|
||||
) {
|
||||
val selectedIndex = episodes.indexOf(episodeItem)
|
||||
when {
|
||||
selected.contains(episodeItem) -> {
|
||||
val removedIndex = episodes.indexOf(episodeItem)
|
||||
selected.remove(episodeItem)
|
||||
|
||||
if (removedIndex == selectedPositions[0]) {
|
||||
selectedPositions[0] = episodes.indexOfFirst { selected.contains(it) }
|
||||
} else if (removedIndex == selectedPositions[1]) {
|
||||
selectedPositions[1] = episodes.indexOfLast { selected.contains(it) }
|
||||
}
|
||||
}
|
||||
selected.isNotEmpty() -> {
|
||||
if (selectedIndex < selectedPositions[0]) {
|
||||
selectedPositions[0] = selectedIndex
|
||||
} else if (selectedIndex > selectedPositions[1]) {
|
||||
selectedPositions[1] = selectedIndex
|
||||
}
|
||||
selected.add(episodeItem)
|
||||
}
|
||||
episodeItem.selected -> onToggleSelection(false)
|
||||
episodes.any { it.selected } -> onToggleSelection(true)
|
||||
else -> onEpisodeClicked(episodeItem.episode, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatProgress(milliseconds: Long): String {
|
||||
return if (milliseconds > 3600000L) String.format(
|
||||
"%d:%02d:%02d",
|
||||
TimeUnit.MILLISECONDS.toHours(milliseconds),
|
||||
TimeUnit.MILLISECONDS.toMinutes(milliseconds) -
|
||||
TimeUnit.HOURS.toMinutes(TimeUnit.MILLISECONDS.toHours(milliseconds)),
|
||||
TimeUnit.MILLISECONDS.toSeconds(milliseconds) -
|
||||
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(milliseconds)),
|
||||
) else {
|
||||
String.format(
|
||||
"%d:%02d",
|
||||
TimeUnit.MILLISECONDS.toMinutes(milliseconds),
|
||||
TimeUnit.MILLISECONDS.toSeconds(milliseconds) -
|
||||
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(milliseconds)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material.icons.outlined.Save
|
||||
import androidx.compose.material.icons.outlined.Share
|
||||
|
@ -24,11 +24,14 @@ import androidx.compose.material3.IconButton
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.view.updatePadding
|
||||
|
@ -63,7 +66,7 @@ fun AnimeCoverDialog(
|
|||
) {
|
||||
IconButton(onClick = onDismissRequest) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
imageVector = Icons.Outlined.Close,
|
||||
contentDescription = stringResource(R.string.action_close),
|
||||
)
|
||||
}
|
||||
|
@ -82,9 +85,15 @@ fun AnimeCoverDialog(
|
|||
}
|
||||
if (onEditClick != null) {
|
||||
Box {
|
||||
val (expanded, onExpand) = remember { mutableStateOf(false) }
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
IconButton(
|
||||
onClick = { if (isCustomCover) onExpand(true) else onEditClick(EditCoverAction.EDIT) },
|
||||
onClick = {
|
||||
if (isCustomCover) {
|
||||
expanded = true
|
||||
} else {
|
||||
onEditClick(EditCoverAction.EDIT)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Edit,
|
||||
|
@ -93,20 +102,21 @@ fun AnimeCoverDialog(
|
|||
}
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { onExpand(false) },
|
||||
onDismissRequest = { expanded = false },
|
||||
offset = DpOffset(8.dp, 0.dp),
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(R.string.action_edit)) },
|
||||
onClick = {
|
||||
onEditClick(EditCoverAction.EDIT)
|
||||
onExpand(false)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(R.string.action_delete)) },
|
||||
onClick = {
|
||||
onEditClick(EditCoverAction.DELETE)
|
||||
onExpand(false)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package eu.kanade.presentation.anime.components
|
||||
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
@Composable
|
||||
fun DeleteEpisodesDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onConfirm: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
onConfirm()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(R.string.are_you_sure))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(R.string.confirm_delete_episodes))
|
||||
},
|
||||
)
|
||||
}
|
|
@ -29,11 +29,12 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import eu.kanade.presentation.components.EpisodeDownloadAction
|
||||
import eu.kanade.presentation.components.EpisodeDownloadIndicator
|
||||
import eu.kanade.presentation.manga.EpisodeDownloadAction
|
||||
import eu.kanade.presentation.manga.components.DotSeparatorText
|
||||
import eu.kanade.presentation.util.ReadItemAlpha
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.AnimeDownload
|
||||
import eu.kanade.tachiyomi.data.animedownload.model.AnimeDownload
|
||||
|
||||
@Composable
|
||||
fun AnimeEpisodeListItem(
|
||||
|
@ -45,8 +46,8 @@ fun AnimeEpisodeListItem(
|
|||
seen: Boolean,
|
||||
bookmark: Boolean,
|
||||
selected: Boolean,
|
||||
downloadState: AnimeDownload.State,
|
||||
downloadProgress: Int,
|
||||
downloadStateProvider: () -> AnimeDownload.State,
|
||||
downloadProgressProvider: () -> Int,
|
||||
onLongClick: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
onDownloadClick: ((EpisodeDownloadAction) -> Unit)?,
|
||||
|
@ -66,13 +67,13 @@ fun AnimeEpisodeListItem(
|
|||
} else {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
val textAlpha = remember(seen) { if (seen) SeenItemAlpha else 1f }
|
||||
val textAlpha = remember(seen) { if (seen) ReadItemAlpha else 1f }
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
var textHeight by remember { mutableStateOf(0) }
|
||||
if (bookmark) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bookmark,
|
||||
imageVector = Icons.Filled.Bookmark,
|
||||
contentDescription = stringResource(R.string.action_filter_bookmarked),
|
||||
modifier = Modifier
|
||||
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
|
||||
|
@ -82,8 +83,8 @@ fun AnimeEpisodeListItem(
|
|||
}
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
.copy(color = textColor),
|
||||
color = textColor,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
onTextLayout = { textHeight = it.size.height },
|
||||
|
@ -109,7 +110,7 @@ fun AnimeEpisodeListItem(
|
|||
text = watchProgress,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.alpha(SeenItemAlpha),
|
||||
modifier = Modifier.alpha(ReadItemAlpha),
|
||||
)
|
||||
if (scanlator != null) DotSeparatorText()
|
||||
}
|
||||
|
@ -128,12 +129,10 @@ fun AnimeEpisodeListItem(
|
|||
if (onDownloadClick != null) {
|
||||
EpisodeDownloadIndicator(
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
downloadState = downloadState,
|
||||
downloadProgress = downloadProgress,
|
||||
downloadStateProvider = downloadStateProvider,
|
||||
downloadProgressProvider = downloadProgressProvider,
|
||||
onClick = onDownloadClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val SeenItemAlpha = .38f
|
||||
|
|
|
@ -21,19 +21,20 @@ import androidx.compose.foundation.layout.size
|
|||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AttachMoney
|
||||
import androidx.compose.material.icons.filled.Block
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Done
|
||||
import androidx.compose.material.icons.filled.DoneAll
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
import androidx.compose.material.icons.filled.Public
|
||||
import androidx.compose.material.icons.filled.Schedule
|
||||
import androidx.compose.material.icons.filled.Sync
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material.icons.outlined.AttachMoney
|
||||
import androidx.compose.material.icons.outlined.Block
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.Done
|
||||
import androidx.compose.material.icons.outlined.DoneAll
|
||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||
import androidx.compose.material.icons.outlined.Pause
|
||||
import androidx.compose.material.icons.outlined.Public
|
||||
import androidx.compose.material.icons.outlined.Schedule
|
||||
import androidx.compose.material.icons.outlined.Sync
|
||||
import androidx.compose.material.icons.outlined.Warning
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
|
||||
|
@ -42,7 +43,6 @@ import androidx.compose.material3.ProvideTextStyle
|
|||
import androidx.compose.material3.SuggestionChip
|
||||
import androidx.compose.material3.SuggestionChipDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
|
@ -62,6 +62,7 @@ import androidx.compose.ui.layout.ContentScale
|
|||
import androidx.compose.ui.layout.SubcomposeLayout
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
@ -76,7 +77,6 @@ import eu.kanade.presentation.components.MangaCover
|
|||
import eu.kanade.presentation.components.TextButton
|
||||
import eu.kanade.presentation.manga.components.DotSeparatorText
|
||||
import eu.kanade.presentation.util.clickableNoIndication
|
||||
import eu.kanade.presentation.util.quantityStringResource
|
||||
import eu.kanade.presentation.util.secondaryItemAlpha
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
|
@ -86,33 +86,21 @@ import kotlin.math.roundToInt
|
|||
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
||||
|
||||
@Composable
|
||||
fun AnimeInfoHeader(
|
||||
fun AnimeInfoBox(
|
||||
modifier: Modifier = Modifier,
|
||||
windowWidthSizeClass: WindowWidthSizeClass,
|
||||
isTabletUi: Boolean,
|
||||
appBarPadding: Dp,
|
||||
title: String,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
description: String?,
|
||||
tagsProvider: () -> List<String>?,
|
||||
sourceName: String,
|
||||
isStubSource: Boolean,
|
||||
coverDataProvider: () -> Anime,
|
||||
favorite: Boolean,
|
||||
status: Long,
|
||||
trackingCount: Int,
|
||||
fromSource: Boolean,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
onTrackingClicked: (() -> Unit)?,
|
||||
onTagClicked: (String) -> Unit,
|
||||
onEditCategory: (() -> Unit)?,
|
||||
onCoverClick: () -> Unit,
|
||||
doSearch: (query: String, global: Boolean) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Column(modifier = modifier) {
|
||||
Box {
|
||||
Box(modifier = modifier) {
|
||||
// Backdrop
|
||||
val backdropGradientColors = listOf(
|
||||
Color.Transparent,
|
||||
|
@ -133,15 +121,15 @@ fun AnimeInfoHeader(
|
|||
.alpha(.2f),
|
||||
)
|
||||
|
||||
// Anime & source info
|
||||
// Manga & source info
|
||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
|
||||
if (windowWidthSizeClass == WindowWidthSizeClass.Compact) {
|
||||
if (!isTabletUi) {
|
||||
AnimeAndSourceTitlesSmall(
|
||||
appBarPadding = appBarPadding,
|
||||
coverDataProvider = coverDataProvider,
|
||||
onCoverClick = onCoverClick,
|
||||
title = title,
|
||||
context = context,
|
||||
context = LocalContext.current,
|
||||
doSearch = doSearch,
|
||||
author = author,
|
||||
artist = artist,
|
||||
|
@ -155,7 +143,7 @@ fun AnimeInfoHeader(
|
|||
coverDataProvider = coverDataProvider,
|
||||
onCoverClick = onCoverClick,
|
||||
title = title,
|
||||
context = context,
|
||||
context = LocalContext.current,
|
||||
doSearch = doSearch,
|
||||
author = author,
|
||||
artist = artist,
|
||||
|
@ -166,9 +154,19 @@ fun AnimeInfoHeader(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
Row(modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
||||
@Composable
|
||||
fun AnimeActionRow(
|
||||
modifier: Modifier = Modifier,
|
||||
favorite: Boolean,
|
||||
trackingCount: Int,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
onTrackingClicked: (() -> Unit)?,
|
||||
onEditCategory: (() -> Unit)?,
|
||||
) {
|
||||
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||
AnimeActionButton(
|
||||
title = if (favorite) {
|
||||
|
@ -176,7 +174,7 @@ fun AnimeInfoHeader(
|
|||
} else {
|
||||
stringResource(R.string.add_to_library)
|
||||
},
|
||||
icon = if (favorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
|
||||
icon = if (favorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
|
||||
color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||
onClick = onAddToLibraryClicked,
|
||||
onLongClick = onEditCategory,
|
||||
|
@ -186,9 +184,9 @@ fun AnimeInfoHeader(
|
|||
title = if (trackingCount == 0) {
|
||||
stringResource(R.string.manga_tracking_tab)
|
||||
} else {
|
||||
quantityStringResource(id = R.plurals.num_trackers, quantity = trackingCount, trackingCount)
|
||||
pluralStringResource(id = R.plurals.num_trackers, count = trackingCount, trackingCount)
|
||||
},
|
||||
icon = if (trackingCount == 0) Icons.Default.Sync else Icons.Default.Done,
|
||||
icon = if (trackingCount == 0) Icons.Outlined.Sync else Icons.Outlined.Done,
|
||||
color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary,
|
||||
onClick = onTrackingClicked,
|
||||
)
|
||||
|
@ -196,20 +194,28 @@ fun AnimeInfoHeader(
|
|||
if (onWebViewClicked != null) {
|
||||
AnimeActionButton(
|
||||
title = stringResource(R.string.action_web_view),
|
||||
icon = Icons.Default.Public,
|
||||
icon = Icons.Outlined.Public,
|
||||
color = defaultActionButtonColor,
|
||||
onClick = onWebViewClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expandable description-tags
|
||||
Column {
|
||||
@Composable
|
||||
fun ExpandableAnimeDescription(
|
||||
modifier: Modifier = Modifier,
|
||||
defaultExpandState: Boolean,
|
||||
description: String?,
|
||||
tagsProvider: () -> List<String>?,
|
||||
onTagClicked: (String) -> Unit,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
val (expanded, onExpanded) = rememberSaveable {
|
||||
mutableStateOf(fromSource || windowWidthSizeClass != WindowWidthSizeClass.Compact)
|
||||
mutableStateOf(defaultExpandState)
|
||||
}
|
||||
val desc =
|
||||
description.takeIf { !it.isNullOrBlank() } ?: stringResource(id = R.string.description_placeholder)
|
||||
description.takeIf { !it.isNullOrBlank() } ?: stringResource(R.string.description_placeholder)
|
||||
val trimmedDescription = remember(desc) {
|
||||
desc
|
||||
.replace(whitespaceLineRegex, "\n")
|
||||
|
@ -222,10 +228,7 @@ fun AnimeInfoHeader(
|
|||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.clickableNoIndication(
|
||||
onLongClick = { context.copyToClipboard(desc, desc) },
|
||||
onClick = { onExpanded(!expanded) },
|
||||
),
|
||||
.clickableNoIndication { onExpanded(!expanded) },
|
||||
)
|
||||
val tags = tagsProvider()
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
|
@ -265,7 +268,6 @@ fun AnimeInfoHeader(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimeAndSourceTitlesLarge(
|
||||
|
@ -288,13 +290,14 @@ private fun AnimeAndSourceTitlesLarge(
|
|||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
MangaCover.Book(
|
||||
modifier = Modifier.fillMaxWidth(0.4f),
|
||||
modifier = Modifier.fillMaxWidth(0.65f),
|
||||
data = coverDataProvider(),
|
||||
contentDescription = stringResource(R.string.manga_cover),
|
||||
onClick = onCoverClick,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = title.takeIf { it.isNotBlank() } ?: stringResource(R.string.unknown),
|
||||
text = title.ifBlank { stringResource(R.string.unknown_title) },
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.clickableNoIndication(
|
||||
onLongClick = { if (title.isNotBlank()) context.copyToClipboard(title, title) },
|
||||
|
@ -311,10 +314,12 @@ private fun AnimeAndSourceTitlesLarge(
|
|||
.padding(top = 2.dp)
|
||||
.clickableNoIndication(
|
||||
onLongClick = {
|
||||
if (!author.isNullOrBlank()) context.copyToClipboard(
|
||||
if (!author.isNullOrBlank()) {
|
||||
context.copyToClipboard(
|
||||
author,
|
||||
author,
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
|
||||
),
|
||||
|
@ -341,13 +346,13 @@ private fun AnimeAndSourceTitlesLarge(
|
|||
) {
|
||||
Icon(
|
||||
imageVector = when (status) {
|
||||
SAnime.ONGOING.toLong() -> Icons.Default.Schedule
|
||||
SAnime.COMPLETED.toLong() -> Icons.Default.DoneAll
|
||||
SAnime.LICENSED.toLong() -> Icons.Default.AttachMoney
|
||||
SAnime.PUBLISHING_FINISHED.toLong() -> Icons.Default.Done
|
||||
SAnime.CANCELLED.toLong() -> Icons.Default.Close
|
||||
SAnime.ON_HIATUS.toLong() -> Icons.Default.Pause
|
||||
else -> Icons.Default.Block
|
||||
SAnime.ONGOING.toLong() -> Icons.Outlined.Schedule
|
||||
SAnime.COMPLETED.toLong() -> Icons.Outlined.DoneAll
|
||||
SAnime.LICENSED.toLong() -> Icons.Outlined.AttachMoney
|
||||
SAnime.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
|
||||
SAnime.CANCELLED.toLong() -> Icons.Outlined.Close
|
||||
SAnime.ON_HIATUS.toLong() -> Icons.Outlined.Pause
|
||||
else -> Icons.Outlined.Block
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
|
@ -371,7 +376,7 @@ private fun AnimeAndSourceTitlesLarge(
|
|||
DotSeparatorText()
|
||||
if (isStubSource) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Warning,
|
||||
imageVector = Icons.Outlined.Warning,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
|
@ -415,18 +420,21 @@ private fun AnimeAndSourceTitlesSmall(
|
|||
.sizeIn(maxWidth = 100.dp)
|
||||
.align(Alignment.Top),
|
||||
data = coverDataProvider(),
|
||||
contentDescription = stringResource(R.string.manga_cover),
|
||||
onClick = onCoverClick,
|
||||
)
|
||||
Column(modifier = Modifier.padding(start = 16.dp)) {
|
||||
Text(
|
||||
text = title.ifBlank { stringResource(R.string.unknown) },
|
||||
text = title.ifBlank { stringResource(R.string.unknown_title) },
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.clickableNoIndication(
|
||||
onLongClick = {
|
||||
if (title.isNotBlank()) context.copyToClipboard(
|
||||
if (title.isNotBlank()) {
|
||||
context.copyToClipboard(
|
||||
title,
|
||||
title,
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = { if (title.isNotBlank()) doSearch(title, true) },
|
||||
),
|
||||
|
@ -441,10 +449,12 @@ private fun AnimeAndSourceTitlesSmall(
|
|||
.padding(top = 2.dp)
|
||||
.clickableNoIndication(
|
||||
onLongClick = {
|
||||
if (!author.isNullOrBlank()) context.copyToClipboard(
|
||||
if (!author.isNullOrBlank()) {
|
||||
context.copyToClipboard(
|
||||
author,
|
||||
author,
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
|
||||
),
|
||||
|
@ -469,13 +479,13 @@ private fun AnimeAndSourceTitlesSmall(
|
|||
) {
|
||||
Icon(
|
||||
imageVector = when (status) {
|
||||
SAnime.ONGOING.toLong() -> Icons.Default.Schedule
|
||||
SAnime.COMPLETED.toLong() -> Icons.Default.DoneAll
|
||||
SAnime.LICENSED.toLong() -> Icons.Default.AttachMoney
|
||||
SAnime.PUBLISHING_FINISHED.toLong() -> Icons.Default.Done
|
||||
SAnime.CANCELLED.toLong() -> Icons.Default.Close
|
||||
SAnime.ON_HIATUS.toLong() -> Icons.Default.Pause
|
||||
else -> Icons.Default.Block
|
||||
SAnime.ONGOING.toLong() -> Icons.Outlined.Schedule
|
||||
SAnime.COMPLETED.toLong() -> Icons.Outlined.DoneAll
|
||||
SAnime.LICENSED.toLong() -> Icons.Outlined.AttachMoney
|
||||
SAnime.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
|
||||
SAnime.CANCELLED.toLong() -> Icons.Outlined.Close
|
||||
SAnime.ON_HIATUS.toLong() -> Icons.Outlined.Pause
|
||||
else -> Icons.Outlined.Block
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
|
@ -499,7 +509,7 @@ private fun AnimeAndSourceTitlesSmall(
|
|||
DotSeparatorText()
|
||||
if (isStubSource) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Warning,
|
||||
imageVector = Icons.Outlined.Warning,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
|
@ -555,6 +565,7 @@ private fun AnimeSummary(
|
|||
expandedHeight = expandedPlaceable.maxByOrNull { it.height }?.height?.coerceAtLeast(shrunkHeight) ?: 0
|
||||
|
||||
val actualPlaceable = subcompose("description") {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = if (expanded) expandedDescription else shrunkDescription,
|
||||
maxLines = Int.MAX_VALUE,
|
||||
|
@ -562,6 +573,7 @@ private fun AnimeSummary(
|
|||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
)
|
||||
}
|
||||
}.map { it.measure(constraints) }
|
||||
|
||||
val scrimPlaceable = subcompose("scrim") {
|
||||
|
@ -573,7 +585,7 @@ private fun AnimeSummary(
|
|||
val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down)
|
||||
Icon(
|
||||
painter = rememberAnimatedVectorPainter(image, !expanded),
|
||||
contentDescription = null,
|
||||
contentDescription = stringResource(if (expanded) R.string.manga_info_collapse else R.string.manga_info_expand),
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.background(Brush.radialGradient(colors = colors.asReversed())),
|
||||
)
|
||||
|
|
|
@ -1,245 +0,0 @@
|
|||
package eu.kanade.presentation.anime.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.FlipToBack
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.SelectAll
|
||||
import androidx.compose.material.icons.outlined.Download
|
||||
import androidx.compose.material.icons.outlined.Share
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SmallTopAppBar
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.components.DropdownMenu
|
||||
import eu.kanade.presentation.manga.DownloadAction
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
@Composable
|
||||
fun AnimeSmallAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
titleAlphaProvider: () -> Float,
|
||||
backgroundAlphaProvider: () -> Float = titleAlphaProvider,
|
||||
incognitoMode: Boolean,
|
||||
downloadedOnlyMode: Boolean,
|
||||
onBackClicked: () -> Unit,
|
||||
onShareClicked: (() -> Unit)?,
|
||||
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
changeAnimeSkipIntro: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
// For action mode
|
||||
actionModeCounter: Int,
|
||||
onSelectAll: () -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
) {
|
||||
val isActionMode = actionModeCounter > 0
|
||||
val backgroundAlpha = if (isActionMode) 1f else backgroundAlphaProvider()
|
||||
val backgroundColor by TopAppBarDefaults.centerAlignedTopAppBarColors().containerColor(1f)
|
||||
Column(
|
||||
modifier = modifier.drawBehind {
|
||||
drawRect(backgroundColor.copy(alpha = backgroundAlpha))
|
||||
},
|
||||
) {
|
||||
SmallTopAppBar(
|
||||
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top)),
|
||||
title = {
|
||||
Text(
|
||||
text = if (isActionMode) actionModeCounter.toString() else title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.alpha(titleAlphaProvider()),
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClicked) {
|
||||
Icon(
|
||||
imageVector = if (isActionMode) Icons.Default.Close else Icons.Default.ArrowBack,
|
||||
contentDescription = stringResource(R.string.abc_action_bar_up_description),
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (isActionMode) {
|
||||
IconButton(onClick = onSelectAll) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SelectAll,
|
||||
contentDescription = stringResource(R.string.action_select_all),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onInvertSelection) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FlipToBack,
|
||||
contentDescription = stringResource(R.string.action_select_inverse),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (onShareClicked != null) {
|
||||
IconButton(onClick = onShareClicked) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Share,
|
||||
contentDescription = stringResource(R.string.action_share),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (onDownloadClicked != null) {
|
||||
val (downloadExpanded, onDownloadExpanded) = remember { mutableStateOf(false) }
|
||||
Box {
|
||||
IconButton(onClick = { onDownloadExpanded(!downloadExpanded) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Download,
|
||||
contentDescription = stringResource(R.string.manga_download),
|
||||
)
|
||||
}
|
||||
val onDismissRequest = { onDownloadExpanded(false) }
|
||||
DropdownMenu(
|
||||
expanded = downloadExpanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(R.string.download_1_episode)) },
|
||||
onClick = {
|
||||
onDownloadClicked(DownloadAction.NEXT_1_CHAPTER)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(R.string.download_5_episodes)) },
|
||||
onClick = {
|
||||
onDownloadClicked(DownloadAction.NEXT_5_CHAPTERS)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(R.string.download_10_episodes)) },
|
||||
onClick = {
|
||||
onDownloadClicked(DownloadAction.NEXT_10_CHAPTERS)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(R.string.download_custom)) },
|
||||
onClick = {
|
||||
onDownloadClicked(DownloadAction.CUSTOM)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(R.string.download_unseen)) },
|
||||
onClick = {
|
||||
onDownloadClicked(DownloadAction.UNREAD_CHAPTERS)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(R.string.download_all)) },
|
||||
onClick = {
|
||||
onDownloadClicked(DownloadAction.ALL_CHAPTERS)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (onEditCategoryClicked != null && onMigrateClicked != null) {
|
||||
val (moreExpanded, onMoreExpanded) = remember { mutableStateOf(false) }
|
||||
Box {
|
||||
IconButton(onClick = { onMoreExpanded(!moreExpanded) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = stringResource(R.string.abc_action_menu_overflow_description),
|
||||
)
|
||||
}
|
||||
val onDismissRequest = { onMoreExpanded(false) }
|
||||
DropdownMenu(
|
||||
expanded = moreExpanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(R.string.action_edit_anime_categories)) },
|
||||
onClick = {
|
||||
onEditCategoryClicked()
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(R.string.action_migrate)) },
|
||||
onClick = {
|
||||
onMigrateClicked()
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(R.string.action_change_intro_length)) },
|
||||
onClick = {
|
||||
changeAnimeSkipIntro?.invoke()
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// Background handled by parent
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = Color.Transparent,
|
||||
scrolledContainerColor = Color.Transparent,
|
||||
),
|
||||
)
|
||||
|
||||
if (downloadedOnlyMode) {
|
||||
Text(
|
||||
text = stringResource(R.string.label_downloaded_only),
|
||||
modifier = Modifier
|
||||
.background(color = MaterialTheme.colorScheme.tertiary)
|
||||
.fillMaxWidth()
|
||||
.padding(4.dp),
|
||||
color = MaterialTheme.colorScheme.onTertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
}
|
||||
if (incognitoMode) {
|
||||
Text(
|
||||
text = stringResource(R.string.pref_incognito_mode),
|
||||
modifier = Modifier
|
||||
.background(color = MaterialTheme.colorScheme.primary)
|
||||
.fillMaxWidth()
|
||||
.padding(4.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
package eu.kanade.presentation.anime.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import eu.kanade.domain.anime.model.Anime
|
||||
import eu.kanade.presentation.manga.DownloadAction
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun AnimeTopAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
description: String?,
|
||||
tagsProvider: () -> List<String>?,
|
||||
coverDataProvider: () -> Anime,
|
||||
sourceName: String,
|
||||
isStubSource: Boolean,
|
||||
favorite: Boolean,
|
||||
status: Long,
|
||||
trackingCount: Int,
|
||||
episodeCount: Int?,
|
||||
episodeFiltered: Boolean,
|
||||
incognitoMode: Boolean,
|
||||
downloadedOnlyMode: Boolean,
|
||||
fromSource: Boolean,
|
||||
onBackClicked: () -> Unit,
|
||||
onCoverClick: () -> Unit,
|
||||
onTagClicked: (String) -> Unit,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
onTrackingClicked: (() -> Unit)?,
|
||||
onFilterButtonClicked: () -> Unit,
|
||||
onShareClicked: (() -> Unit)?,
|
||||
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
changeAnimeSkipIntro: (() -> Unit)?,
|
||||
doGlobalSearch: (query: String, global: Boolean) -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior?,
|
||||
// For action mode
|
||||
actionModeCounter: Int,
|
||||
onSelectAll: () -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
onSmallAppBarHeightChanged: (Int) -> Unit,
|
||||
|
||||
) {
|
||||
val scrollPercentageProvider = { scrollBehavior?.scrollFraction?.coerceIn(0f, 1f) ?: 0f }
|
||||
val inverseScrollPercentageProvider = { 1f - scrollPercentageProvider() }
|
||||
|
||||
Layout(
|
||||
modifier = modifier,
|
||||
content = {
|
||||
val (smallHeightPx, onSmallHeightPxChanged) = remember { mutableStateOf(0) }
|
||||
Column(modifier = Modifier.layoutId("animeInfo")) {
|
||||
AnimeInfoHeader(
|
||||
windowWidthSizeClass = WindowWidthSizeClass.Compact,
|
||||
appBarPadding = with(LocalDensity.current) { smallHeightPx.toDp() },
|
||||
title = title,
|
||||
author = author,
|
||||
artist = artist,
|
||||
description = description,
|
||||
tagsProvider = tagsProvider,
|
||||
sourceName = sourceName,
|
||||
isStubSource = isStubSource,
|
||||
coverDataProvider = coverDataProvider,
|
||||
favorite = favorite,
|
||||
status = status,
|
||||
trackingCount = trackingCount,
|
||||
fromSource = fromSource,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
onTrackingClicked = onTrackingClicked,
|
||||
onTagClicked = onTagClicked,
|
||||
onEditCategory = onEditCategoryClicked,
|
||||
onCoverClick = onCoverClick,
|
||||
doSearch = doGlobalSearch,
|
||||
)
|
||||
EpisodeHeader(
|
||||
episodeCount = episodeCount,
|
||||
isEpisodeFiltered = episodeFiltered,
|
||||
onFilterButtonClicked = onFilterButtonClicked,
|
||||
)
|
||||
}
|
||||
|
||||
AnimeSmallAppBar(
|
||||
modifier = Modifier
|
||||
.layoutId("topBar")
|
||||
.onSizeChanged {
|
||||
onSmallHeightPxChanged(it.height)
|
||||
onSmallAppBarHeightChanged(it.height)
|
||||
},
|
||||
title = title,
|
||||
titleAlphaProvider = { if (actionModeCounter == 0) scrollPercentageProvider() else 1f },
|
||||
incognitoMode = incognitoMode,
|
||||
downloadedOnlyMode = downloadedOnlyMode,
|
||||
onBackClicked = onBackClicked,
|
||||
onShareClicked = onShareClicked,
|
||||
onDownloadClicked = onDownloadClicked,
|
||||
onEditCategoryClicked = onEditCategoryClicked,
|
||||
onMigrateClicked = onMigrateClicked,
|
||||
changeAnimeSkipIntro = changeAnimeSkipIntro,
|
||||
actionModeCounter = actionModeCounter,
|
||||
onSelectAll = onSelectAll,
|
||||
onInvertSelection = onInvertSelection,
|
||||
|
||||
)
|
||||
},
|
||||
) { measurables, constraints ->
|
||||
val animeInfoPlaceable = measurables
|
||||
.first { it.layoutId == "animeInfo" }
|
||||
.measure(constraints.copy(maxHeight = Constraints.Infinity))
|
||||
val topBarPlaceable = measurables
|
||||
.first { it.layoutId == "topBar" }
|
||||
.measure(constraints)
|
||||
val animeInfoHeight = animeInfoPlaceable.height
|
||||
val topBarHeight = topBarPlaceable.height
|
||||
val animeInfoSansTopBarHeightPx = animeInfoHeight - topBarHeight
|
||||
val layoutHeight = topBarHeight +
|
||||
(animeInfoSansTopBarHeightPx * inverseScrollPercentageProvider()).roundToInt()
|
||||
|
||||
layout(constraints.maxWidth, layoutHeight) {
|
||||
val animeInfoY = (-animeInfoSansTopBarHeightPx * scrollPercentageProvider()).roundToInt()
|
||||
animeInfoPlaceable.place(0, animeInfoY)
|
||||
topBarPlaceable.place(0, 0)
|
||||
|
||||
// Update offset limit
|
||||
val offsetLimit = -animeInfoSansTopBarHeightPx.toFloat()
|
||||
if (scrollBehavior?.state?.offsetLimit != offsetLimit) {
|
||||
scrollBehavior?.state?.offsetLimit = offsetLimit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,32 +4,25 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.FilterList
|
||||
import androidx.compose.material3.Icon
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.util.quantityStringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
|
||||
@Composable
|
||||
fun EpisodeHeader(
|
||||
episodeCount: Int?,
|
||||
isEpisodeFiltered: Boolean,
|
||||
onFilterButtonClicked: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onFilterButtonClicked)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
|
@ -37,20 +30,11 @@ fun EpisodeHeader(
|
|||
text = if (episodeCount == null) {
|
||||
stringResource(R.string.episodes)
|
||||
} else {
|
||||
quantityStringResource(id = R.plurals.anime_num_episodes, quantity = episodeCount)
|
||||
pluralStringResource(id = R.plurals.anime_num_episodes, count = episodeCount, episodeCount)
|
||||
},
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.weight(1f),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.FilterList,
|
||||
contentDescription = stringResource(R.string.action_filter),
|
||||
tint = if (isEpisodeFiltered) {
|
||||
Color(LocalContext.current.getResourceColor(R.attr.colorFilterActive))
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onBackground
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,27 @@
|
|||
package eu.kanade.presentation.browse
|
||||
package eu.kanade.presentation.animebrowse
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import android.util.DisplayMetrics
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.outlined.History
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Divider
|
||||
|
@ -30,58 +32,116 @@ import androidx.compose.material3.OutlinedButton
|
|||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||
import eu.kanade.presentation.animebrowse.components.AnimeExtensionIcon
|
||||
import eu.kanade.presentation.browse.NsfwWarningDialog
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.components.DIVIDER_ALPHA
|
||||
import eu.kanade.presentation.components.Divider
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.presentation.components.PreferenceRow
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
||||
import eu.kanade.presentation.util.horizontalPadding
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.animeextension.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.extension.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.ui.browse.animeextension.details.AnimeExtensionDetailsPresenter
|
||||
import eu.kanade.tachiyomi.ui.browse.animeextension.details.AnimeExtensionSourceItem
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
|
||||
@Composable
|
||||
fun AnimeExtensionDetailsScreen(
|
||||
nestedScrollInterop: NestedScrollConnection,
|
||||
navigateUp: () -> Unit,
|
||||
presenter: AnimeExtensionDetailsPresenter,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickAppInfo: () -> Unit,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
onClickSource: (sourceId: Long) -> Unit,
|
||||
) {
|
||||
val extension = presenter.extension
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
if (extension == null) {
|
||||
EmptyScreen(textResource = R.string.empty_screen)
|
||||
return
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
title = stringResource(R.string.label_extension_info),
|
||||
navigateUp = navigateUp,
|
||||
actions = {
|
||||
AppBarActions(
|
||||
actions = buildList {
|
||||
if (presenter.extension?.isUnofficial == false) {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.whats_new),
|
||||
icon = Icons.Outlined.History,
|
||||
onClick = { uriHandler.openUri(presenter.getChangelogUrl()) },
|
||||
),
|
||||
)
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_faq_and_guides),
|
||||
icon = Icons.Outlined.HelpOutline,
|
||||
onClick = { uriHandler.openUri(presenter.getReadmeUrl()) },
|
||||
),
|
||||
)
|
||||
}
|
||||
addAll(
|
||||
listOf(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(R.string.action_enable_all),
|
||||
onClick = { presenter.toggleSources(true) },
|
||||
),
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(R.string.action_disable_all),
|
||||
onClick = { presenter.toggleSources(false) },
|
||||
),
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(R.string.pref_clear_cookies),
|
||||
onClick = { presenter.clearCookies() },
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
AnimeExtensionDetails(paddingValues, presenter, onClickSourcePreferences)
|
||||
}
|
||||
}
|
||||
|
||||
val sources by presenter.sourcesState.collectAsState()
|
||||
|
||||
@Composable
|
||||
private fun AnimeExtensionDetails(
|
||||
contentPadding: PaddingValues,
|
||||
presenter: AnimeExtensionDetailsPresenter,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
) {
|
||||
when {
|
||||
presenter.isLoading -> LoadingScreen()
|
||||
presenter.extension == null -> EmptyScreen(
|
||||
textResource = R.string.empty_screen,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
else -> {
|
||||
val context = LocalContext.current
|
||||
val extension = presenter.extension
|
||||
var showNsfwWarning by remember { mutableStateOf(false) }
|
||||
|
||||
ScrollbarLazyColumn(
|
||||
modifier = Modifier.nestedScroll(nestedScrollInterop),
|
||||
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
when {
|
||||
extension.isUnofficial ->
|
||||
|
@ -97,8 +157,13 @@ fun AnimeExtensionDetailsScreen(
|
|||
item {
|
||||
DetailsHeader(
|
||||
extension = extension,
|
||||
onClickUninstall = onClickUninstall,
|
||||
onClickAppInfo = onClickAppInfo,
|
||||
onClickUninstall = { presenter.uninstallExtension() },
|
||||
onClickAppInfo = {
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", extension.pkgName, null)
|
||||
context.startActivity(this)
|
||||
}
|
||||
},
|
||||
onClickAgeRating = {
|
||||
showNsfwWarning = true
|
||||
},
|
||||
|
@ -106,14 +171,14 @@ fun AnimeExtensionDetailsScreen(
|
|||
}
|
||||
|
||||
items(
|
||||
items = sources,
|
||||
items = presenter.sources,
|
||||
key = { it.source.id },
|
||||
) { source ->
|
||||
SourceSwitchPreference(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
source = source,
|
||||
onClickSourcePreferences = onClickSourcePreferences,
|
||||
onClickSource = onClickSource,
|
||||
onClickSource = { presenter.toggleSource(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -125,23 +190,22 @@ fun AnimeExtensionDetailsScreen(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WarningBanner(@StringRes textRes: Int) {
|
||||
Box(
|
||||
Text(
|
||||
text = stringResource(textRes),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.error)
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(textRes),
|
||||
color = MaterialTheme.colorScheme.onError,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DetailsHeader(
|
||||
|
@ -164,7 +228,7 @@ private fun DetailsHeader(
|
|||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
ExtensionIcon(
|
||||
AnimeExtensionIcon(
|
||||
modifier = Modifier
|
||||
.size(112.dp),
|
||||
extension = extension,
|
||||
|
@ -268,7 +332,9 @@ private fun InfoText(
|
|||
|
||||
val clickableModifier = if (onClick != null) {
|
||||
Modifier.clickable(interactionSource, indication = null) { onClick() }
|
||||
} else Modifier
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier.then(clickableModifier),
|
|
@ -0,0 +1,25 @@
|
|||
package eu.kanade.presentation.animebrowse
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import eu.kanade.tachiyomi.animeextension.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.ui.browse.animeextension.details.AnimeExtensionSourceItem
|
||||
|
||||
@Stable
|
||||
interface AnimeExtensionDetailsState {
|
||||
val isLoading: Boolean
|
||||
val extension: AnimeExtension.Installed?
|
||||
val sources: List<AnimeExtensionSourceItem>
|
||||
}
|
||||
|
||||
fun AnimeExtensionDetailsState(): AnimeExtensionDetailsState {
|
||||
return AnimeExtensionDetailsStateImpl()
|
||||
}
|
||||
|
||||
class AnimeExtensionDetailsStateImpl : AnimeExtensionDetailsState {
|
||||
override var isLoading: Boolean by mutableStateOf(true)
|
||||
override var extension: AnimeExtension.Installed? by mutableStateOf(null)
|
||||
override var sources: List<AnimeExtensionSourceItem> by mutableStateOf(emptyList())
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
package eu.kanade.presentation.animebrowse
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.FastScrollLazyColumn
|
||||
import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.presentation.components.PreferenceRow
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.browse.animeextension.AnimeExtensionFilterPresenter
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
||||
@Composable
|
||||
fun AnimeExtensionFilterScreen(
|
||||
navigateUp: () -> Unit,
|
||||
presenter: AnimeExtensionFilterPresenter,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
title = stringResource(R.string.label_extensions),
|
||||
navigateUp = navigateUp,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
when {
|
||||
presenter.isLoading -> LoadingScreen()
|
||||
presenter.isEmpty -> EmptyScreen(
|
||||
textResource = R.string.empty_screen,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
else -> {
|
||||
SourceFilterContent(
|
||||
contentPadding = contentPadding,
|
||||
state = presenter,
|
||||
onClickLang = {
|
||||
presenter.toggleLanguage(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
presenter.events.collectLatest {
|
||||
when (it) {
|
||||
AnimeExtensionFilterPresenter.Event.FailedFetchingLanguages -> {
|
||||
context.toast(R.string.internal_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SourceFilterContent(
|
||||
contentPadding: PaddingValues,
|
||||
state: AnimeExtensionFilterState,
|
||||
onClickLang: (String) -> Unit,
|
||||
) {
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
items(
|
||||
items = state.items,
|
||||
) { model ->
|
||||
ExtensionFilterItem(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
lang = model.lang,
|
||||
enabled = model.enabled,
|
||||
onClickItem = onClickLang,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionFilterItem(
|
||||
modifier: Modifier,
|
||||
lang: String,
|
||||
enabled: Boolean,
|
||||
onClickItem: (String) -> Unit,
|
||||
) {
|
||||
PreferenceRow(
|
||||
modifier = modifier,
|
||||
title = LocaleHelper.getSourceDisplayName(lang, LocalContext.current),
|
||||
action = {
|
||||
Switch(checked = enabled, onCheckedChange = null)
|
||||
},
|
||||
onClick = { onClickItem(lang) },
|
||||
)
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package eu.kanade.presentation.animebrowse
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import eu.kanade.tachiyomi.ui.browse.animeextension.AnimeFilterUiModel
|
||||
|
||||
@Stable
|
||||
interface AnimeExtensionFilterState {
|
||||
val isLoading: Boolean
|
||||
val items: List<AnimeFilterUiModel>
|
||||
val isEmpty: Boolean
|
||||
}
|
||||
|
||||
fun AnimeExtensionFilterState(): AnimeExtensionFilterState {
|
||||
return AnimeExtensionFilterStateImpl()
|
||||
}
|
||||
|
||||
class AnimeExtensionFilterStateImpl : AnimeExtensionFilterState {
|
||||
override var isLoading: Boolean by mutableStateOf(true)
|
||||
override var items: List<AnimeFilterUiModel> by mutableStateOf(emptyList())
|
||||
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
|
||||
}
|
|
@ -1,59 +1,62 @@
|
|||
package eu.kanade.presentation.browse
|
||||
package eu.kanade.presentation.animebrowse
|
||||
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import eu.kanade.presentation.animebrowse.components.AnimeExtensionIcon
|
||||
import eu.kanade.presentation.browse.ExtensionHeader
|
||||
import eu.kanade.presentation.browse.ExtensionTrustDialog
|
||||
import eu.kanade.presentation.browse.components.BaseBrowseItem
|
||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.FastScrollLazyColumn
|
||||
import eu.kanade.presentation.components.SwipeRefreshIndicator
|
||||
import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.presentation.components.SwipeRefresh
|
||||
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
|
||||
import eu.kanade.presentation.util.horizontalPadding
|
||||
import eu.kanade.presentation.util.plus
|
||||
import eu.kanade.presentation.util.secondaryItemAlpha
|
||||
import eu.kanade.presentation.util.topPaddingValues
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.extension.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.animeextension.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.ui.browse.animeextension.AnimeExtensionUiModel
|
||||
import eu.kanade.tachiyomi.ui.browse.animeextension.AnimeExtensionsPresenter
|
||||
import eu.kanade.tachiyomi.ui.browse.animeextension.ExtensionState
|
||||
import eu.kanade.tachiyomi.ui.browse.animeextension.ExtensionUiModel
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
|
||||
@Composable
|
||||
fun AnimeExtensionScreen(
|
||||
nestedScrollInterop: NestedScrollConnection,
|
||||
presenter: AnimeExtensionsPresenter,
|
||||
contentPadding: PaddingValues,
|
||||
onLongClickItem: (AnimeExtension) -> Unit,
|
||||
onClickItemCancel: (AnimeExtension) -> Unit,
|
||||
onInstallExtension: (AnimeExtension.Available) -> Unit,
|
||||
|
@ -63,21 +66,22 @@ fun AnimeExtensionScreen(
|
|||
onOpenExtension: (AnimeExtension.Installed) -> Unit,
|
||||
onClickUpdateAll: () -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
onLaunched: () -> Unit,
|
||||
) {
|
||||
val state by presenter.state.collectAsState()
|
||||
val isRefreshing = presenter.isRefreshing
|
||||
|
||||
SwipeRefresh(
|
||||
modifier = Modifier.nestedScroll(nestedScrollInterop),
|
||||
state = rememberSwipeRefreshState(isRefreshing),
|
||||
indicator = { s, trigger -> SwipeRefreshIndicator(s, trigger) },
|
||||
refreshing = presenter.isRefreshing,
|
||||
onRefresh = onRefresh,
|
||||
enabled = !presenter.isLoading,
|
||||
) {
|
||||
when (state) {
|
||||
is ExtensionState.Initialized -> {
|
||||
ExtensionContent(
|
||||
items = (state as ExtensionState.Initialized).list,
|
||||
when {
|
||||
presenter.isLoading -> LoadingScreen()
|
||||
presenter.isEmpty -> EmptyScreen(
|
||||
textResource = R.string.empty_screen,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
else -> {
|
||||
AnimeExtensionContent(
|
||||
state = presenter,
|
||||
contentPadding = contentPadding,
|
||||
onLongClickItem = onLongClickItem,
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
onInstallExtension = onInstallExtension,
|
||||
|
@ -86,17 +90,16 @@ fun AnimeExtensionScreen(
|
|||
onTrustExtension = onTrustExtension,
|
||||
onOpenExtension = onOpenExtension,
|
||||
onClickUpdateAll = onClickUpdateAll,
|
||||
onLaunched = onLaunched,
|
||||
)
|
||||
}
|
||||
ExtensionState.Uninitialized -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExtensionContent(
|
||||
items: List<ExtensionUiModel>,
|
||||
private fun AnimeExtensionContent(
|
||||
state: AnimeExtensionsState,
|
||||
contentPadding: PaddingValues,
|
||||
onLongClickItem: (AnimeExtension) -> Unit,
|
||||
onClickItemCancel: (AnimeExtension) -> Unit,
|
||||
onInstallExtension: (AnimeExtension.Available) -> Unit,
|
||||
|
@ -105,31 +108,29 @@ fun ExtensionContent(
|
|||
onTrustExtension: (AnimeExtension.Untrusted) -> Unit,
|
||||
onOpenExtension: (AnimeExtension.Installed) -> Unit,
|
||||
onClickUpdateAll: () -> Unit,
|
||||
onLaunched: () -> Unit,
|
||||
) {
|
||||
var trustState by remember { mutableStateOf<AnimeExtension.Untrusted?>(null) }
|
||||
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
|
||||
contentPadding = contentPadding + topPaddingValues,
|
||||
) {
|
||||
items(
|
||||
items = items,
|
||||
key = {
|
||||
when (it) {
|
||||
is ExtensionUiModel.Header.Resource -> it.textRes
|
||||
is ExtensionUiModel.Header.Text -> it.text
|
||||
is ExtensionUiModel.Item -> it.key()
|
||||
}
|
||||
},
|
||||
items = state.items,
|
||||
contentType = {
|
||||
when (it) {
|
||||
is ExtensionUiModel.Item -> "item"
|
||||
else -> "header"
|
||||
is AnimeExtensionUiModel.Header -> "header"
|
||||
is AnimeExtensionUiModel.Item -> "item"
|
||||
}
|
||||
},
|
||||
key = {
|
||||
when (it) {
|
||||
is AnimeExtensionUiModel.Header -> "animeextensionHeader-${it.hashCode()}"
|
||||
is AnimeExtensionUiModel.Item -> "animeextension-${it.hashCode()}"
|
||||
}
|
||||
},
|
||||
) { item ->
|
||||
when (item) {
|
||||
is ExtensionUiModel.Header.Resource -> {
|
||||
is AnimeExtensionUiModel.Header.Resource -> {
|
||||
val action: @Composable RowScope.() -> Unit =
|
||||
if (item.textRes == R.string.ext_updates_pending) {
|
||||
{
|
||||
|
@ -151,26 +152,20 @@ fun ExtensionContent(
|
|||
action = action,
|
||||
)
|
||||
}
|
||||
is ExtensionUiModel.Header.Text -> {
|
||||
is AnimeExtensionUiModel.Header.Text -> {
|
||||
ExtensionHeader(
|
||||
text = item.text,
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
)
|
||||
}
|
||||
is ExtensionUiModel.Item -> {
|
||||
is AnimeExtensionUiModel.Item -> {
|
||||
AnimeExtensionItem(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
item = item,
|
||||
onClickItem = {
|
||||
when (it) {
|
||||
is AnimeExtension.Available -> onInstallExtension(it)
|
||||
is AnimeExtension.Installed -> {
|
||||
if (it.hasUpdate) {
|
||||
onUpdateExtension(it)
|
||||
} else {
|
||||
onOpenExtension(it)
|
||||
}
|
||||
}
|
||||
is AnimeExtension.Installed -> onOpenExtension(it)
|
||||
is AnimeExtension.Untrusted -> { trustState = it }
|
||||
}
|
||||
},
|
||||
|
@ -190,9 +185,6 @@ fun ExtensionContent(
|
|||
}
|
||||
},
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
onLaunched()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -215,9 +207,9 @@ fun ExtensionContent(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun AnimeExtensionItem(
|
||||
private fun AnimeExtensionItem(
|
||||
modifier: Modifier = Modifier,
|
||||
item: ExtensionUiModel.Item,
|
||||
item: AnimeExtensionUiModel.Item,
|
||||
onClickItem: (AnimeExtension) -> Unit,
|
||||
onLongClickItem: (AnimeExtension) -> Unit,
|
||||
onClickItemCancel: (AnimeExtension) -> Unit,
|
||||
|
@ -233,10 +225,30 @@ fun AnimeExtensionItem(
|
|||
onClickItem = { onClickItem(extension) },
|
||||
onLongClickItem = { onLongClickItem(extension) },
|
||||
icon = {
|
||||
ExtensionIcon(extension = extension)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val idle = installStep.isCompleted()
|
||||
if (!idle) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(40.dp),
|
||||
strokeWidth = 2.dp,
|
||||
)
|
||||
}
|
||||
|
||||
val padding by animateDpAsState(targetValue = if (idle) 0.dp else 8.dp)
|
||||
AnimeExtensionIcon(
|
||||
extension = extension,
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.padding(padding),
|
||||
)
|
||||
}
|
||||
},
|
||||
action = {
|
||||
ExtensionItemActions(
|
||||
AnimeExtensionItemActions(
|
||||
extension = extension,
|
||||
installStep = installStep,
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
|
@ -244,29 +256,20 @@ fun AnimeExtensionItem(
|
|||
)
|
||||
},
|
||||
) {
|
||||
ExtensionItemContent(
|
||||
AnimeExtensionItemContent(
|
||||
extension = extension,
|
||||
installStep = installStep,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExtensionItemContent(
|
||||
private fun AnimeExtensionItemContent(
|
||||
extension: AnimeExtension,
|
||||
installStep: InstallStep,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val warning = remember(extension) {
|
||||
when {
|
||||
extension is AnimeExtension.Untrusted -> R.string.ext_untrusted
|
||||
extension is AnimeExtension.Installed && extension.isUnofficial -> R.string.ext_unofficial
|
||||
extension is AnimeExtension.Installed && extension.isObsolete -> R.string.ext_obsolete
|
||||
extension.isNsfw -> R.string.ext_nsfw_short
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier.padding(start = horizontalPadding),
|
||||
) {
|
||||
|
@ -276,56 +279,72 @@ fun ExtensionItemContent(
|
|||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
// Won't look good but it's not like we can ellipsize overflowing content
|
||||
FlowRow(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
mainAxisSpacing = 4.dp,
|
||||
) {
|
||||
if (extension.lang.isNullOrEmpty().not()) {
|
||||
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
||||
if (extension is AnimeExtension.Installed && extension.lang.isNotEmpty()) {
|
||||
Text(
|
||||
text = LocaleHelper.getSourceDisplayName(extension.lang, context),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current),
|
||||
)
|
||||
}
|
||||
|
||||
if (extension.versionName.isNotEmpty()) {
|
||||
Text(
|
||||
text = extension.versionName,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
|
||||
val warning = when {
|
||||
extension is AnimeExtension.Untrusted -> R.string.ext_untrusted
|
||||
extension is AnimeExtension.Installed && extension.isUnofficial -> R.string.ext_unofficial
|
||||
extension is AnimeExtension.Installed && extension.isObsolete -> R.string.ext_obsolete
|
||||
extension.isNsfw -> R.string.ext_nsfw_short
|
||||
else -> null
|
||||
}
|
||||
if (warning != null) {
|
||||
Text(
|
||||
text = stringResource(id = warning).uppercase(),
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
text = stringResource(warning).uppercase(),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
if (!installStep.isCompleted()) {
|
||||
DotSeparatorNoSpaceText()
|
||||
Text(
|
||||
text = when (installStep) {
|
||||
InstallStep.Pending -> stringResource(R.string.ext_pending)
|
||||
InstallStep.Downloading -> stringResource(R.string.ext_downloading)
|
||||
InstallStep.Installing -> stringResource(R.string.ext_installing)
|
||||
else -> error("Must not show non-install process text")
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExtensionItemActions(
|
||||
private fun AnimeExtensionItemActions(
|
||||
extension: AnimeExtension,
|
||||
installStep: InstallStep,
|
||||
modifier: Modifier = Modifier,
|
||||
onClickItemCancel: (AnimeExtension) -> Unit = {},
|
||||
onClickItemAction: (AnimeExtension) -> Unit = {},
|
||||
) {
|
||||
val isIdle = remember(installStep) {
|
||||
installStep == InstallStep.Idle || installStep == InstallStep.Error
|
||||
}
|
||||
val isIdle = installStep.isCompleted()
|
||||
Row(modifier = modifier) {
|
||||
if (isIdle) {
|
||||
TextButton(
|
||||
onClick = { onClickItemAction(extension) },
|
||||
enabled = isIdle,
|
||||
) {
|
||||
Text(
|
||||
text = when (installStep) {
|
||||
InstallStep.Pending -> stringResource(R.string.ext_pending)
|
||||
InstallStep.Downloading -> stringResource(R.string.ext_downloading)
|
||||
InstallStep.Installing -> stringResource(R.string.ext_installing)
|
||||
InstallStep.Installed -> stringResource(R.string.ext_installed)
|
||||
InstallStep.Error -> stringResource(R.string.action_retry)
|
||||
InstallStep.Idle -> {
|
||||
|
@ -341,18 +360,15 @@ fun ExtensionItemActions(
|
|||
is AnimeExtension.Available -> stringResource(R.string.ext_install)
|
||||
}
|
||||
}
|
||||
else -> error("Must not show install process text")
|
||||
},
|
||||
style = LocalTextStyle.current.copy(
|
||||
color = if (isIdle) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceTint,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (isIdle.not()) {
|
||||
} else {
|
||||
IconButton(onClick = { onClickItemCancel(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "",
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
imageVector = Icons.Outlined.Close,
|
||||
contentDescription = stringResource(R.string.action_cancel),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package eu.kanade.presentation.animebrowse
|
||||
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import eu.kanade.tachiyomi.ui.browse.animeextension.AnimeExtensionUiModel
|
||||
|
||||
interface AnimeExtensionsState {
|
||||
val isLoading: Boolean
|
||||
val isRefreshing: Boolean
|
||||
val items: List<AnimeExtensionUiModel>
|
||||
val updates: Int
|
||||
val isEmpty: Boolean
|
||||
}
|
||||
|
||||
fun AnimeExtensionState(): AnimeExtensionsState {
|
||||
return AnimeExtensionsStateImpl()
|
||||
}
|
||||
|
||||
class AnimeExtensionsStateImpl : AnimeExtensionsState {
|
||||
override var isLoading: Boolean by mutableStateOf(true)
|
||||
override var isRefreshing: Boolean by mutableStateOf(false)
|
||||
override var items: List<AnimeExtensionUiModel> by mutableStateOf(emptyList())
|
||||
override var updates: Int by mutableStateOf(0)
|
||||
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package eu.kanade.presentation.animebrowse
|
||||
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import eu.kanade.domain.anime.model.Anime
|
||||
import eu.kanade.presentation.browse.BrowseSourceFloatingActionButton
|
||||
import eu.kanade.presentation.browse.components.BrowseSourceSearchToolbar
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.animesource.LocalAnimeSource
|
||||
import eu.kanade.tachiyomi.ui.browse.animesource.browse.BrowseAnimeSourcePresenter
|
||||
import eu.kanade.tachiyomi.ui.more.MoreController
|
||||
|
||||
@Composable
|
||||
fun AnimeSourceSearchScreen(
|
||||
presenter: BrowseAnimeSourcePresenter,
|
||||
navigateUp: () -> Unit,
|
||||
onFabClick: () -> Unit,
|
||||
onAnimeClick: (Anime) -> Unit,
|
||||
onWebViewClick: () -> Unit,
|
||||
) {
|
||||
val columns by presenter.getColumnsPreferenceForCurrentOrientation()
|
||||
|
||||
val mangaList = presenter.getAnimeList().collectAsLazyPagingItems()
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
val onHelpClick = {
|
||||
uriHandler.openUri(LocalAnimeSource.HELP_URL)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
BrowseSourceSearchToolbar(
|
||||
searchQuery = presenter.searchQuery ?: "",
|
||||
onSearchQueryChanged = { presenter.searchQuery = it },
|
||||
placeholderText = stringResource(R.string.action_search_hint),
|
||||
navigateUp = navigateUp,
|
||||
onResetClick = { presenter.searchQuery = "" },
|
||||
onSearchClick = { presenter.search(it) },
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
BrowseSourceFloatingActionButton(
|
||||
isVisible = presenter.filters.isNotEmpty(),
|
||||
onFabClick = onFabClick,
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
SnackbarHost(hostState = snackbarHostState)
|
||||
},
|
||||
) { paddingValues ->
|
||||
BrowseAnimeSourceContent(
|
||||
state = presenter,
|
||||
animeList = mangaList,
|
||||
getAnimeState = { presenter.getAnime(it) },
|
||||
columns = columns,
|
||||
displayMode = presenter.displayMode,
|
||||
snackbarHostState = snackbarHostState,
|
||||
contentPadding = paddingValues,
|
||||
onWebViewClick = onWebViewClick,
|
||||
onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) },
|
||||
onLocalAnimeSourceHelpClick = onHelpClick,
|
||||
onAnimeClick = onAnimeClick,
|
||||
onAnimeLongClick = onAnimeClick,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
package eu.kanade.presentation.animebrowse
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.domain.animesource.model.AnimeSource
|
||||
import eu.kanade.presentation.animebrowse.components.BaseAnimeSourceItem
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.FastScrollLazyColumn
|
||||
import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.presentation.components.PreferenceRow
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.browse.animesource.AnimeFilterUiModel
|
||||
import eu.kanade.tachiyomi.ui.browse.animesource.AnimeSourcesFilterPresenter
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
||||
@Composable
|
||||
fun AnimeSourcesFilterScreen(
|
||||
navigateUp: () -> Unit,
|
||||
presenter: AnimeSourcesFilterPresenter,
|
||||
onClickLang: (String) -> Unit,
|
||||
onClickSource: (AnimeSource) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
title = stringResource(R.string.label_sources),
|
||||
navigateUp = navigateUp,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
when {
|
||||
presenter.isLoading -> LoadingScreen()
|
||||
presenter.isEmpty -> EmptyScreen(
|
||||
textResource = R.string.source_filter_empty_screen,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
else -> {
|
||||
AnimeSourcesFilterContent(
|
||||
contentPadding = contentPadding,
|
||||
state = presenter,
|
||||
onClickLang = onClickLang,
|
||||
onClickSource = onClickSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
presenter.events.collectLatest { event ->
|
||||
when (event) {
|
||||
AnimeSourcesFilterPresenter.Event.FailedFetchingLanguages -> {
|
||||
context.toast(R.string.internal_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimeSourcesFilterContent(
|
||||
contentPadding: PaddingValues,
|
||||
state: AnimeSourcesFilterState,
|
||||
onClickLang: (String) -> Unit,
|
||||
onClickSource: (AnimeSource) -> Unit,
|
||||
) {
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
items(
|
||||
items = state.items,
|
||||
contentType = {
|
||||
when (it) {
|
||||
is AnimeFilterUiModel.Header -> "header"
|
||||
is AnimeFilterUiModel.Item -> "item"
|
||||
}
|
||||
},
|
||||
key = {
|
||||
when (it) {
|
||||
is AnimeFilterUiModel.Header -> it.hashCode()
|
||||
is AnimeFilterUiModel.Item -> "source-filter-${it.source.key()}"
|
||||
}
|
||||
},
|
||||
) { model ->
|
||||
when (model) {
|
||||
is AnimeFilterUiModel.Header -> AnimeSourcesFilterHeader(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
language = model.language,
|
||||
enabled = model.enabled,
|
||||
onClickItem = onClickLang,
|
||||
)
|
||||
is AnimeFilterUiModel.Item -> AnimeSourcesFilterItem(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
source = model.source,
|
||||
isEnabled = model.isEnabled,
|
||||
onClickItem = onClickSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AnimeSourceFilterHeader(
|
||||
modifier: Modifier,
|
||||
language: String,
|
||||
isEnabled: Boolean,
|
||||
onClickItem: (String) -> Unit,
|
||||
) {
|
||||
PreferenceRow(
|
||||
modifier = modifier,
|
||||
title = LocaleHelper.getSourceDisplayName(language, LocalContext.current),
|
||||
action = {
|
||||
Switch(checked = isEnabled, onCheckedChange = null)
|
||||
},
|
||||
onClick = { onClickItem(language) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimeSourcesFilterHeader(
|
||||
modifier: Modifier,
|
||||
language: String,
|
||||
enabled: Boolean,
|
||||
onClickItem: (String) -> Unit,
|
||||
) {
|
||||
PreferenceRow(
|
||||
modifier = modifier,
|
||||
title = LocaleHelper.getSourceDisplayName(language, LocalContext.current),
|
||||
action = {
|
||||
Switch(checked = enabled, onCheckedChange = null)
|
||||
},
|
||||
onClick = { onClickItem(language) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimeSourcesFilterItem(
|
||||
modifier: Modifier,
|
||||
source: AnimeSource,
|
||||
isEnabled: Boolean,
|
||||
onClickItem: (AnimeSource) -> Unit,
|
||||
) {
|
||||
BaseAnimeSourceItem(
|
||||
modifier = modifier,
|
||||
source = source,
|
||||
showLanguageInContent = false,
|
||||
onClickItem = { onClickItem(source) },
|
||||
action = {
|
||||
Checkbox(checked = isEnabled, onCheckedChange = null)
|
||||
},
|
||||
)
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package eu.kanade.presentation.animebrowse
|
||||
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import eu.kanade.tachiyomi.ui.browse.animesource.AnimeFilterUiModel
|
||||
|
||||
interface AnimeSourcesFilterState {
|
||||
val isLoading: Boolean
|
||||
val items: List<AnimeFilterUiModel>
|
||||
val isEmpty: Boolean
|
||||
}
|
||||
|
||||
fun AnimeSourcesFilterState(): AnimeSourcesFilterState {
|
||||
return AnimeSourcesFilterStateImpl()
|
||||
}
|
||||
|
||||
class AnimeSourcesFilterStateImpl : AnimeSourcesFilterState {
|
||||
override var isLoading: Boolean by mutableStateOf(true)
|
||||
override var items: List<AnimeFilterUiModel> by mutableStateOf(emptyList())
|
||||
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
|
||||
}
|
|
@ -1,16 +1,10 @@
|
|||
package eu.kanade.presentation.browse
|
||||
package eu.kanade.presentation.animebrowse
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PushPin
|
||||
|
@ -23,73 +17,78 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.domain.animesource.interactor.GetRemoteAnime
|
||||
import eu.kanade.domain.animesource.model.AnimeSource
|
||||
import eu.kanade.domain.animesource.model.Pin
|
||||
import eu.kanade.presentation.browse.components.BaseAnimeSourceItem
|
||||
import eu.kanade.presentation.animebrowse.components.BaseAnimeSourceItem
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
||||
import eu.kanade.presentation.theme.header
|
||||
import eu.kanade.presentation.util.horizontalPadding
|
||||
import eu.kanade.presentation.util.plus
|
||||
import eu.kanade.presentation.util.topPaddingValues
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.animesource.LocalAnimeSource
|
||||
import eu.kanade.tachiyomi.ui.browse.animesource.AnimeSourceState
|
||||
import eu.kanade.tachiyomi.ui.browse.animesource.AnimeSourcesPresenter
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
||||
@Composable
|
||||
fun AnimeSourcesScreen(
|
||||
nestedScrollInterop: NestedScrollConnection,
|
||||
presenter: AnimeSourcesPresenter,
|
||||
onClickItem: (AnimeSource) -> Unit,
|
||||
contentPadding: PaddingValues,
|
||||
onClickItem: (AnimeSource, String) -> Unit,
|
||||
onClickDisable: (AnimeSource) -> Unit,
|
||||
onClickLatest: (AnimeSource) -> Unit,
|
||||
onClickPin: (AnimeSource) -> Unit,
|
||||
) {
|
||||
val state by presenter.state.collectAsState()
|
||||
|
||||
when (state) {
|
||||
is AnimeSourceState.Loading -> LoadingScreen()
|
||||
is AnimeSourceState.Error -> Text(text = (state as AnimeSourceState.Error).error.message!!)
|
||||
is AnimeSourceState.Success -> AnimeSourceList(
|
||||
nestedScrollConnection = nestedScrollInterop,
|
||||
list = (state as AnimeSourceState.Success).uiModels,
|
||||
val context = LocalContext.current
|
||||
when {
|
||||
presenter.isLoading -> LoadingScreen()
|
||||
presenter.isEmpty -> EmptyScreen(
|
||||
textResource = R.string.source_empty_screen,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
else -> {
|
||||
AnimeSourceList(
|
||||
state = presenter,
|
||||
contentPadding = contentPadding,
|
||||
onClickItem = onClickItem,
|
||||
onClickDisable = onClickDisable,
|
||||
onClickLatest = onClickLatest,
|
||||
onClickPin = onClickPin,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AnimeSourceList(
|
||||
nestedScrollConnection: NestedScrollConnection,
|
||||
list: List<AnimeSourceUiModel>,
|
||||
onClickItem: (AnimeSource) -> Unit,
|
||||
onClickDisable: (AnimeSource) -> Unit,
|
||||
onClickLatest: (AnimeSource) -> Unit,
|
||||
onClickPin: (AnimeSource) -> Unit,
|
||||
) {
|
||||
if (list.isEmpty()) {
|
||||
EmptyScreen(textResource = R.string.source_empty_screen)
|
||||
return
|
||||
LaunchedEffect(Unit) {
|
||||
presenter.events.collectLatest { event ->
|
||||
when (event) {
|
||||
AnimeSourcesPresenter.Event.FailedFetchingSources -> {
|
||||
context.toast(R.string.internal_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val (sourceState, setSourceState) = remember { mutableStateOf<AnimeSource?>(null) }
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.nestedScroll(nestedScrollConnection),
|
||||
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
|
||||
@Composable
|
||||
private fun AnimeSourceList(
|
||||
state: AnimeSourcesState,
|
||||
contentPadding: PaddingValues,
|
||||
onClickItem: (AnimeSource, String) -> Unit,
|
||||
onClickDisable: (AnimeSource) -> Unit,
|
||||
onClickPin: (AnimeSource) -> Unit,
|
||||
) {
|
||||
ScrollbarLazyColumn(
|
||||
contentPadding = contentPadding + topPaddingValues,
|
||||
) {
|
||||
items(
|
||||
items = list,
|
||||
items = state.items,
|
||||
contentType = {
|
||||
when (it) {
|
||||
is AnimeSourceUiModel.Header -> "header"
|
||||
|
@ -99,13 +98,13 @@ fun AnimeSourceList(
|
|||
key = {
|
||||
when (it) {
|
||||
is AnimeSourceUiModel.Header -> it.hashCode()
|
||||
is AnimeSourceUiModel.Item -> it.source.key()
|
||||
is AnimeSourceUiModel.Item -> "source-${it.source.key()}"
|
||||
}
|
||||
},
|
||||
) { model ->
|
||||
when (model) {
|
||||
is AnimeSourceUiModel.Header -> {
|
||||
SourceHeader(
|
||||
AnimeSourceHeader(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
language = model.language,
|
||||
)
|
||||
|
@ -114,49 +113,60 @@ fun AnimeSourceList(
|
|||
modifier = Modifier.animateItemPlacement(),
|
||||
source = model.source,
|
||||
onClickItem = onClickItem,
|
||||
onLongClickItem = {
|
||||
setSourceState(it)
|
||||
},
|
||||
onClickLatest = onClickLatest,
|
||||
onLongClickItem = { state.dialog = AnimeSourcesPresenter.Dialog(it) },
|
||||
onClickPin = onClickPin,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceState != null) {
|
||||
if (state.dialog != null) {
|
||||
val source = state.dialog!!.source
|
||||
AnimeSourceOptionsDialog(
|
||||
source = sourceState,
|
||||
source = source,
|
||||
onClickPin = {
|
||||
onClickPin(sourceState)
|
||||
setSourceState(null)
|
||||
onClickPin(source)
|
||||
state.dialog = null
|
||||
},
|
||||
onClickDisable = {
|
||||
onClickDisable(sourceState)
|
||||
setSourceState(null)
|
||||
onClickDisable(source)
|
||||
state.dialog = null
|
||||
},
|
||||
onDismiss = { setSourceState(null) },
|
||||
onDismiss = { state.dialog = null },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AnimeSourceItem(
|
||||
private fun AnimeSourceHeader(
|
||||
modifier: Modifier = Modifier,
|
||||
language: String,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Text(
|
||||
text = LocaleHelper.getSourceDisplayName(language, context),
|
||||
modifier = modifier
|
||||
.padding(horizontal = horizontalPadding, vertical = 8.dp),
|
||||
style = MaterialTheme.typography.header,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimeSourceItem(
|
||||
modifier: Modifier = Modifier,
|
||||
source: AnimeSource,
|
||||
onClickItem: (AnimeSource) -> Unit,
|
||||
onClickItem: (AnimeSource, String) -> Unit,
|
||||
onLongClickItem: (AnimeSource) -> Unit,
|
||||
onClickLatest: (AnimeSource) -> Unit,
|
||||
onClickPin: (AnimeSource) -> Unit,
|
||||
) {
|
||||
BaseAnimeSourceItem(
|
||||
modifier = modifier,
|
||||
source = source,
|
||||
onClickItem = { onClickItem(source) },
|
||||
onClickItem = { onClickItem(source, GetRemoteAnime.QUERY_POPULAR) },
|
||||
onLongClickItem = { onLongClickItem(source) },
|
||||
action = { source ->
|
||||
action = {
|
||||
if (source.supportsLatest) {
|
||||
TextButton(onClick = { onClickLatest(source) }) {
|
||||
TextButton(onClick = { onClickItem(source, GetRemoteAnime.QUERY_LATEST) }) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.latest),
|
||||
style = LocalTextStyle.current.copy(
|
||||
|
@ -174,46 +184,24 @@ fun AnimeSourceItem(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun AnimeSourceIcon(
|
||||
source: AnimeSource,
|
||||
) {
|
||||
val icon = source.icon
|
||||
val modifier = Modifier
|
||||
.height(40.dp)
|
||||
.aspectRatio(1f)
|
||||
if (icon != null) {
|
||||
Image(
|
||||
bitmap = icon,
|
||||
contentDescription = "",
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.ic_local_source),
|
||||
contentDescription = "",
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AnimeSourcePinButton(
|
||||
private fun AnimeSourcePinButton(
|
||||
isPinned: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val icon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin
|
||||
val tint = if (isPinned) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground
|
||||
val description = if (isPinned) R.string.action_unpin else R.string.action_pin
|
||||
IconButton(onClick = onClick) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = "",
|
||||
tint = tint,
|
||||
contentDescription = stringResource(description),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AnimeSourceOptionsDialog(
|
||||
private fun AnimeSourceOptionsDialog(
|
||||
source: AnimeSource,
|
||||
onClickPin: () -> Unit,
|
||||
onClickDisable: () -> Unit,
|
||||
|
@ -221,7 +209,7 @@ fun AnimeSourceOptionsDialog(
|
|||
) {
|
||||
AlertDialog(
|
||||
title = {
|
||||
Text(text = source.nameWithLanguage)
|
||||
Text(text = source.visualName)
|
||||
},
|
||||
text = {
|
||||
Column {
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue