mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-29 09:39:03 +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")) {
|
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
||||||
apply<com.google.gms.googleservices.GoogleServicesPlugin>()
|
apply<com.google.gms.googleservices.GoogleServicesPlugin>()
|
||||||
apply<com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsPlugin>()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
shortcutHelper.setFilePath("./shortcuts.xml")
|
shortcutHelper.setFilePath("./shortcuts.xml")
|
||||||
|
@ -29,7 +28,7 @@ android {
|
||||||
minSdk = AndroidConfig.minSdk
|
minSdk = AndroidConfig.minSdk
|
||||||
targetSdk = AndroidConfig.targetSdk
|
targetSdk = AndroidConfig.targetSdk
|
||||||
versionCode = 90
|
versionCode = 90
|
||||||
versionName = "0.13.5.0"
|
versionName = "0.14.0.0"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
|
@ -296,7 +295,6 @@ dependencies {
|
||||||
// Crash reports/analytics
|
// Crash reports/analytics
|
||||||
implementation(libs.acra.http)
|
implementation(libs.acra.http)
|
||||||
implementation(libs.firebase.analytics)
|
implementation(libs.firebase.analytics)
|
||||||
implementation(libs.firebase.crashlytics)
|
|
||||||
|
|
||||||
// Shizuku
|
// Shizuku
|
||||||
implementation(libs.bundles.shizuku)
|
implementation(libs.bundles.shizuku)
|
||||||
|
|
|
@ -141,7 +141,7 @@
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".extension.util.AnimeExtensionInstallActivity"
|
android:name=".animeextension.util.AnimeExtensionInstallActivity"
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
@ -225,12 +225,6 @@
|
||||||
android:name=".data.notification.NotificationReceiver"
|
android:name=".data.notification.NotificationReceiver"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<receiver android:name="androidx.media.session.MediaButtonReceiver">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MEDIA_BUTTON"/>
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".glance.UpdatesGridGlanceReceiver"
|
android:name=".glance.UpdatesGridGlanceReceiver"
|
||||||
android:enabled="@bool/glance_appwidget_available"
|
android:enabled="@bool/glance_appwidget_available"
|
||||||
|
@ -258,7 +252,7 @@
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".data.download.AnimeDownloadService"
|
android:name=".data.animedownload.AnimeDownloadService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
|
@ -272,7 +266,7 @@
|
||||||
<service android:name=".extension.util.ExtensionInstallService"
|
<service android:name=".extension.util.ExtensionInstallService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service android:name=".extension.util.AnimeExtensionInstallService"
|
<service android:name=".animeextension.util.AnimeExtensionInstallService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||||
|
|
|
@ -1,40 +1,17 @@
|
||||||
package eu.kanade.data.anime
|
package eu.kanade.data.anime
|
||||||
|
|
||||||
import eu.kanade.domain.anime.model.Anime
|
import eu.kanade.domain.anime.model.Anime
|
||||||
import eu.kanade.domain.episode.model.Episode
|
import eu.kanade.domain.animelib.model.AnimelibAnime
|
||||||
import eu.kanade.tachiyomi.data.database.models.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 =
|
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, viewer, episodeFlags, coverLastModified, dateAdded ->
|
{ id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, _, initialized, viewerFlags, episodeFlags, coverLastModified, dateAdded, updateStrategy ->
|
||||||
Anime(
|
Anime(
|
||||||
id = id,
|
id = id,
|
||||||
source = source,
|
source = source,
|
||||||
favorite = favorite,
|
favorite = favorite,
|
||||||
lastUpdate = lastUpdate ?: 0,
|
lastUpdate = lastUpdate ?: 0,
|
||||||
dateAdded = dateAdded,
|
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,
|
viewerFlags = viewerFlags,
|
||||||
episodeFlags = episodeFlags,
|
episodeFlags = episodeFlags,
|
||||||
coverLastModified = coverLastModified,
|
coverLastModified = coverLastModified,
|
||||||
|
@ -46,46 +23,40 @@ val animeEpisodeMapper: (Long, Long, String, String?, String?, String?, List<Str
|
||||||
genre = genre,
|
genre = genre,
|
||||||
status = status,
|
status = status,
|
||||||
thumbnailUrl = thumbnailUrl,
|
thumbnailUrl = thumbnailUrl,
|
||||||
|
updateStrategy = updateStrategy,
|
||||||
initialized = initialized,
|
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, UpdateStrategy, Long, Long, Long, Long, Long, Long, Long) -> AnimelibAnime =
|
||||||
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, thumbnailUrl, favorite, lastUpdate, nextUpdate, initialized, viewerFlags, episodeFlags, coverLastModified, dateAdded, updateStrategy, totalCount, seenCount, latestUpload, episodeFetchedAt, lastSeen, bookmarkCount, category ->
|
||||||
{ _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(
|
||||||
AnimelibAnime().apply {
|
anime = animeMapper(
|
||||||
this.id = _id
|
id,
|
||||||
this.source = source
|
source,
|
||||||
this.url = url
|
url,
|
||||||
this.artist = artist
|
artist,
|
||||||
this.author = author
|
author,
|
||||||
this.description = description
|
description,
|
||||||
this.genre = genre?.joinToString()
|
genre,
|
||||||
this.title = title
|
title,
|
||||||
this.status = status.toInt()
|
status,
|
||||||
this.thumbnail_url = thumbnail_url
|
thumbnailUrl,
|
||||||
this.favorite = favorite
|
favorite,
|
||||||
this.last_update = last_update ?: 0
|
lastUpdate,
|
||||||
this.initialized = initialized
|
nextUpdate,
|
||||||
this.viewer_flags = viewer.toInt()
|
initialized,
|
||||||
this.episode_flags = episode_flags.toInt()
|
viewerFlags,
|
||||||
this.cover_last_modified = cover_last_modified
|
episodeFlags,
|
||||||
this.date_added = date_added
|
coverLastModified,
|
||||||
this.unseenCount = unseen_count.toInt()
|
dateAdded,
|
||||||
this.seenCount = seen_count.toInt()
|
updateStrategy,
|
||||||
this.category = category.toInt()
|
),
|
||||||
}
|
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.AnimeDatabaseHandler
|
||||||
import eu.kanade.data.listOfStringsAdapter
|
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.Anime
|
||||||
import eu.kanade.domain.anime.model.AnimeUpdate
|
import eu.kanade.domain.anime.model.AnimeUpdate
|
||||||
import eu.kanade.domain.anime.repository.AnimeRepository
|
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.logcat
|
||||||
|
import eu.kanade.tachiyomi.util.system.toLong
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
|
||||||
|
@ -24,7 +25,11 @@ class AnimeRepositoryImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getAnimeByUrlAndSourceId(url: String, sourceId: Long): Anime? {
|
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> {
|
override suspend fun getFavorites(): List<Anime> {
|
||||||
|
@ -32,11 +37,11 @@ class AnimeRepositoryImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getAnimelibAnime(): List<AnimelibAnime> {
|
override suspend fun getAnimelibAnime(): List<AnimelibAnime> {
|
||||||
return handler.awaitList { animesQueries.getAnimelib(animelibAnime) }
|
return handler.awaitList { animelibViewQueries.animelib(animelibAnime) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAnimelibAnimeAsFlow(): Flow<List<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>> {
|
override fun getFavoritesBySourceId(sourceId: Long): Flow<List<Anime>> {
|
||||||
|
@ -69,7 +74,7 @@ class AnimeRepositoryImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun insert(anime: Anime): Long? {
|
override suspend fun insert(anime: Anime): Long? {
|
||||||
return handler.awaitOneOrNull {
|
return handler.awaitOneOrNull(inTransaction = true) {
|
||||||
animesQueries.insert(
|
animesQueries.insert(
|
||||||
source = anime.source,
|
source = anime.source,
|
||||||
url = anime.url,
|
url = anime.url,
|
||||||
|
@ -88,6 +93,7 @@ class AnimeRepositoryImpl(
|
||||||
episodeFlags = anime.episodeFlags,
|
episodeFlags = anime.episodeFlags,
|
||||||
coverLastModified = anime.coverLastModified,
|
coverLastModified = anime.coverLastModified,
|
||||||
dateAdded = anime.dateAdded,
|
dateAdded = anime.dateAdded,
|
||||||
|
updateStrategy = anime.updateStrategy,
|
||||||
)
|
)
|
||||||
animesQueries.selectLastInsertedRowId()
|
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 {
|
return try {
|
||||||
partialUpdate(*values.toTypedArray())
|
partialUpdate(*animeUpdates.toTypedArray())
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e)
|
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) {
|
handler.await(inTransaction = true) {
|
||||||
values.forEach { value ->
|
animeUpdates.forEach { value ->
|
||||||
animesQueries.update(
|
animesQueries.update(
|
||||||
source = value.source,
|
source = value.source,
|
||||||
url = value.url,
|
url = value.url,
|
||||||
|
@ -134,6 +140,7 @@ class AnimeRepositoryImpl(
|
||||||
coverLastModified = value.coverLastModified,
|
coverLastModified = value.coverLastModified,
|
||||||
dateAdded = value.dateAdded,
|
dateAdded = value.dateAdded,
|
||||||
animeId = value.id,
|
animeId = value.id,
|
||||||
|
updateStrategy = value.updateStrategy?.let(updateStrategyAdapter::encode),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,21 @@
|
||||||
package eu.kanade.data.animehistory
|
package eu.kanade.data.animehistory
|
||||||
|
|
||||||
import androidx.paging.PagingSource
|
|
||||||
import eu.kanade.data.AnimeDatabaseHandler
|
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.AnimeHistoryUpdate
|
||||||
import eu.kanade.domain.animehistory.model.AnimeHistoryWithRelations
|
import eu.kanade.domain.animehistory.model.AnimeHistoryWithRelations
|
||||||
import eu.kanade.domain.animehistory.repository.AnimeHistoryRepository
|
import eu.kanade.domain.animehistory.repository.AnimeHistoryRepository
|
||||||
import eu.kanade.domain.episode.model.Episode
|
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
|
||||||
class AnimeHistoryRepositoryImpl(
|
class AnimeHistoryRepositoryImpl(
|
||||||
private val handler: AnimeDatabaseHandler,
|
private val handler: AnimeDatabaseHandler,
|
||||||
) : AnimeHistoryRepository {
|
) : AnimeHistoryRepository {
|
||||||
|
|
||||||
override fun getHistory(query: String): PagingSource<Long, AnimeHistoryWithRelations> {
|
override fun getHistory(query: String): Flow<List<AnimeHistoryWithRelations>> {
|
||||||
return handler.subscribeToPagingSource(
|
return handler.subscribeToList {
|
||||||
countQuery = { animehistoryViewQueries.countHistory(query) },
|
animehistoryViewQueries.animehistory(query, animehistoryWithRelationsMapper)
|
||||||
queryProvider = { limit, offset ->
|
}
|
||||||
animehistoryViewQueries.animehistory(query, limit, offset, animehistoryWithRelationsMapper)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getLastHistory(): AnimeHistoryWithRelations? {
|
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) {
|
override suspend fun resetHistory(historyId: Long) {
|
||||||
try {
|
try {
|
||||||
handler.await { animehistoryQueries.resetAnimeHistoryById(historyId) }
|
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.lang,
|
||||||
source.name,
|
source.name,
|
||||||
supportsLatest = false,
|
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.data.AnimeDatabaseHandler
|
||||||
import eu.kanade.domain.animesource.model.AnimeSource
|
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.domain.animesource.repository.AnimeSourceRepository
|
||||||
|
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
||||||
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
|
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
|
||||||
import eu.kanade.tachiyomi.animesource.LocalAnimeSource
|
import eu.kanade.tachiyomi.animesource.LocalAnimeSource
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import eu.kanade.tachiyomi.animesource.AnimeSource as LoadedAnimeSource
|
|
||||||
|
|
||||||
class AnimeSourceRepositoryImpl(
|
class AnimeSourceRepositoryImpl(
|
||||||
private val sourceManager: AnimeSourceManager,
|
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() }
|
val sourceIdWithNonLibraryAnime = handler.subscribeToList { animesQueries.getSourceIdsWithNonLibraryAnime() }
|
||||||
return sourceIdWithNonLibraryAnime.map { sourceId ->
|
return sourceIdWithNonLibraryAnime.map { sourceId ->
|
||||||
sourceId.map { (sourceId, count) ->
|
sourceId.map { (sourceId, count) ->
|
||||||
val source = sourceManager.getOrStub(sourceId)
|
val source = sourceManager.getOrStub(sourceId)
|
||||||
source to count
|
AnimeSourceWithCount(animesourceMapper(source), count)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getAnimeSourceData(id: Long): AnimeSourceData? {
|
override fun search(
|
||||||
return handler.awaitOneOrNull { animesourcesQueries.getAnimeSourceData(id, animesourceDataMapper) }
|
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) {
|
override fun getPopular(sourceId: Long): AnimeSourcePagingSourceType {
|
||||||
handler.await { animesourcesQueries.upsert(id, lang, name) }
|
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())
|
insertValues(*tracks.toTypedArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun insertValues(vararg values: AnimeTrack) {
|
private suspend fun insertValues(vararg tracks: AnimeTrack) {
|
||||||
handler.await(inTransaction = true) {
|
handler.await(inTransaction = true) {
|
||||||
values.forEach { animeTrack ->
|
tracks.forEach { animeTrack ->
|
||||||
anime_syncQueries.insert(
|
anime_syncQueries.insert(
|
||||||
animeId = animeTrack.animeId,
|
animeId = animeTrack.animeId,
|
||||||
syncId = animeTrack.syncId,
|
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.Category
|
||||||
import eu.kanade.domain.category.model.CategoryUpdate
|
import eu.kanade.domain.category.model.CategoryUpdate
|
||||||
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
|
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
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
class CategoryRepositoryImplAnime(
|
class CategoryRepositoryImplAnime(
|
||||||
private val handler: AnimeDatabaseHandler,
|
private val handler: AnimeDatabaseHandler,
|
||||||
) : CategoryRepositoryAnime {
|
) : CategoryRepositoryAnime {
|
||||||
|
|
||||||
|
override suspend fun get(id: Long): Category? {
|
||||||
|
return handler.awaitOneOrNull { categoriesQueries.getCategory(id, categoryMapper) }
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getAll(): List<Category> {
|
override suspend fun getAll(): List<Category> {
|
||||||
return handler.awaitList { categoriesQueries.getCategories(categoryMapper) }
|
return handler.awaitList { categoriesQueries.getCategories(categoryMapper) }
|
||||||
}
|
}
|
||||||
|
@ -31,29 +35,43 @@ class CategoryRepositoryImplAnime(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(DuplicateNameException::class)
|
override suspend fun insert(category: Category) {
|
||||||
override suspend fun insert(name: String, order: Long) {
|
|
||||||
if (checkDuplicateName(name)) throw DuplicateNameException(name)
|
|
||||||
handler.await {
|
handler.await {
|
||||||
categoriesQueries.insert(
|
categoriesQueries.insert(
|
||||||
name = name,
|
name = category.name,
|
||||||
order = order,
|
order = category.order,
|
||||||
flags = 0L,
|
flags = category.flags,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(DuplicateNameException::class)
|
override suspend fun updatePartial(update: CategoryUpdate) {
|
||||||
override suspend fun update(payload: CategoryUpdate) {
|
|
||||||
if (payload.name != null && checkDuplicateName(payload.name)) throw DuplicateNameException(payload.name)
|
|
||||||
handler.await {
|
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(
|
categoriesQueries.update(
|
||||||
name = payload.name,
|
name = update.name,
|
||||||
order = payload.order,
|
order = update.order,
|
||||||
flags = payload.flags,
|
flags = update.flags,
|
||||||
categoryId = payload.id,
|
categoryId = update.id,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun updateAllFlags(flags: Long?) {
|
||||||
|
handler.await {
|
||||||
|
categoriesQueries.updateAllFlags(flags)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun delete(categoryId: Long) {
|
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
|
package eu.kanade.data.episode
|
||||||
|
|
||||||
import eu.kanade.data.AnimeDatabaseHandler
|
import eu.kanade.data.AnimeDatabaseHandler
|
||||||
import eu.kanade.data.toLong
|
|
||||||
import eu.kanade.domain.episode.model.Episode
|
import eu.kanade.domain.episode.model.Episode
|
||||||
import eu.kanade.domain.episode.model.EpisodeUpdate
|
import eu.kanade.domain.episode.model.EpisodeUpdate
|
||||||
import eu.kanade.domain.episode.repository.EpisodeRepository
|
import eu.kanade.domain.episode.repository.EpisodeRepository
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import eu.kanade.tachiyomi.util.system.toLong
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
|
||||||
|
@ -83,6 +83,10 @@ class EpisodeRepositoryImpl(
|
||||||
return handler.awaitList { episodesQueries.getEpisodesByAnimeId(animeId, episodeMapper) }
|
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? {
|
override suspend fun getEpisodeById(id: Long): Episode? {
|
||||||
return handler.awaitOneOrNull { episodesQueries.getEpisodeById(id, episodeMapper) }
|
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
|
package eu.kanade.data.source
|
||||||
|
|
||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
|
import eu.kanade.data.chapter.NoChaptersException
|
||||||
import eu.kanade.domain.source.model.SourcePagingSourceType
|
import eu.kanade.domain.source.model.SourcePagingSourceType
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
@ -22,7 +23,7 @@ abstract class SourcePagingSource(
|
||||||
withIOContext {
|
withIOContext {
|
||||||
requestNextPage(page.toInt())
|
requestNextPage(page.toInt())
|
||||||
.takeIf { it.mangas.isNotEmpty() }
|
.takeIf { it.mangas.isNotEmpty() }
|
||||||
?: throw NoResultsException()
|
?: throw NoChaptersException()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return LoadResult.Error(e)
|
return LoadResult.Error(e)
|
||||||
|
|
|
@ -2,8 +2,10 @@ package eu.kanade.domain
|
||||||
|
|
||||||
import eu.kanade.data.anime.AnimeRepositoryImpl
|
import eu.kanade.data.anime.AnimeRepositoryImpl
|
||||||
import eu.kanade.data.animehistory.AnimeHistoryRepositoryImpl
|
import eu.kanade.data.animehistory.AnimeHistoryRepositoryImpl
|
||||||
|
import eu.kanade.data.animesource.AnimeSourceDataRepositoryImpl
|
||||||
import eu.kanade.data.animesource.AnimeSourceRepositoryImpl
|
import eu.kanade.data.animesource.AnimeSourceRepositoryImpl
|
||||||
import eu.kanade.data.animetrack.AnimeTrackRepositoryImpl
|
import eu.kanade.data.animetrack.AnimeTrackRepositoryImpl
|
||||||
|
import eu.kanade.data.animeupdates.AnimeUpdatesRepositoryImpl
|
||||||
import eu.kanade.data.category.CategoryRepositoryImpl
|
import eu.kanade.data.category.CategoryRepositoryImpl
|
||||||
import eu.kanade.data.category.CategoryRepositoryImplAnime
|
import eu.kanade.data.category.CategoryRepositoryImplAnime
|
||||||
import eu.kanade.data.chapter.ChapterRepositoryImpl
|
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.SourceDataRepositoryImpl
|
||||||
import eu.kanade.data.source.SourceRepositoryImpl
|
import eu.kanade.data.source.SourceRepositoryImpl
|
||||||
import eu.kanade.data.track.TrackRepositoryImpl
|
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.GetAnime
|
||||||
import eu.kanade.domain.anime.interactor.GetAnimeWithEpisodes
|
import eu.kanade.domain.anime.interactor.GetAnimeWithEpisodes
|
||||||
import eu.kanade.domain.anime.interactor.GetAnimelibAnime
|
import eu.kanade.domain.anime.interactor.GetAnimelibAnime
|
||||||
import eu.kanade.domain.anime.interactor.GetDuplicateLibraryAnime
|
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.SetAnimeEpisodeFlags
|
||||||
import eu.kanade.domain.anime.interactor.SetAnimeViewerFlags
|
import eu.kanade.domain.anime.interactor.SetAnimeViewerFlags
|
||||||
import eu.kanade.domain.anime.interactor.UpdateAnime
|
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.animedownload.interactor.DeleteAnimeDownload
|
||||||
import eu.kanade.domain.animeextension.interactor.GetAnimeExtensionLanguages
|
import eu.kanade.domain.animeextension.interactor.GetAnimeExtensionLanguages
|
||||||
import eu.kanade.domain.animeextension.interactor.GetAnimeExtensionSources
|
import eu.kanade.domain.animeextension.interactor.GetAnimeExtensionSources
|
||||||
import eu.kanade.domain.animeextension.interactor.GetAnimeExtensionUpdates
|
import eu.kanade.domain.animeextension.interactor.GetAnimeExtensionsByType
|
||||||
import eu.kanade.domain.animeextension.interactor.GetAnimeExtensions
|
import eu.kanade.domain.animehistory.interactor.DeleteAllAnimeHistory
|
||||||
import eu.kanade.domain.animehistory.interactor.DeleteAnimeHistoryTable
|
|
||||||
import eu.kanade.domain.animehistory.interactor.GetAnimeHistory
|
import eu.kanade.domain.animehistory.interactor.GetAnimeHistory
|
||||||
import eu.kanade.domain.animehistory.interactor.GetNextEpisode
|
import eu.kanade.domain.animehistory.interactor.GetNextEpisode
|
||||||
import eu.kanade.domain.animehistory.interactor.RemoveAnimeHistoryByAnimeId
|
import eu.kanade.domain.animehistory.interactor.RemoveAnimeHistoryByAnimeId
|
||||||
import eu.kanade.domain.animehistory.interactor.RemoveAnimeHistoryById
|
import eu.kanade.domain.animehistory.interactor.RemoveAnimeHistoryById
|
||||||
import eu.kanade.domain.animehistory.interactor.UpsertAnimeHistory
|
import eu.kanade.domain.animehistory.interactor.UpsertAnimeHistory
|
||||||
import eu.kanade.domain.animehistory.repository.AnimeHistoryRepository
|
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.GetAnimeSourcesWithFavoriteCount
|
||||||
import eu.kanade.domain.animesource.interactor.GetAnimeSourcesWithNonLibraryAnime
|
import eu.kanade.domain.animesource.interactor.GetAnimeSourcesWithNonLibraryAnime
|
||||||
import eu.kanade.domain.animesource.interactor.GetEnabledAnimeSources
|
import eu.kanade.domain.animesource.interactor.GetEnabledAnimeSources
|
||||||
import eu.kanade.domain.animesource.interactor.GetLanguagesWithAnimeSources
|
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.ToggleAnimeSource
|
||||||
import eu.kanade.domain.animesource.interactor.ToggleAnimeSourcePin
|
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.animesource.repository.AnimeSourceRepository
|
||||||
import eu.kanade.domain.animetrack.interactor.DeleteAnimeTrack
|
import eu.kanade.domain.animetrack.interactor.DeleteAnimeTrack
|
||||||
import eu.kanade.domain.animetrack.interactor.GetAnimeTracks
|
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.interactor.InsertAnimeTrack
|
||||||
import eu.kanade.domain.animetrack.repository.AnimeTrackRepository
|
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.CreateCategoryWithName
|
||||||
|
import eu.kanade.domain.category.interactor.DeleteAnimeCategory
|
||||||
import eu.kanade.domain.category.interactor.DeleteCategory
|
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.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.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.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.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.SetDisplayModeForAnimeCategory
|
||||||
|
import eu.kanade.domain.category.interactor.SetDisplayModeForCategory
|
||||||
import eu.kanade.domain.category.interactor.SetMangaCategories
|
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.SetSortModeForCategory
|
||||||
|
import eu.kanade.domain.category.interactor.UpdateAnimeCategory
|
||||||
import eu.kanade.domain.category.interactor.UpdateCategory
|
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.CategoryRepository
|
||||||
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
|
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
|
||||||
import eu.kanade.domain.chapter.interactor.GetChapter
|
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.download.interactor.DeleteDownload
|
||||||
import eu.kanade.domain.episode.interactor.GetEpisode
|
import eu.kanade.domain.episode.interactor.GetEpisode
|
||||||
import eu.kanade.domain.episode.interactor.GetEpisodeByAnimeId
|
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.SetSeenStatus
|
||||||
import eu.kanade.domain.episode.interactor.ShouldUpdateDbEpisode
|
import eu.kanade.domain.episode.interactor.ShouldUpdateDbEpisode
|
||||||
import eu.kanade.domain.episode.interactor.SyncEpisodesWithSource
|
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.addFactory
|
||||||
import uy.kohesive.injekt.api.addSingletonFactory
|
import uy.kohesive.injekt.api.addSingletonFactory
|
||||||
import uy.kohesive.injekt.api.get
|
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
|
import eu.kanade.domain.anime.interactor.ResetViewerFlags as ResetViewerFlagsAnime
|
||||||
|
|
||||||
class DomainModule : InjektModule {
|
class DomainModule : InjektModule {
|
||||||
|
|
||||||
override fun InjektRegistrar.registerInjectables() {
|
override fun InjektRegistrar.registerInjectables() {
|
||||||
|
|
||||||
addSingletonFactory<CategoryRepositoryAnime> { CategoryRepositoryImplAnime(get()) }
|
addSingletonFactory<CategoryRepositoryAnime> { CategoryRepositoryImplAnime(get()) }
|
||||||
addFactory { GetCategoriesAnime(get()) }
|
addFactory { GetAnimeCategories(get()) }
|
||||||
addFactory { ResetAnimeCategoryFlags(get(), get()) }
|
addFactory { ResetAnimeCategoryFlags(get(), get()) }
|
||||||
addFactory { SetDisplayModeForAnimeCategory(get(), get()) }
|
addFactory { SetDisplayModeForAnimeCategory(get(), get()) }
|
||||||
addFactory { SetSortModeForAnimeCategory(get(), get()) }
|
addFactory { SetSortModeForAnimeCategory(get(), get()) }
|
||||||
|
@ -190,7 +197,7 @@ class DomainModule : InjektModule {
|
||||||
|
|
||||||
addSingletonFactory<AnimeTrackRepository> { AnimeTrackRepositoryImpl(get()) }
|
addSingletonFactory<AnimeTrackRepository> { AnimeTrackRepositoryImpl(get()) }
|
||||||
addFactory { DeleteAnimeTrack(get()) }
|
addFactory { DeleteAnimeTrack(get()) }
|
||||||
addFactory { GetAnimeTracksPerAnime(get())
|
addFactory { GetTracksPerAnime(get()) }
|
||||||
addFactory { GetAnimeTracks(get()) }
|
addFactory { GetAnimeTracks(get()) }
|
||||||
addFactory { InsertAnimeTrack(get()) }
|
addFactory { InsertAnimeTrack(get()) }
|
||||||
|
|
||||||
|
@ -200,15 +207,6 @@ class DomainModule : InjektModule {
|
||||||
addFactory { GetTracks(get()) }
|
addFactory { GetTracks(get()) }
|
||||||
addFactory { InsertTrack(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()) }
|
addSingletonFactory<EpisodeRepository> { EpisodeRepositoryImpl(get()) }
|
||||||
addFactory { GetEpisode(get()) }
|
addFactory { GetEpisode(get()) }
|
||||||
addFactory { GetEpisodeByAnimeId(get()) }
|
addFactory { GetEpisodeByAnimeId(get()) }
|
||||||
|
@ -218,6 +216,15 @@ class DomainModule : InjektModule {
|
||||||
addFactory { SyncEpisodesWithSource(get(), get(), get(), get()) }
|
addFactory { SyncEpisodesWithSource(get(), get(), get(), get()) }
|
||||||
addFactory { SyncEpisodesWithTrackServiceTwoWay(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()) }
|
addSingletonFactory<AnimeHistoryRepository> { AnimeHistoryRepositoryImpl(get()) }
|
||||||
addFactory { DeleteAllAnimeHistory(get()) }
|
addFactory { DeleteAllAnimeHistory(get()) }
|
||||||
addFactory { GetAnimeHistory(get()) }
|
addFactory { GetAnimeHistory(get()) }
|
||||||
|
@ -257,8 +264,6 @@ class DomainModule : InjektModule {
|
||||||
addFactory { GetRemoteAnime(get()) }
|
addFactory { GetRemoteAnime(get()) }
|
||||||
addFactory { GetAnimeSourcesWithFavoriteCount(get(), get()) }
|
addFactory { GetAnimeSourcesWithFavoriteCount(get(), get()) }
|
||||||
addFactory { GetAnimeSourcesWithNonLibraryAnime(get()) }
|
addFactory { GetAnimeSourcesWithNonLibraryAnime(get()) }
|
||||||
addFactory { SetAnimeMigrateSorting(get()) }
|
|
||||||
addFactory { ToggleAnimeLanguage(get()) }
|
|
||||||
addFactory { ToggleAnimeSource(get()) }
|
addFactory { ToggleAnimeSource(get()) }
|
||||||
addFactory { ToggleAnimeSourcePin(get()) }
|
addFactory { ToggleAnimeSourcePin(get()) }
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ class GetAnime(
|
||||||
return animeRepository.getAnimeByIdAsFlow(id)
|
return animeRepository.getAnimeByIdAsFlow(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun await(url: String, sourceId: Long): Anime? {
|
fun subscribe(url: String, sourceId: Long): Flow<Anime?> {
|
||||||
return animeRepository.getAnimeByUrlAndSourceId(url, sourceId)
|
return animeRepository.getAnimeByUrlAndSourceIdAsFlow(url, sourceId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import eu.kanade.domain.anime.model.Anime
|
||||||
import eu.kanade.domain.anime.repository.AnimeRepository
|
import eu.kanade.domain.anime.repository.AnimeRepository
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
class GetFavorites(
|
class GetAnimeFavorites(
|
||||||
private val animeRepository: AnimeRepository,
|
private val animeRepository: AnimeRepository,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -24,4 +24,8 @@ class GetAnimeWithEpisodes(
|
||||||
suspend fun awaitAnime(id: Long): Anime {
|
suspend fun awaitAnime(id: Long): Anime {
|
||||||
return animeRepository.getAnimeById(id)
|
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
|
package eu.kanade.domain.anime.interactor
|
||||||
|
|
||||||
import eu.kanade.domain.anime.repository.AnimeRepository
|
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
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
class GetAnimelibAnime(
|
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.isLocal
|
||||||
import eu.kanade.domain.anime.model.toDbAnime
|
import eu.kanade.domain.anime.model.toDbAnime
|
||||||
import eu.kanade.domain.anime.repository.AnimeRepository
|
import eu.kanade.domain.anime.repository.AnimeRepository
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
|
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
|
||||||
import tachiyomi.animesource.model.AnimeInfo
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
@ -20,23 +20,30 @@ class UpdateAnime(
|
||||||
return animeRepository.update(animeUpdate)
|
return animeRepository.update(animeUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitAll(values: List<AnimeUpdate>): Boolean {
|
suspend fun awaitAll(animeUpdates: List<AnimeUpdate>): Boolean {
|
||||||
return animeRepository.updateAll(values)
|
return animeRepository.updateAll(animeUpdates)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitUpdateFromSource(
|
suspend fun awaitUpdateFromSource(
|
||||||
localAnime: Anime,
|
localAnime: Anime,
|
||||||
remoteAnime: AnimeInfo,
|
remoteAnime: SAnime,
|
||||||
manualFetch: Boolean,
|
manualFetch: Boolean,
|
||||||
coverCache: AnimeCoverCache = Injekt.get(),
|
coverCache: AnimeCoverCache = Injekt.get(),
|
||||||
): Boolean {
|
): Boolean {
|
||||||
// if the anime isn't a favorite, set its title from source and update in db
|
val remoteTitle = try {
|
||||||
val title = if (!localAnime.favorite) remoteAnime.title else null
|
remoteAnime.title
|
||||||
|
} catch (_: UninitializedPropertyAccessException) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
// Never refresh covers if the url is empty to avoid "losing" existing covers
|
// if the anime isn't a favorite, set its title from source and update in db
|
||||||
val updateCover = remoteAnime.cover.isNotEmpty() && (manualFetch || localAnime.thumbnailUrl != remoteAnime.cover)
|
val title = if (remoteTitle.isEmpty() || localAnime.favorite) null else remoteTitle
|
||||||
val coverLastModified = if (updateCover) {
|
|
||||||
|
val coverLastModified =
|
||||||
when {
|
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.isLocal() -> Date().time
|
||||||
localAnime.hasCustomCover(coverCache) -> {
|
localAnime.hasCustomCover(coverCache) -> {
|
||||||
coverCache.deleteFromCache(localAnime.toDbAnime(), false)
|
coverCache.deleteFromCache(localAnime.toDbAnime(), false)
|
||||||
|
@ -47,19 +54,21 @@ class UpdateAnime(
|
||||||
Date().time
|
Date().time
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else null
|
|
||||||
|
val thumbnailUrl = remoteAnime.thumbnail_url?.takeIf { it.isNotEmpty() }
|
||||||
|
|
||||||
return animeRepository.update(
|
return animeRepository.update(
|
||||||
AnimeUpdate(
|
AnimeUpdate(
|
||||||
id = localAnime.id,
|
id = localAnime.id,
|
||||||
title = title?.takeIf { it.isNotEmpty() },
|
title = title,
|
||||||
coverLastModified = coverLastModified,
|
coverLastModified = coverLastModified,
|
||||||
author = remoteAnime.author,
|
author = remoteAnime.author,
|
||||||
artist = remoteAnime.artist,
|
artist = remoteAnime.artist,
|
||||||
description = remoteAnime.description,
|
description = remoteAnime.description,
|
||||||
genre = remoteAnime.genres,
|
genre = remoteAnime.getGenres(),
|
||||||
thumbnailUrl = remoteAnime.cover.takeIf { it.isNotEmpty() },
|
thumbnailUrl = thumbnailUrl,
|
||||||
status = remoteAnime.status.toLong(),
|
status = remoteAnime.status.toLong(),
|
||||||
|
updateStrategy = remoteAnime.update_strategy,
|
||||||
initialized = true,
|
initialized = true,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
package eu.kanade.domain.anime.model
|
package eu.kanade.domain.anime.model
|
||||||
|
|
||||||
import eu.kanade.data.listOfStringsAdapter
|
import eu.kanade.data.listOfStringsAdapter
|
||||||
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
|
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.AnimeImpl
|
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.LocalSource
|
||||||
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
import tachiyomi.animesource.model.AnimeInfo
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
@ -30,6 +30,7 @@ data class Anime(
|
||||||
val genre: List<String>?,
|
val genre: List<String>?,
|
||||||
val status: Long,
|
val status: Long,
|
||||||
val thumbnailUrl: String?,
|
val thumbnailUrl: String?,
|
||||||
|
val updateStrategy: UpdateStrategy,
|
||||||
val initialized: Boolean,
|
val initialized: Boolean,
|
||||||
) : Serializable {
|
) : Serializable {
|
||||||
|
|
||||||
|
@ -79,7 +80,7 @@ data class Anime(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun forceDownloaded(): Boolean {
|
fun forceDownloaded(): Boolean {
|
||||||
return favorite && Injekt.get<PreferencesHelper>().downloadedOnly().get()
|
return favorite && Injekt.get<BasePreferences>().downloadedOnly().get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sortDescending(): Boolean {
|
fun sortDescending(): Boolean {
|
||||||
|
@ -98,6 +99,28 @@ data class Anime(
|
||||||
it.initialized = initialized
|
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 {
|
companion object {
|
||||||
// Generic filter that does not filter anything
|
// Generic filter that does not filter anything
|
||||||
const val SHOW_ALL = 0x00000000L
|
const val SHOW_ALL = 0x00000000L
|
||||||
|
@ -133,17 +156,18 @@ data class Anime(
|
||||||
title = "",
|
title = "",
|
||||||
source = -1L,
|
source = -1L,
|
||||||
favorite = false,
|
favorite = false,
|
||||||
lastUpdate = -1L,
|
lastUpdate = 0L,
|
||||||
dateAdded = -1L,
|
dateAdded = 0L,
|
||||||
viewerFlags = -1L,
|
viewerFlags = 0L,
|
||||||
episodeFlags = -1L,
|
episodeFlags = 0L,
|
||||||
coverLastModified = -1L,
|
coverLastModified = 0L,
|
||||||
artist = null,
|
artist = null,
|
||||||
author = null,
|
author = null,
|
||||||
description = null,
|
description = null,
|
||||||
genre = null,
|
genre = null,
|
||||||
status = 0L,
|
status = 0L,
|
||||||
thumbnailUrl = null,
|
thumbnailUrl = null,
|
||||||
|
updateStrategy = UpdateStrategy.ALWAYS_UPDATE,
|
||||||
initialized = false,
|
initialized = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -181,20 +205,10 @@ fun Anime.toDbAnime(): DbAnime = AnimeImpl().also {
|
||||||
it.genre = genre?.let(listOfStringsAdapter::encode)
|
it.genre = genre?.let(listOfStringsAdapter::encode)
|
||||||
it.status = status.toInt()
|
it.status = status.toInt()
|
||||||
it.thumbnail_url = thumbnailUrl
|
it.thumbnail_url = thumbnailUrl
|
||||||
|
it.update_strategy = updateStrategy
|
||||||
it.initialized = initialized
|
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 {
|
fun Anime.toAnimeUpdate(): AnimeUpdate {
|
||||||
return AnimeUpdate(
|
return AnimeUpdate(
|
||||||
id = id,
|
id = id,
|
||||||
|
@ -213,6 +227,22 @@ fun Anime.toAnimeUpdate(): AnimeUpdate {
|
||||||
genre = genre,
|
genre = genre,
|
||||||
status = status,
|
status = status,
|
||||||
thumbnailUrl = thumbnailUrl,
|
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,
|
initialized = initialized,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package eu.kanade.domain.anime.model
|
package eu.kanade.domain.anime.model
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
|
|
||||||
data class AnimeUpdate(
|
data class AnimeUpdate(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val source: Long? = null,
|
val source: Long? = null,
|
||||||
|
@ -17,5 +19,6 @@ data class AnimeUpdate(
|
||||||
val genre: List<String>? = null,
|
val genre: List<String>? = null,
|
||||||
val status: Long? = null,
|
val status: Long? = null,
|
||||||
val thumbnailUrl: String? = null,
|
val thumbnailUrl: String? = null,
|
||||||
|
val updateStrategy: UpdateStrategy? = null,
|
||||||
val initialized: Boolean? = 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.Anime
|
||||||
import eu.kanade.domain.anime.model.AnimeUpdate
|
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
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface AnimeRepository {
|
interface AnimeRepository {
|
||||||
|
@ -13,6 +13,8 @@ interface AnimeRepository {
|
||||||
|
|
||||||
suspend fun getAnimeByUrlAndSourceId(url: String, sourceId: Long): Anime?
|
suspend fun getAnimeByUrlAndSourceId(url: String, sourceId: Long): Anime?
|
||||||
|
|
||||||
|
fun getAnimeByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow<Anime?>
|
||||||
|
|
||||||
suspend fun getFavorites(): List<Anime>
|
suspend fun getFavorites(): List<Anime>
|
||||||
|
|
||||||
suspend fun getAnimelibAnime(): List<AnimelibAnime>
|
suspend fun getAnimelibAnime(): List<AnimelibAnime>
|
||||||
|
@ -31,5 +33,5 @@ interface AnimeRepository {
|
||||||
|
|
||||||
suspend fun update(update: AnimeUpdate): Boolean
|
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.anime.model.Anime
|
||||||
import eu.kanade.domain.episode.model.Episode
|
import eu.kanade.domain.episode.model.Episode
|
||||||
|
import eu.kanade.domain.episode.model.toDbEpisode
|
||||||
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
|
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
|
||||||
import eu.kanade.tachiyomi.data.download.AnimeDownloadManager
|
import eu.kanade.tachiyomi.data.animedownload.AnimeDownloadManager
|
||||||
import kotlinx.coroutines.NonCancellable
|
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class DeleteAnimeDownload(
|
class DeleteAnimeDownload(
|
||||||
private val sourceManager: AnimeSourceManager,
|
private val sourceManager: AnimeSourceManager,
|
||||||
private val downloadManager: AnimeDownloadManager,
|
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 ->
|
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
|
package eu.kanade.domain.animeextension.interactor
|
||||||
|
|
||||||
import eu.kanade.core.util.asFlow
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.animeextension.AnimeExtensionManager
|
||||||
import eu.kanade.tachiyomi.extension.AnimeExtensionManager
|
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
|
||||||
class GetAnimeExtensionLanguages(
|
class GetAnimeExtensionLanguages(
|
||||||
private val preferences: PreferencesHelper,
|
private val preferences: SourcePreferences,
|
||||||
private val extensionManager: AnimeExtensionManager,
|
private val extensionManager: AnimeExtensionManager,
|
||||||
) {
|
) {
|
||||||
fun subscribe(): Flow<List<String>> {
|
fun subscribe(): Flow<List<String>> {
|
||||||
return combine(
|
return combine(
|
||||||
preferences.enabledLanguages().asFlow(),
|
preferences.enabledLanguages().changes(),
|
||||||
extensionManager.getAvailableExtensionsObservable().asFlow(),
|
extensionManager.availableExtensionsFlow,
|
||||||
) { enabledLanguage, availableExtensions ->
|
) { enabledLanguage, availableExtensions ->
|
||||||
availableExtensions
|
availableExtensions
|
||||||
.map { it.lang }
|
.map { it.lang }
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
package eu.kanade.domain.animeextension.interactor
|
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.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 eu.kanade.tachiyomi.ui.browse.animeextension.details.AnimeExtensionSourceItem
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
class GetAnimeExtensionSources(
|
class GetAnimeExtensionSources(
|
||||||
private val preferences: PreferencesHelper,
|
private val preferences: SourcePreferences,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun subscribe(extension: AnimeExtension.Installed): Flow<List<AnimeExtensionSourceItem>> {
|
fun subscribe(extension: AnimeExtension.Installed): Flow<List<AnimeExtensionSourceItem>> {
|
||||||
|
@ -16,7 +16,7 @@ class GetAnimeExtensionSources(
|
||||||
val isMultiLangSingleSource =
|
val isMultiLangSingleSource =
|
||||||
isMultiSource && extension.sources.map { it.name }.distinct().size == 1
|
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
|
fun AnimeSource.isEnabled() = id.toString() !in disabledSources
|
||||||
|
|
||||||
extension.sources
|
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
|
package eu.kanade.domain.animeextension.interactor
|
||||||
|
|
||||||
import eu.kanade.core.util.asFlow
|
import eu.kanade.domain.animeextension.model.AnimeExtensions
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.extension.AnimeExtensionManager
|
import eu.kanade.tachiyomi.animeextension.AnimeExtensionManager
|
||||||
import eu.kanade.tachiyomi.extension.model.AnimeExtension
|
import eu.kanade.tachiyomi.animeextension.model.AnimeExtension
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
|
||||||
typealias ExtensionSegregation = Triple<List<AnimeExtension.Installed>, List<AnimeExtension.Untrusted>, List<AnimeExtension.Available>>
|
class GetAnimeExtensionsByType(
|
||||||
|
private val preferences: SourcePreferences,
|
||||||
class GetAnimeExtensions(
|
|
||||||
private val preferences: PreferencesHelper,
|
|
||||||
private val extensionManager: AnimeExtensionManager,
|
private val extensionManager: AnimeExtensionManager,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun subscribe(): Flow<ExtensionSegregation> {
|
fun subscribe(): Flow<AnimeExtensions> {
|
||||||
val showNsfwSources = preferences.showNsfwSource().get()
|
val showNsfwSources = preferences.showNsfwSource().get()
|
||||||
|
|
||||||
return combine(
|
return combine(
|
||||||
preferences.enabledLanguages().asFlow(),
|
preferences.enabledLanguages().changes(),
|
||||||
extensionManager.getInstalledExtensionsObservable().asFlow(),
|
extensionManager.installedExtensionsFlow,
|
||||||
extensionManager.getUntrustedExtensionsObservable().asFlow(),
|
extensionManager.untrustedExtensionsFlow,
|
||||||
extensionManager.getAvailableExtensionsObservable().asFlow(),
|
extensionManager.availableExtensionsFlow,
|
||||||
) { _activeLanguages, _installed, _untrusted, _available ->
|
) { _activeLanguages, _installed, _untrusted, _available ->
|
||||||
|
val (updates, installed) = _installed
|
||||||
val installed = _installed
|
.filter { (showNsfwSources || it.isNsfw.not()) }
|
||||||
.filter { it.hasUpdate.not() && (showNsfwSources || it.isNsfw.not()) }
|
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
compareBy<AnimeExtension.Installed> { it.isObsolete.not() }
|
compareBy<AnimeExtension.Installed> { it.isObsolete.not() }
|
||||||
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
|
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
|
||||||
)
|
)
|
||||||
|
.partition { it.hasUpdate }
|
||||||
|
|
||||||
val untrusted = _untrusted
|
val untrusted = _untrusted
|
||||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||||
|
@ -38,11 +36,11 @@ class GetAnimeExtensions(
|
||||||
.filter { extension ->
|
.filter { extension ->
|
||||||
_installed.none { it.pkgName == extension.pkgName } &&
|
_installed.none { it.pkgName == extension.pkgName } &&
|
||||||
_untrusted.none { it.pkgName == extension.pkgName } &&
|
_untrusted.none { it.pkgName == extension.pkgName } &&
|
||||||
extension.lang in _activeLanguages &&
|
|
||||||
(showNsfwSources || extension.isNsfw.not())
|
(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
|
import eu.kanade.domain.animehistory.repository.AnimeHistoryRepository
|
||||||
|
|
||||||
class DeleteAnimeHistoryTable(
|
class DeleteAllAnimeHistory(
|
||||||
private val repository: AnimeHistoryRepository,
|
private val repository: AnimeHistoryRepository,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
package eu.kanade.domain.animehistory.interactor
|
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.model.AnimeHistoryWithRelations
|
||||||
import eu.kanade.domain.animehistory.repository.AnimeHistoryRepository
|
import eu.kanade.domain.animehistory.repository.AnimeHistoryRepository
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
@ -11,11 +8,7 @@ class GetAnimeHistory(
|
||||||
private val repository: AnimeHistoryRepository,
|
private val repository: AnimeHistoryRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun subscribe(query: String): Flow<PagingData<AnimeHistoryWithRelations>> {
|
fun subscribe(query: String): Flow<List<AnimeHistoryWithRelations>> {
|
||||||
return Pager(
|
return repository.getHistory(query)
|
||||||
PagingConfig(pageSize = 25),
|
|
||||||
) {
|
|
||||||
repository.getHistory(query)
|
|
||||||
}.flow
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,51 @@
|
||||||
package eu.kanade.domain.animehistory.interactor
|
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.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.domain.episode.model.Episode
|
||||||
|
import eu.kanade.tachiyomi.util.episode.getEpisodeSort
|
||||||
|
|
||||||
class GetNextEpisode(
|
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? {
|
suspend fun await(): Episode? {
|
||||||
return repository.getNextEpisode(animeId, episodeId)
|
val history = historyRepository.getLastHistory() ?: return null
|
||||||
|
return await(history.animeId, history.episodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun await(): Episode? {
|
suspend fun await(animeId: Long, episodeId: Long): Episode? {
|
||||||
val history = repository.getLastHistory() ?: return null
|
val episode = getEpisode.await(episodeId) ?: return null
|
||||||
return repository.getNextEpisode(history.animeId, history.episodeId)
|
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
|
package eu.kanade.domain.animehistory.repository
|
||||||
|
|
||||||
import androidx.paging.PagingSource
|
|
||||||
import eu.kanade.domain.animehistory.model.AnimeHistoryUpdate
|
import eu.kanade.domain.animehistory.model.AnimeHistoryUpdate
|
||||||
import eu.kanade.domain.animehistory.model.AnimeHistoryWithRelations
|
import eu.kanade.domain.animehistory.model.AnimeHistoryWithRelations
|
||||||
import eu.kanade.domain.episode.model.Episode
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface AnimeHistoryRepository {
|
interface AnimeHistoryRepository {
|
||||||
|
|
||||||
fun getHistory(query: String): PagingSource<Long, AnimeHistoryWithRelations>
|
fun getHistory(query: String): Flow<List<AnimeHistoryWithRelations>>
|
||||||
|
|
||||||
suspend fun getLastHistory(): AnimeHistoryWithRelations?
|
suspend fun getLastHistory(): AnimeHistoryWithRelations?
|
||||||
|
|
||||||
suspend fun getNextEpisode(animeId: Long, episodeId: Long): Episode?
|
|
||||||
|
|
||||||
suspend fun resetHistory(historyId: Long)
|
suspend fun resetHistory(historyId: Long)
|
||||||
|
|
||||||
suspend fun resetHistoryByAnimeId(animeId: 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.model.AnimeSource
|
||||||
import eu.kanade.domain.animesource.repository.AnimeSourceRepository
|
import eu.kanade.domain.animesource.repository.AnimeSourceRepository
|
||||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
|
@ -12,13 +12,13 @@ import java.util.Locale
|
||||||
|
|
||||||
class GetAnimeSourcesWithFavoriteCount(
|
class GetAnimeSourcesWithFavoriteCount(
|
||||||
private val repository: AnimeSourceRepository,
|
private val repository: AnimeSourceRepository,
|
||||||
private val preferences: PreferencesHelper,
|
private val preferences: SourcePreferences,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun subscribe(): Flow<List<Pair<AnimeSource, Long>>> {
|
fun subscribe(): Flow<List<Pair<AnimeSource, Long>>> {
|
||||||
return combine(
|
return combine(
|
||||||
preferences.migrationSortingDirection().asFlow(),
|
preferences.migrationSortingDirection().changes(),
|
||||||
preferences.migrationSortingMode().asFlow(),
|
preferences.migrationSortingMode().changes(),
|
||||||
repository.getSourcesWithFavoriteCount(),
|
repository.getSourcesWithFavoriteCount(),
|
||||||
) { direction, mode, list ->
|
) { direction, mode, list ->
|
||||||
list.sortedWith(sortFn(direction, mode))
|
list.sortedWith(sortFn(direction, mode))
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
package eu.kanade.domain.animesource.interactor
|
package eu.kanade.domain.animesource.interactor
|
||||||
|
|
||||||
|
import eu.kanade.domain.animesource.model.AnimeSourceWithCount
|
||||||
import eu.kanade.domain.animesource.repository.AnimeSourceRepository
|
import eu.kanade.domain.animesource.repository.AnimeSourceRepository
|
||||||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
class GetAnimeSourcesWithNonLibraryAnime(
|
class GetAnimeSourcesWithNonLibraryAnime(
|
||||||
private val repository: AnimeSourceRepository,
|
private val repository: AnimeSourceRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun subscribe(): Flow<List<Pair<AnimeSource, Long>>> {
|
fun subscribe(): Flow<List<AnimeSourceWithCount>> {
|
||||||
return repository.getSourcesWithNonLibraryAnime()
|
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.Pin
|
||||||
import eu.kanade.domain.animesource.model.Pins
|
import eu.kanade.domain.animesource.model.Pins
|
||||||
import eu.kanade.domain.animesource.repository.AnimeSourceRepository
|
import eu.kanade.domain.animesource.repository.AnimeSourceRepository
|
||||||
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.animesource.LocalAnimeSource
|
import eu.kanade.tachiyomi.animesource.LocalAnimeSource
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
|
||||||
class GetEnabledAnimeSources(
|
class GetEnabledAnimeSources(
|
||||||
private val repository: AnimeSourceRepository,
|
private val repository: AnimeSourceRepository,
|
||||||
private val preferences: PreferencesHelper,
|
private val preferences: SourcePreferences,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun subscribe(): Flow<List<AnimeSource>> {
|
fun subscribe(): Flow<List<AnimeSource>> {
|
||||||
return combine(
|
return combine(
|
||||||
preferences.pinnedAnimeSources().asFlow(),
|
preferences.pinnedAnimeSources().changes(),
|
||||||
preferences.enabledLanguages().asFlow(),
|
preferences.enabledLanguages().changes(),
|
||||||
preferences.disabledAnimeSources().asFlow(),
|
preferences.disabledAnimeSources().changes(),
|
||||||
preferences.lastUsedAnimeSource().asFlow(),
|
preferences.lastUsedAnimeSource().changes(),
|
||||||
repository.getSources(),
|
repository.getSources(),
|
||||||
) { pinnedSourceIds, enabledLanguages, disabledSources, lastUsedSource, sources ->
|
) { pinnedSourceIds, enabledLanguages, disabledSources, lastUsedSource, sources ->
|
||||||
val duplicatePins = preferences.duplicatePinnedSources().get()
|
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.model.AnimeSource
|
||||||
import eu.kanade.domain.animesource.repository.AnimeSourceRepository
|
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 eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
|
||||||
class GetLanguagesWithAnimeSources(
|
class GetLanguagesWithAnimeSources(
|
||||||
private val repository: AnimeSourceRepository,
|
private val repository: AnimeSourceRepository,
|
||||||
private val preferences: PreferencesHelper,
|
private val preferences: SourcePreferences,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun subscribe(): Flow<Map<String, List<AnimeSource>>> {
|
fun subscribe(): Flow<Map<String, List<AnimeSource>>> {
|
||||||
return combine(
|
return combine(
|
||||||
preferences.enabledLanguages().asFlow(),
|
preferences.enabledLanguages().changes(),
|
||||||
preferences.disabledSources().asFlow(),
|
preferences.disabledSources().changes(),
|
||||||
repository.getOnlineSources(),
|
repository.getOnlineSources(),
|
||||||
) { enabledLanguage, disabledSource, onlineSources ->
|
) { enabledLanguage, disabledSource, onlineSources ->
|
||||||
val sortedSources = onlineSources.sortedWith(
|
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
|
package eu.kanade.domain.animesource.interactor
|
||||||
|
|
||||||
import eu.kanade.domain.animesource.model.AnimeSource
|
import eu.kanade.domain.animesource.model.AnimeSource
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.util.preference.minusAssign
|
import eu.kanade.tachiyomi.core.preference.getAndSet
|
||||||
import eu.kanade.tachiyomi.util.preference.plusAssign
|
|
||||||
|
|
||||||
class ToggleAnimeSource(
|
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)
|
await(source.id, enable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun await(sourceId: Long, enable: Boolean = sourceId.toString() in preferences.disabledAnimeSources().get()) {
|
fun await(sourceId: Long, enable: Boolean = isEnabled(sourceId)) {
|
||||||
if (enable) {
|
preferences.disabledAnimeSources().getAndSet { disabled ->
|
||||||
preferences.disabledAnimeSources() -= sourceId.toString()
|
if (enable) disabled.minus("$sourceId") else disabled.plus("$sourceId")
|
||||||
} else {
|
|
||||||
preferences.disabledAnimeSources() += sourceId.toString()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isEnabled(sourceId: Long): Boolean {
|
||||||
|
return sourceId.toString() in preferences.disabledAnimeSources().get()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,17 @@
|
||||||
package eu.kanade.domain.animesource.interactor
|
package eu.kanade.domain.animesource.interactor
|
||||||
|
|
||||||
import eu.kanade.domain.animesource.model.AnimeSource
|
import eu.kanade.domain.animesource.model.AnimeSource
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.util.preference.minusAssign
|
import eu.kanade.tachiyomi.core.preference.getAndSet
|
||||||
import eu.kanade.tachiyomi.util.preference.plusAssign
|
|
||||||
|
|
||||||
class ToggleAnimeSourcePin(
|
class ToggleAnimeSourcePin(
|
||||||
private val preferences: PreferencesHelper,
|
private val preferences: SourcePreferences,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun await(source: AnimeSource) {
|
fun await(source: AnimeSource) {
|
||||||
val isPinned = source.id.toString() in preferences.pinnedAnimeSources().get()
|
val isPinned = source.id.toString() in preferences.pinnedAnimeSources().get()
|
||||||
if (isPinned) {
|
preferences.pinnedAnimeSources().getAndSet { pinned ->
|
||||||
preferences.pinnedAnimeSources() -= source.id.toString()
|
if (isPinned) pinned.minus("${source.id}") else pinned.plus("${source.id}")
|
||||||
} else {
|
|
||||||
preferences.pinnedAnimeSources() += source.id.toString()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 id: Long,
|
||||||
val lang: String,
|
val lang: String,
|
||||||
val name: 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.ImageBitmap
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
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.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
@ -17,8 +17,11 @@ data class AnimeSource(
|
||||||
val isUsedLast: Boolean = false,
|
val isUsedLast: Boolean = false,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val nameWithLanguage: String
|
val visualName: String
|
||||||
get() = "$name (${lang.uppercase()})"
|
get() = when {
|
||||||
|
lang.isEmpty() -> name
|
||||||
|
else -> "$name (${lang.uppercase()})"
|
||||||
|
}
|
||||||
|
|
||||||
val icon: ImageBitmap?
|
val icon: ImageBitmap?
|
||||||
get() {
|
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
|
package eu.kanade.domain.animesource.repository
|
||||||
|
|
||||||
import eu.kanade.domain.animesource.model.AnimeSource
|
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 kotlinx.coroutines.flow.Flow
|
||||||
import eu.kanade.tachiyomi.animesource.AnimeSource as LoadedAnimeSource
|
|
||||||
|
|
||||||
interface AnimeSourceRepository {
|
interface AnimeSourceRepository {
|
||||||
|
|
||||||
|
@ -13,9 +14,11 @@ interface AnimeSourceRepository {
|
||||||
|
|
||||||
fun getSourcesWithFavoriteCount(): Flow<List<Pair<AnimeSource, Long>>>
|
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>> {
|
fun subscribe(animeId: Long): Flow<List<AnimeTrack>> {
|
||||||
return animetrackRepository.getAnimeTracksByAnimeIdAsFlow(animeId)
|
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.preference.PreferenceStore
|
||||||
import eu.kanade.tachiyomi.core.provider.FolderProvider
|
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(
|
class BackupPreferences(
|
||||||
private val folderProvider: FolderProvider,
|
private val folderProvider: FolderProvider,
|
||||||
|
@ -13,4 +17,6 @@ class BackupPreferences(
|
||||||
fun numberOfBackups() = preferenceStore.getInt("backup_slots", 2)
|
fun numberOfBackups() = preferenceStore.getInt("backup_slots", 2)
|
||||||
|
|
||||||
fun backupInterval() = preferenceStore.getInt("backup_interval", 12)
|
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.core.preference.getEnum
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
|
||||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
|
||||||
|
|
||||||
class BasePreferences(
|
class BasePreferences(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
|
@ -26,5 +24,6 @@ class BasePreferences(
|
||||||
if (DeviceUtil.isMiui) PreferenceValues.ExtensionInstaller.LEGACY else PreferenceValues.ExtensionInstaller.PACKAGEINSTALLER,
|
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 eu.kanade.domain.category.repository.CategoryRepositoryAnime
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
class GetCategoriesAnime(
|
class GetAnimeCategories(
|
||||||
private val categoryRepository: CategoryRepositoryAnime,
|
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.model.CategoryUpdate
|
||||||
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
|
import eu.kanade.domain.category.repository.CategoryRepositoryAnime
|
||||||
|
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||||
|
|
||||||
class UpdateCategoryAnime(
|
class UpdateAnimeCategory(
|
||||||
private val categoryRepository: CategoryRepositoryAnime,
|
private val categoryRepository: CategoryRepositoryAnime,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(payload: CategoryUpdate): Result {
|
suspend fun await(payload: CategoryUpdate): Result = withNonCancellableContext {
|
||||||
return try {
|
try {
|
||||||
categoryRepository.update(payload)
|
categoryRepository.updatePartial(payload)
|
||||||
Result.Success
|
Result.Success
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.Error(e)
|
Result.Error(e)
|
|
@ -6,6 +6,8 @@ import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface CategoryRepositoryAnime {
|
interface CategoryRepositoryAnime {
|
||||||
|
|
||||||
|
suspend fun get(id: Long): Category?
|
||||||
|
|
||||||
suspend fun getAll(): List<Category>
|
suspend fun getAll(): List<Category>
|
||||||
|
|
||||||
fun getAllAsFlow(): Flow<List<Category>>
|
fun getAllAsFlow(): Flow<List<Category>>
|
||||||
|
@ -14,13 +16,13 @@ interface CategoryRepositoryAnime {
|
||||||
|
|
||||||
fun getCategoriesByAnimeIdAsFlow(animeId: Long): Flow<List<Category>>
|
fun getCategoriesByAnimeIdAsFlow(animeId: Long): Flow<List<Category>>
|
||||||
|
|
||||||
@Throws(DuplicateNameException::class)
|
suspend fun insert(category: Category)
|
||||||
suspend fun insert(name: String, order: Long)
|
|
||||||
|
|
||||||
@Throws(DuplicateNameException::class)
|
suspend fun updatePartial(update: CategoryUpdate)
|
||||||
suspend fun update(payload: CategoryUpdate)
|
|
||||||
|
suspend fun updatePartial(updates: List<CategoryUpdate>)
|
||||||
|
|
||||||
|
suspend fun updateAllFlags(flags: Long?)
|
||||||
|
|
||||||
suspend fun delete(categoryId: 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 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 saveChaptersAsCBZ() = preferenceStore.getBoolean("save_chapter_as_cbz", true)
|
||||||
|
|
||||||
fun splitTallImages() = preferenceStore.getBoolean("split_tall_images", false)
|
fun splitTallImages() = preferenceStore.getBoolean("split_tall_images", false)
|
||||||
|
|
||||||
fun autoDownloadWhileReading() = preferenceStore.getInt("auto_download_while_reading", 0)
|
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 removeAfterReadSlots() = preferenceStore.getInt("remove_after_read_slots", -1)
|
||||||
|
|
||||||
fun removeAfterMarkedAsRead() = preferenceStore.getBoolean("pref_remove_after_marked_as_read_key", false)
|
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 removeBookmarkedChapters() = preferenceStore.getBoolean("pref_remove_bookmarked", false)
|
||||||
|
|
||||||
fun removeExcludeCategories() = preferenceStore.getStringSet("remove_exclude_categories", emptySet())
|
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 downloadNewChapters() = preferenceStore.getBoolean("download_new", false)
|
||||||
|
fun downloadNewEpisodes() = preferenceStore.getBoolean("download_new_episode", false)
|
||||||
|
|
||||||
fun downloadNewChapterCategories() = preferenceStore.getStringSet("download_new_categories", emptySet())
|
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 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.model.Anime
|
||||||
import eu.kanade.domain.anime.repository.AnimeRepository
|
import eu.kanade.domain.anime.repository.AnimeRepository
|
||||||
import eu.kanade.domain.animedownload.interactor.DeleteAnimeDownload
|
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.Episode
|
||||||
import eu.kanade.domain.episode.model.EpisodeUpdate
|
import eu.kanade.domain.episode.model.EpisodeUpdate
|
||||||
import eu.kanade.domain.episode.repository.EpisodeRepository
|
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 eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.NonCancellable
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
|
||||||
class SetSeenStatus(
|
class SetSeenStatus(
|
||||||
private val preferences: PreferencesHelper,
|
private val downloadPreferences: DownloadPreferences,
|
||||||
private val deleteDownload: DeleteAnimeDownload,
|
private val deleteDownload: DeleteAnimeDownload,
|
||||||
private val animeRepository: AnimeRepository,
|
private val animeRepository: AnimeRepository,
|
||||||
private val episodeRepository: EpisodeRepository,
|
private val episodeRepository: EpisodeRepository,
|
||||||
|
@ -27,38 +26,33 @@ class SetSeenStatus(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun await(seen: Boolean, vararg values: Episode): Result = withContext(NonCancellable) f@{
|
suspend fun await(seen: Boolean, vararg episodes: Episode): Result = withNonCancellableContext {
|
||||||
val episodes = values.filterNot { it.seen == seen }
|
val episodesToUpdate = episodes.filter {
|
||||||
|
when (seen) {
|
||||||
if (episodes.isEmpty()) {
|
true -> !it.seen
|
||||||
return@f Result.NoEpisodes
|
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 {
|
try {
|
||||||
episodeRepository.updateAll(
|
episodeRepository.updateAll(
|
||||||
episodes.map { episode ->
|
episodesToUpdate.map { mapper(it, seen) },
|
||||||
mapper(episode, seen)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
return@f Result.InternalError(e)
|
return@withNonCancellableContext Result.InternalError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seen && preferences.removeAfterMarkedAsRead()) {
|
if (seen && downloadPreferences.removeAfterMarkedAsRead().get()) {
|
||||||
anime.forEach { anime ->
|
episodesToUpdate
|
||||||
|
.groupBy { it.animeId }
|
||||||
|
.forEach { (animeId, episodes) ->
|
||||||
deleteDownload.awaitAll(
|
deleteDownload.awaitAll(
|
||||||
anime = anime,
|
anime = animeRepository.getAnimeById(animeId),
|
||||||
values = episodes
|
episodes = episodes.toTypedArray(),
|
||||||
.filter { anime.id == it.animeId }
|
|
||||||
.toTypedArray(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,10 +60,10 @@ class SetSeenStatus(
|
||||||
Result.Success
|
Result.Success
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun await(animeId: Long, seen: Boolean): Result = withContext(NonCancellable) f@{
|
suspend fun await(animeId: Long, seen: Boolean): Result = withNonCancellableContext {
|
||||||
return@f await(
|
await(
|
||||||
seen = seen,
|
seen = seen,
|
||||||
values = episodeRepository
|
episodes = episodeRepository
|
||||||
.getEpisodeByAnimeId(animeId)
|
.getEpisodeByAnimeId(animeId)
|
||||||
.toTypedArray(),
|
.toTypedArray(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
package eu.kanade.domain.episode.interactor
|
package eu.kanade.domain.episode.interactor
|
||||||
|
|
||||||
|
import eu.kanade.data.episode.CleanupEpisodeName
|
||||||
import eu.kanade.data.episode.NoEpisodesException
|
import eu.kanade.data.episode.NoEpisodesException
|
||||||
import eu.kanade.domain.anime.interactor.UpdateAnime
|
import eu.kanade.domain.anime.interactor.UpdateAnime
|
||||||
import eu.kanade.domain.anime.model.Anime
|
import eu.kanade.domain.anime.model.Anime
|
||||||
import eu.kanade.domain.episode.model.Episode
|
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.model.toEpisodeUpdate
|
||||||
import eu.kanade.domain.episode.repository.EpisodeRepository
|
import eu.kanade.domain.episode.repository.EpisodeRepository
|
||||||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
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.model.SEpisode
|
||||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
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 eu.kanade.tachiyomi.util.episode.EpisodeRecognition
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
@ -20,6 +23,7 @@ import java.util.TreeSet
|
||||||
|
|
||||||
class SyncEpisodesWithSource(
|
class SyncEpisodesWithSource(
|
||||||
private val downloadManager: AnimeDownloadManager = Injekt.get(),
|
private val downloadManager: AnimeDownloadManager = Injekt.get(),
|
||||||
|
private val downloadProvider: AnimeDownloadProvider = Injekt.get(),
|
||||||
private val episodeRepository: EpisodeRepository = Injekt.get(),
|
private val episodeRepository: EpisodeRepository = Injekt.get(),
|
||||||
private val shouldUpdateDbEpisode: ShouldUpdateDbEpisode = Injekt.get(),
|
private val shouldUpdateDbEpisode: ShouldUpdateDbEpisode = Injekt.get(),
|
||||||
private val updateAnime: UpdateAnime = Injekt.get(),
|
private val updateAnime: UpdateAnime = Injekt.get(),
|
||||||
|
@ -27,12 +31,20 @@ class SyncEpisodesWithSource(
|
||||||
private val getEpisodeByAnimeId: GetEpisodeByAnimeId = Injekt.get(),
|
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(
|
suspend fun await(
|
||||||
rawSourceEpisodes: List<SEpisode>,
|
rawSourceEpisodes: List<SEpisode>,
|
||||||
anime: Anime,
|
anime: Anime,
|
||||||
source: AnimeSource,
|
source: AnimeSource,
|
||||||
): Pair<List<Episode>, List<Episode>> {
|
): List<Episode> {
|
||||||
if (rawSourceEpisodes.isEmpty() && source.id != LocalAnimeSource.ID) {
|
if (rawSourceEpisodes.isEmpty() && !source.isLocal()) {
|
||||||
throw NoEpisodesException()
|
throw NoEpisodesException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +53,7 @@ class SyncEpisodesWithSource(
|
||||||
.mapIndexed { i, sEpisode ->
|
.mapIndexed { i, sEpisode ->
|
||||||
Episode.create()
|
Episode.create()
|
||||||
.copyFromSEpisode(sEpisode)
|
.copyFromSEpisode(sEpisode)
|
||||||
|
.copy(name = CleanupEpisodeName.await(sEpisode.name, anime.title))
|
||||||
.copy(animeId = anime.id, sourceOrder = i.toLong())
|
.copy(animeId = anime.id, sourceOrder = i.toLong())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,8 +107,11 @@ class SyncEpisodesWithSource(
|
||||||
toAdd.add(toAddEpisode)
|
toAdd.add(toAddEpisode)
|
||||||
} else {
|
} else {
|
||||||
if (shouldUpdateDbEpisode.await(dbEpisode, episode)) {
|
if (shouldUpdateDbEpisode.await(dbEpisode, episode)) {
|
||||||
if (dbEpisode.name != episode.name && downloadManager.isEpisodeDownloaded(dbEpisode.name, dbEpisode.scanlator, anime.title, anime.source)) {
|
val shouldRenameEpisode = downloadProvider.isEpisodeDirNameChanged(dbEpisode, episode) &&
|
||||||
downloadManager.renameEpisode(source, anime, 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(
|
var toChangeEpisode = dbEpisode.copy(
|
||||||
name = episode.name,
|
name = episode.name,
|
||||||
|
@ -113,18 +129,18 @@ class SyncEpisodesWithSource(
|
||||||
|
|
||||||
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
||||||
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
||||||
return Pair(emptyList(), emptyList())
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
val reAdded = mutableListOf<Episode>()
|
val reAdded = mutableListOf<Episode>()
|
||||||
|
|
||||||
val deletedEpisodeNumbers = TreeSet<Float>()
|
val deletedEpisodeNumbers = TreeSet<Float>()
|
||||||
val deletedSeenEpisodeNumbers = TreeSet<Float>()
|
val deletedSeenEpisodeNumbers = TreeSet<Float>()
|
||||||
|
val deletedBookmarkedEpisodeNumbers = TreeSet<Float>()
|
||||||
|
|
||||||
toDelete.forEach { episode ->
|
toDelete.forEach { episode ->
|
||||||
if (episode.seen) {
|
if (episode.seen) deletedSeenEpisodeNumbers.add(episode.episodeNumber)
|
||||||
deletedSeenEpisodeNumbers.add(episode.episodeNumber)
|
if (episode.bookmark) deletedBookmarkedEpisodeNumbers.add(episode.episodeNumber)
|
||||||
}
|
|
||||||
deletedEpisodeNumbers.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
|
// 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.
|
// Sources MUST return the episodes from most to less recent, which is common.
|
||||||
|
|
||||||
var itemCount = toAdd.size
|
var itemCount = toAdd.size
|
||||||
var updatedToAdd = toAdd.map { toAddItem ->
|
var updatedToAdd = toAdd.map { toAddItem ->
|
||||||
var episode = toAddItem.copy(dateFetch = rightNow + itemCount--)
|
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(
|
||||||
episode = episode.copy(seen = true)
|
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
|
// Try to to use the fetch date of the original entry to not pollute 'Updates' tab
|
||||||
val oldDateFetch = deletedEpisodeNumberDateFetchMap[episode.episodeNumber]
|
deletedEpisodeNumberDateFetchMap[episode.episodeNumber]?.let {
|
||||||
oldDateFetch?.let {
|
|
||||||
episode = episode.copy(dateFetch = it)
|
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
|
// Note that last_update actually represents last time the episode list changed at all
|
||||||
updateAnime.awaitUpdateLastUpdate(anime.id)
|
updateAnime.awaitUpdateLastUpdate(anime.id)
|
||||||
|
|
||||||
@Suppress("ConvertArgumentToSet") // See tachiyomiorg/tachiyomi#6372.
|
val reAddedUrls = reAdded.map { it.url }.toHashSet()
|
||||||
return Pair(updatedToAdd.subtract(reAdded).toList(), toDelete.subtract(reAdded).toList())
|
|
||||||
|
return updatedToAdd.filterNot { it.url in reAddedUrls }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ data class Episode(
|
||||||
url = sEpisode.url,
|
url = sEpisode.url,
|
||||||
dateUpload = sEpisode.date_upload,
|
dateUpload = sEpisode.date_upload,
|
||||||
episodeNumber = sEpisode.episode_number,
|
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 getEpisodeByAnimeId(animeId: Long): List<Episode>
|
||||||
|
|
||||||
|
suspend fun getBookmarkedEpisodesByAnimeId(animeId: Long): List<Episode>
|
||||||
|
|
||||||
suspend fun getEpisodeById(id: Long): Episode?
|
suspend fun getEpisodeById(id: Long): Episode?
|
||||||
|
|
||||||
fun getEpisodeByAnimeIdAsFlow(animeId: Long): Flow<List<Episode>>
|
fun getEpisodeByAnimeIdAsFlow(animeId: Long): Flow<List<Episode>>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package eu.kanade.domain.library.service
|
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.LibraryDisplayMode
|
||||||
import eu.kanade.domain.library.model.LibrarySort
|
import eu.kanade.domain.library.model.LibrarySort
|
||||||
import eu.kanade.domain.manga.model.Manga
|
import eu.kanade.domain.manga.model.Manga
|
||||||
|
@ -13,6 +14,7 @@ import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
class LibraryPreferences(
|
class LibraryPreferences(
|
||||||
private val preferenceStore: PreferenceStore,
|
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)
|
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 showUpdatesNavBadge() = preferenceStore.getBoolean("library_update_show_tab_badge", false)
|
||||||
fun unreadUpdatesCount() = preferenceStore.getInt("library_unread_updates_count", 0)
|
fun unreadUpdatesCount() = preferenceStore.getInt("library_unread_updates_count", 0)
|
||||||
|
fun unseenUpdatesCount() = preferenceStore.getInt("library_unseen_updates_count", 0)
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Category
|
// region Category
|
||||||
|
|
||||||
fun defaultCategory() = preferenceStore.getInt("default_category", -1)
|
fun defaultCategory() = preferenceStore.getInt("default_category", -1)
|
||||||
|
fun defaultAnimeCategory() = preferenceStore.getInt("default_anime_category", -1)
|
||||||
|
|
||||||
fun lastUsedCategory() = preferenceStore.getInt("last_used_category", 0)
|
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 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 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 categorizedDisplaySettings() = preferenceStore.getBoolean("categorized_display", false)
|
||||||
|
|
||||||
fun libraryUpdateCategories() = preferenceStore.getStringSet("library_update_categories", emptySet())
|
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 libraryUpdateCategoriesExclude() = preferenceStore.getStringSet("library_update_categories_exclude", emptySet())
|
||||||
|
fun animelibUpdateCategoriesExclude() = preferenceStore.getStringSet("animelib_update_categories_exclude", emptySet())
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Chapter
|
// region Chapter
|
||||||
|
|
||||||
fun filterChapterByRead() = preferenceStore.getLong("default_chapter_filter_by_read", Manga.SHOW_ALL)
|
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 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 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
|
// and upload date
|
||||||
fun sortChapterBySourceOrNumber() = preferenceStore.getLong("default_chapter_sort_by_source_or_number", Manga.CHAPTER_SORTING_SOURCE)
|
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 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 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) {
|
fun setChapterSettingsDefault(manga: Manga) {
|
||||||
filterChapterByRead().set(manga.unreadFilterRaw)
|
filterChapterByRead().set(manga.unreadFilterRaw)
|
||||||
|
@ -105,6 +120,15 @@ class LibraryPreferences(
|
||||||
sortChapterByAscendingOrDescending().set(if (manga.sortDescending()) Manga.CHAPTER_SORT_DESC else Manga.CHAPTER_SORT_ASC)
|
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)
|
fun autoClearChapterCache() = preferenceStore.getBoolean("auto_clear_chapter_cache", false)
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
|
@ -16,12 +16,18 @@ class SourcePreferences(
|
||||||
|
|
||||||
fun disabledSources() = preferenceStore.getStringSet("hidden_catalogues", emptySet())
|
fun disabledSources() = preferenceStore.getStringSet("hidden_catalogues", emptySet())
|
||||||
|
|
||||||
|
fun disabledAnimeSources() = preferenceStore.getStringSet("hidden_anime_catalogues", emptySet())
|
||||||
|
|
||||||
fun pinnedSources() = preferenceStore.getStringSet("pinned_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 duplicatePinnedSources() = preferenceStore.getBoolean("duplicate_pinned_sources", false)
|
||||||
|
|
||||||
fun lastUsedSource() = preferenceStore.getLong("last_catalogue_source", -1)
|
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 showNsfwSource() = preferenceStore.getBoolean("show_nsfw_source", true)
|
||||||
|
|
||||||
fun migrationSortingMode() = preferenceStore.getEnum("pref_migration_sorting", SetMigrateSorting.Mode.ALPHABETICAL)
|
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 extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)
|
||||||
|
|
||||||
|
fun animeextensionUpdatesCount() = preferenceStore.getInt("animeext_updates_count", 0)
|
||||||
|
|
||||||
fun trustedSignatures() = preferenceStore.getStringSet("trusted_signatures", emptySet())
|
fun trustedSignatures() = preferenceStore.getStringSet("trusted_signatures", emptySet())
|
||||||
|
|
||||||
fun searchPinnedSourcesOnly() = preferenceStore.getBoolean("search_pinned_sources_only", false)
|
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.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
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.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
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.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.systemBars
|
import androidx.compose.foundation.layout.systemBars
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
@ -36,72 +30,54 @@ import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
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.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.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.toMutableStateList
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.res.stringResource
|
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.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.AnimeEpisodeListItem
|
||||||
import eu.kanade.presentation.anime.components.AnimeInfoHeader
|
import eu.kanade.presentation.anime.components.AnimeInfoBox
|
||||||
import eu.kanade.presentation.anime.components.AnimeSmallAppBar
|
|
||||||
import eu.kanade.presentation.anime.components.AnimeTopAppBar
|
|
||||||
import eu.kanade.presentation.anime.components.EpisodeHeader
|
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.ExtendedFloatingActionButton
|
||||||
|
import eu.kanade.presentation.components.LazyColumn
|
||||||
import eu.kanade.presentation.components.Scaffold
|
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.components.VerticalFastScroller
|
||||||
import eu.kanade.presentation.manga.DownloadAction
|
import eu.kanade.presentation.manga.DownloadAction
|
||||||
import eu.kanade.presentation.manga.EpisodeDownloadAction
|
import eu.kanade.presentation.manga.MangaScreenItem
|
||||||
import eu.kanade.presentation.util.ExitUntilCollapsedScrollBehavior
|
import eu.kanade.presentation.manga.components.MangaToolbar
|
||||||
import eu.kanade.presentation.util.isScrolledToEnd
|
import eu.kanade.presentation.util.isScrolledToEnd
|
||||||
import eu.kanade.presentation.util.isScrollingUp
|
import eu.kanade.presentation.util.isScrollingUp
|
||||||
import eu.kanade.presentation.util.plus
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
|
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
|
||||||
import eu.kanade.tachiyomi.animesource.getNameForAnimeInfo
|
import eu.kanade.tachiyomi.animesource.getNameForAnimeInfo
|
||||||
import eu.kanade.tachiyomi.data.download.model.AnimeDownload
|
import eu.kanade.tachiyomi.data.animedownload.model.AnimeDownload
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import eu.kanade.tachiyomi.ui.anime.AnimeScreenState
|
import eu.kanade.tachiyomi.ui.anime.AnimeScreenState
|
||||||
import eu.kanade.tachiyomi.ui.anime.EpisodeItem
|
import eu.kanade.tachiyomi.ui.anime.EpisodeItem
|
||||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
import eu.kanade.tachiyomi.ui.player.setting.PlayerPreferences
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
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
|
@Composable
|
||||||
fun AnimeScreen(
|
fun AnimeScreen(
|
||||||
state: AnimeScreenState.Success,
|
state: AnimeScreenState.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
windowWidthSizeClass: WindowWidthSizeClass,
|
isTabletUi: Boolean,
|
||||||
onBackClicked: () -> Unit,
|
onBackClicked: () -> Unit,
|
||||||
onEpisodeClicked: (Episode, Boolean) -> Unit,
|
onEpisodeClicked: (Episode, Boolean) -> Unit,
|
||||||
onDownloadEpisode: ((List<EpisodeItem>, EpisodeDownloadAction) -> Unit)?,
|
onDownloadEpisode: ((List<EpisodeItem>, EpisodeDownloadAction) -> Unit)?,
|
||||||
|
@ -130,8 +106,12 @@ fun AnimeScreen(
|
||||||
onMarkPreviousAsSeenClicked: (Episode) -> Unit,
|
onMarkPreviousAsSeenClicked: (Episode) -> Unit,
|
||||||
onMultiDeleteClicked: (List<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(
|
AnimeScreenSmallImpl(
|
||||||
state = state,
|
state = state,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
|
@ -142,9 +122,9 @@ fun AnimeScreen(
|
||||||
onWebViewClicked = onWebViewClicked,
|
onWebViewClicked = onWebViewClicked,
|
||||||
onTrackingClicked = onTrackingClicked,
|
onTrackingClicked = onTrackingClicked,
|
||||||
onTagClicked = onTagClicked,
|
onTagClicked = onTagClicked,
|
||||||
onFilterButtonClicked = onFilterButtonClicked,
|
onFilterClicked = onFilterButtonClicked,
|
||||||
onRefresh = onRefresh,
|
onRefresh = onRefresh,
|
||||||
onContinueReading = onContinueWatching,
|
onContinueWatching = onContinueWatching,
|
||||||
onSearch = onSearch,
|
onSearch = onSearch,
|
||||||
onCoverClicked = onCoverClicked,
|
onCoverClicked = onCoverClicked,
|
||||||
onShareClicked = onShareClicked,
|
onShareClicked = onShareClicked,
|
||||||
|
@ -156,11 +136,13 @@ fun AnimeScreen(
|
||||||
onMultiMarkAsSeenClicked = onMultiMarkAsSeenClicked,
|
onMultiMarkAsSeenClicked = onMultiMarkAsSeenClicked,
|
||||||
onMarkPreviousAsSeenClicked = onMarkPreviousAsSeenClicked,
|
onMarkPreviousAsSeenClicked = onMarkPreviousAsSeenClicked,
|
||||||
onMultiDeleteClicked = onMultiDeleteClicked,
|
onMultiDeleteClicked = onMultiDeleteClicked,
|
||||||
|
onEpisodeSelected = onEpisodeSelected,
|
||||||
|
onAllEpisodeSelected = onAllEpisodeSelected,
|
||||||
|
onInvertSelection = onInvertSelection,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
AnimeScreenLargeImpl(
|
AnimeScreenLargeImpl(
|
||||||
state = state,
|
state = state,
|
||||||
windowWidthSizeClass = windowWidthSizeClass,
|
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
onBackClicked = onBackClicked,
|
onBackClicked = onBackClicked,
|
||||||
onEpisodeClicked = onEpisodeClicked,
|
onEpisodeClicked = onEpisodeClicked,
|
||||||
|
@ -171,7 +153,7 @@ fun AnimeScreen(
|
||||||
onTagClicked = onTagClicked,
|
onTagClicked = onTagClicked,
|
||||||
onFilterButtonClicked = onFilterButtonClicked,
|
onFilterButtonClicked = onFilterButtonClicked,
|
||||||
onRefresh = onRefresh,
|
onRefresh = onRefresh,
|
||||||
onContinueReading = onContinueWatching,
|
onContinueWatching = onContinueWatching,
|
||||||
onSearch = onSearch,
|
onSearch = onSearch,
|
||||||
onCoverClicked = onCoverClicked,
|
onCoverClicked = onCoverClicked,
|
||||||
onShareClicked = onShareClicked,
|
onShareClicked = onShareClicked,
|
||||||
|
@ -183,6 +165,9 @@ fun AnimeScreen(
|
||||||
onMultiMarkAsSeenClicked = onMultiMarkAsSeenClicked,
|
onMultiMarkAsSeenClicked = onMultiMarkAsSeenClicked,
|
||||||
onMarkPreviousAsSeenClicked = onMarkPreviousAsSeenClicked,
|
onMarkPreviousAsSeenClicked = onMarkPreviousAsSeenClicked,
|
||||||
onMultiDeleteClicked = onMultiDeleteClicked,
|
onMultiDeleteClicked = onMultiDeleteClicked,
|
||||||
|
onEpisodeSelected = onEpisodeSelected,
|
||||||
|
onAllEpisodeSelected = onAllEpisodeSelected,
|
||||||
|
onInvertSelection = onInvertSelection,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -198,9 +183,9 @@ private fun AnimeScreenSmallImpl(
|
||||||
onWebViewClicked: (() -> Unit)?,
|
onWebViewClicked: (() -> Unit)?,
|
||||||
onTrackingClicked: (() -> Unit)?,
|
onTrackingClicked: (() -> Unit)?,
|
||||||
onTagClicked: (String) -> Unit,
|
onTagClicked: (String) -> Unit,
|
||||||
onFilterButtonClicked: () -> Unit,
|
onFilterClicked: () -> Unit,
|
||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
onContinueReading: () -> Unit,
|
onContinueWatching: () -> Unit,
|
||||||
onSearch: (query: String, global: Boolean) -> Unit,
|
onSearch: (query: String, global: Boolean) -> Unit,
|
||||||
|
|
||||||
// For cover dialog
|
// For cover dialog
|
||||||
|
@ -215,121 +200,64 @@ private fun AnimeScreenSmallImpl(
|
||||||
|
|
||||||
// For bottom action menu
|
// For bottom action menu
|
||||||
onMultiBookmarkClicked: (List<Episode>, bookmarked: Boolean) -> Unit,
|
onMultiBookmarkClicked: (List<Episode>, bookmarked: Boolean) -> Unit,
|
||||||
onMultiMarkAsSeenClicked: (List<Episode>, markAsRead: Boolean) -> Unit,
|
onMultiMarkAsSeenClicked: (List<Episode>, markAsSeen: Boolean) -> Unit,
|
||||||
onMarkPreviousAsSeenClicked: (Episode) -> Unit,
|
onMarkPreviousAsSeenClicked: (Episode) -> Unit,
|
||||||
onMultiDeleteClicked: (List<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()
|
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 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 = {
|
val internalOnBackPressed = {
|
||||||
if (selected.isNotEmpty()) {
|
if (episodes.any { it.selected }) {
|
||||||
selected.clear()
|
onAllEpisodeSelected(false)
|
||||||
} else {
|
} else {
|
||||||
onBackClicked()
|
onBackClicked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BackHandler(onBack = internalOnBackPressed)
|
BackHandler(onBack = internalOnBackPressed)
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
.padding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal).asPaddingValues()),
|
||||||
.padding(insetPadding),
|
|
||||||
topBar = {
|
topBar = {
|
||||||
AnimeTopAppBar(
|
val firstVisibleItemIndex by remember {
|
||||||
modifier = Modifier
|
derivedStateOf { episodeListState.firstVisibleItemIndex }
|
||||||
.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
|
|
||||||
}
|
}
|
||||||
consumed
|
val firstVisibleItemScrollOffset by remember {
|
||||||
},
|
derivedStateOf { episodeListState.firstVisibleItemScrollOffset }
|
||||||
orientation = Orientation.Vertical,
|
}
|
||||||
interactionSource = episodeListState.interactionSource as MutableInteractionSource,
|
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,
|
title = state.anime.title,
|
||||||
author = state.anime.author,
|
titleAlphaProvider = { animatedTitleAlpha },
|
||||||
artist = state.anime.artist,
|
backgroundAlphaProvider = { animatedBgAlpha },
|
||||||
description = state.anime.description,
|
hasFilters = state.anime.episodesFiltered(),
|
||||||
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(),
|
|
||||||
incognitoMode = state.isIncognitoMode,
|
incognitoMode = state.isIncognitoMode,
|
||||||
downloadedOnlyMode = state.isDownloadedOnlyMode,
|
downloadedOnlyMode = state.isDownloadedOnlyMode,
|
||||||
fromSource = state.isFromSource,
|
|
||||||
onBackClicked = internalOnBackPressed,
|
onBackClicked = internalOnBackPressed,
|
||||||
onCoverClick = onCoverClicked,
|
onClickFilter = onFilterClicked,
|
||||||
onTagClicked = onTagClicked,
|
onClickShare = onShareClicked,
|
||||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
onClickDownload = onDownloadActionClicked,
|
||||||
onWebViewClicked = onWebViewClicked,
|
onClickEditCategory = onEditCategoryClicked,
|
||||||
onTrackingClicked = onTrackingClicked,
|
onClickMigrate = onMigrateClicked,
|
||||||
onFilterButtonClicked = onFilterButtonClicked,
|
actionModeCounter = episodes.count { it.selected },
|
||||||
onShareClicked = onShareClicked,
|
onSelectAll = { onAllEpisodeSelected(true) },
|
||||||
onDownloadClicked = onDownloadActionClicked,
|
onInvertSelection = { onInvertSelection() },
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
SharedAnimeBottomActionMenu(
|
SharedAnimeBottomActionMenu(
|
||||||
selected = selected,
|
selected = episodes.filter { it.selected },
|
||||||
onEpisodeClicked = onEpisodeClicked,
|
onEpisodeClicked = onEpisodeClicked,
|
||||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||||
onMultiMarkAsSeenClicked = onMultiMarkAsSeenClicked,
|
onMultiMarkAsSeenClicked = onMultiMarkAsSeenClicked,
|
||||||
|
@ -342,7 +270,7 @@ private fun AnimeScreenSmallImpl(
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = episodes.any { !it.episode.seen } && selected.isEmpty(),
|
visible = episodes.any { !it.episode.seen } && episodes.none { it.selected },
|
||||||
enter = fadeIn(),
|
enter = fadeIn(),
|
||||||
exit = fadeOut(),
|
exit = fadeOut(),
|
||||||
) {
|
) {
|
||||||
|
@ -355,35 +283,102 @@ private fun AnimeScreenSmallImpl(
|
||||||
}
|
}
|
||||||
Text(text = stringResource(id))
|
Text(text = stringResource(id))
|
||||||
},
|
},
|
||||||
icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) },
|
icon = {
|
||||||
onClick = onContinueReading,
|
Icon(
|
||||||
|
imageVector = Icons.Filled.PlayArrow,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = onContinueWatching,
|
||||||
expanded = episodeListState.isScrollingUp() || episodeListState.isScrolledToEnd(),
|
expanded = episodeListState.isScrollingUp() || episodeListState.isScrolledToEnd(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()),
|
.padding(
|
||||||
|
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom)
|
||||||
|
.asPaddingValues(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
val withNavBarContentPadding = contentPadding +
|
val topPadding = contentPadding.calculateTopPadding()
|
||||||
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
|
|
||||||
|
SwipeRefresh(
|
||||||
|
refreshing = state.isRefreshingData,
|
||||||
|
onRefresh = onRefresh,
|
||||||
|
enabled = episodes.none { it.selected },
|
||||||
|
indicatorPadding = contentPadding,
|
||||||
|
) {
|
||||||
VerticalFastScroller(
|
VerticalFastScroller(
|
||||||
listState = episodeListState,
|
listState = episodeListState,
|
||||||
thumbAllowed = { scrollBehavior.state.offset == scrollBehavior.state.offsetLimit },
|
topContentPadding = topPadding,
|
||||||
topContentPadding = withNavBarContentPadding.calculateTopPadding(),
|
|
||||||
endContentPadding = withNavBarContentPadding.calculateEndPadding(LocalLayoutDirection.current),
|
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxHeight(),
|
modifier = Modifier.fillMaxHeight(),
|
||||||
state = episodeListState,
|
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(
|
sharedEpisodeItems(
|
||||||
episodes = episodes,
|
episodes = episodes,
|
||||||
state = state,
|
|
||||||
selected = selected,
|
|
||||||
selectedPositions = selectedPositions,
|
|
||||||
onEpisodeClicked = onEpisodeClicked,
|
onEpisodeClicked = onEpisodeClicked,
|
||||||
onDownloadEpisode = onDownloadEpisode,
|
onDownloadEpisode = onDownloadEpisode,
|
||||||
|
onEpisodeSelected = onEpisodeSelected,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -394,7 +389,6 @@ private fun AnimeScreenSmallImpl(
|
||||||
@Composable
|
@Composable
|
||||||
fun AnimeScreenLargeImpl(
|
fun AnimeScreenLargeImpl(
|
||||||
state: AnimeScreenState.Success,
|
state: AnimeScreenState.Success,
|
||||||
windowWidthSizeClass: WindowWidthSizeClass,
|
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
onBackClicked: () -> Unit,
|
onBackClicked: () -> Unit,
|
||||||
onEpisodeClicked: (Episode, Boolean) -> Unit,
|
onEpisodeClicked: (Episode, Boolean) -> Unit,
|
||||||
|
@ -405,7 +399,7 @@ fun AnimeScreenLargeImpl(
|
||||||
onTagClicked: (String) -> Unit,
|
onTagClicked: (String) -> Unit,
|
||||||
onFilterButtonClicked: () -> Unit,
|
onFilterButtonClicked: () -> Unit,
|
||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
onContinueReading: () -> Unit,
|
onContinueWatching: () -> Unit,
|
||||||
onSearch: (query: String, global: Boolean) -> Unit,
|
onSearch: (query: String, global: Boolean) -> Unit,
|
||||||
|
|
||||||
// For cover dialog
|
// For cover dialog
|
||||||
|
@ -420,40 +414,37 @@ fun AnimeScreenLargeImpl(
|
||||||
|
|
||||||
// For bottom action menu
|
// For bottom action menu
|
||||||
onMultiBookmarkClicked: (List<Episode>, bookmarked: Boolean) -> Unit,
|
onMultiBookmarkClicked: (List<Episode>, bookmarked: Boolean) -> Unit,
|
||||||
onMultiMarkAsSeenClicked: (List<Episode>, markAsRead: Boolean) -> Unit,
|
onMultiMarkAsSeenClicked: (List<Episode>, markAsSeen: Boolean) -> Unit,
|
||||||
onMarkPreviousAsSeenClicked: (Episode) -> Unit,
|
onMarkPreviousAsSeenClicked: (Episode) -> Unit,
|
||||||
onMultiDeleteClicked: (List<Episode>) -> Unit,
|
onMultiDeleteClicked: (List<Episode>) -> Unit,
|
||||||
|
|
||||||
|
// Episode selection
|
||||||
|
onEpisodeSelected: (EpisodeItem, Boolean, Boolean, Boolean) -> Unit,
|
||||||
|
onAllEpisodeSelected: (Boolean) -> Unit,
|
||||||
|
onInvertSelection: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val layoutDirection = LocalLayoutDirection.current
|
val layoutDirection = LocalLayoutDirection.current
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
val episodes = remember(state) { state.processedEpisodes.toList() }
|
||||||
|
|
||||||
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
||||||
val (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(0) }
|
var topBarHeight by remember { mutableStateOf(0) }
|
||||||
SwipeRefresh(
|
SwipeRefresh(
|
||||||
state = rememberSwipeRefreshState(state.isRefreshingInfo || state.isRefreshingEpisode),
|
refreshing = state.isRefreshingData,
|
||||||
onRefresh = onRefresh,
|
onRefresh = onRefresh,
|
||||||
|
enabled = episodes.none { it.selected },
|
||||||
indicatorPadding = PaddingValues(
|
indicatorPadding = PaddingValues(
|
||||||
start = insetPadding.calculateStartPadding(layoutDirection),
|
start = insetPadding.calculateStartPadding(layoutDirection),
|
||||||
top = with(density) { topBarHeight.toDp() },
|
top = with(density) { topBarHeight.toDp() },
|
||||||
end = insetPadding.calculateEndPadding(layoutDirection),
|
end = insetPadding.calculateEndPadding(layoutDirection),
|
||||||
),
|
),
|
||||||
clipIndicatorToPadding = true,
|
|
||||||
indicator = { s, trigger ->
|
|
||||||
SwipeRefreshIndicator(
|
|
||||||
state = s,
|
|
||||||
refreshTriggerDistance = trigger,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
val episodeListState = rememberLazyListState()
|
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 = {
|
val internalOnBackPressed = {
|
||||||
if (selected.isNotEmpty()) {
|
if (episodes.any { it.selected }) {
|
||||||
selected.clear()
|
onAllEpisodeSelected(false)
|
||||||
} else {
|
} else {
|
||||||
onBackClicked()
|
onBackClicked()
|
||||||
}
|
}
|
||||||
|
@ -463,29 +454,23 @@ fun AnimeScreenLargeImpl(
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.padding(insetPadding),
|
modifier = Modifier.padding(insetPadding),
|
||||||
topBar = {
|
topBar = {
|
||||||
AnimeSmallAppBar(
|
MangaToolbar(
|
||||||
modifier = Modifier.onSizeChanged { onTopBarHeightChanged(it.height) },
|
modifier = Modifier.onSizeChanged { topBarHeight = (it.height) },
|
||||||
title = state.anime.title,
|
title = state.anime.title,
|
||||||
titleAlphaProvider = { if (selected.isEmpty()) 0f else 1f },
|
titleAlphaProvider = { if (episodes.any { it.selected }) 1f else 0f },
|
||||||
backgroundAlphaProvider = { 1f },
|
backgroundAlphaProvider = { 1f },
|
||||||
|
hasFilters = state.anime.episodesFiltered(),
|
||||||
incognitoMode = state.isIncognitoMode,
|
incognitoMode = state.isIncognitoMode,
|
||||||
downloadedOnlyMode = state.isDownloadedOnlyMode,
|
downloadedOnlyMode = state.isDownloadedOnlyMode,
|
||||||
onBackClicked = internalOnBackPressed,
|
onBackClicked = internalOnBackPressed,
|
||||||
onShareClicked = onShareClicked,
|
onClickFilter = onFilterButtonClicked,
|
||||||
onDownloadClicked = onDownloadActionClicked,
|
onClickShare = onShareClicked,
|
||||||
onEditCategoryClicked = onEditCategoryClicked,
|
onClickDownload = onDownloadActionClicked,
|
||||||
onMigrateClicked = onMigrateClicked,
|
onClickEditCategory = onEditCategoryClicked,
|
||||||
changeAnimeSkipIntro = changeAnimeSkipIntro,
|
onClickMigrate = onMigrateClicked,
|
||||||
actionModeCounter = selected.size,
|
actionModeCounter = episodes.count { it.selected },
|
||||||
onSelectAll = {
|
onSelectAll = { onAllEpisodeSelected(true) },
|
||||||
selected.clear()
|
onInvertSelection = { onInvertSelection() },
|
||||||
selected.addAll(episodes)
|
|
||||||
},
|
|
||||||
onInvertSelection = {
|
|
||||||
val toSelect = episodes - selected
|
|
||||||
selected.clear()
|
|
||||||
selected.addAll(toSelect)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
|
@ -494,7 +479,7 @@ fun AnimeScreenLargeImpl(
|
||||||
contentAlignment = Alignment.BottomEnd,
|
contentAlignment = Alignment.BottomEnd,
|
||||||
) {
|
) {
|
||||||
SharedAnimeBottomActionMenu(
|
SharedAnimeBottomActionMenu(
|
||||||
selected = selected,
|
selected = episodes.filter { it.selected },
|
||||||
onEpisodeClicked = onEpisodeClicked,
|
onEpisodeClicked = onEpisodeClicked,
|
||||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||||
onMultiMarkAsSeenClicked = onMultiMarkAsSeenClicked,
|
onMultiMarkAsSeenClicked = onMultiMarkAsSeenClicked,
|
||||||
|
@ -508,7 +493,7 @@ fun AnimeScreenLargeImpl(
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = episodes.any { !it.episode.seen } && selected.isEmpty(),
|
visible = episodes.any { !it.episode.seen } && episodes.none { it.selected },
|
||||||
enter = fadeIn(),
|
enter = fadeIn(),
|
||||||
exit = fadeOut(),
|
exit = fadeOut(),
|
||||||
) {
|
) {
|
||||||
|
@ -521,8 +506,8 @@ fun AnimeScreenLargeImpl(
|
||||||
}
|
}
|
||||||
Text(text = stringResource(id))
|
Text(text = stringResource(id))
|
||||||
},
|
},
|
||||||
icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) },
|
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
|
||||||
onClick = onContinueReading,
|
onClick = onContinueWatching,
|
||||||
expanded = episodeListState.isScrollingUp() || episodeListState.isScrolledToEnd(),
|
expanded = episodeListState.isScrollingUp() || episodeListState.isScrolledToEnd(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()),
|
.padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()),
|
||||||
|
@ -530,75 +515,82 @@ fun AnimeScreenLargeImpl(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
Row {
|
TwoPanelBox(
|
||||||
val withNavBarContentPadding = contentPadding +
|
startContent = {
|
||||||
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
|
Column(
|
||||||
AnimeInfoHeader(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.verticalScroll(rememberScrollState()),
|
||||||
.verticalScroll(rememberScrollState())
|
) {
|
||||||
.padding(bottom = withNavBarContentPadding.calculateBottomPadding()),
|
AnimeInfoBox(
|
||||||
windowWidthSizeClass = WindowWidthSizeClass.Expanded,
|
isTabletUi = true,
|
||||||
appBarPadding = contentPadding.calculateTopPadding(),
|
appBarPadding = contentPadding.calculateTopPadding(),
|
||||||
title = state.anime.title,
|
title = state.anime.title,
|
||||||
author = state.anime.author,
|
author = state.anime.author,
|
||||||
artist = state.anime.artist,
|
artist = state.anime.artist,
|
||||||
description = state.anime.description,
|
|
||||||
tagsProvider = { state.anime.genre },
|
|
||||||
sourceName = remember { state.source.getNameForAnimeInfo() },
|
sourceName = remember { state.source.getNameForAnimeInfo() },
|
||||||
isStubSource = remember { state.source is AnimeSourceManager.StubSource },
|
isStubSource = remember { state.source is AnimeSourceManager.StubAnimeSource },
|
||||||
coverDataProvider = { state.anime },
|
coverDataProvider = { state.anime },
|
||||||
favorite = state.anime.favorite,
|
|
||||||
status = state.anime.status,
|
status = state.anime.status,
|
||||||
trackingCount = state.trackingCount,
|
|
||||||
fromSource = state.isFromSource,
|
|
||||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
|
||||||
onWebViewClicked = onWebViewClicked,
|
|
||||||
onTrackingClicked = onTrackingClicked,
|
|
||||||
onTagClicked = onTagClicked,
|
|
||||||
onEditCategory = onEditCategoryClicked,
|
|
||||||
onCoverClick = onCoverClicked,
|
onCoverClick = onCoverClicked,
|
||||||
doSearch = onSearch,
|
doSearch = onSearch,
|
||||||
)
|
)
|
||||||
|
AnimeActionRow(
|
||||||
val episodesWeight = if (windowWidthSizeClass == WindowWidthSizeClass.Medium) 1f else 2f
|
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(
|
VerticalFastScroller(
|
||||||
listState = episodeListState,
|
listState = episodeListState,
|
||||||
modifier = Modifier.weight(episodesWeight),
|
topContentPadding = contentPadding.calculateTopPadding(),
|
||||||
topContentPadding = withNavBarContentPadding.calculateTopPadding(),
|
|
||||||
endContentPadding = withNavBarContentPadding.calculateEndPadding(layoutDirection),
|
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxHeight(),
|
modifier = Modifier.fillMaxHeight(),
|
||||||
state = episodeListState,
|
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(
|
EpisodeHeader(
|
||||||
episodeCount = episodes.size,
|
episodeCount = episodes.size,
|
||||||
isEpisodeFiltered = state.anime.episodesFiltered(),
|
onClick = onFilterButtonClicked,
|
||||||
onFilterButtonClicked = onFilterButtonClicked,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
sharedEpisodeItems(
|
sharedEpisodeItems(
|
||||||
episodes = episodes,
|
episodes = episodes,
|
||||||
state = state,
|
|
||||||
selected = selected,
|
|
||||||
selectedPositions = selectedPositions,
|
|
||||||
onEpisodeClicked = onEpisodeClicked,
|
onEpisodeClicked = onEpisodeClicked,
|
||||||
onDownloadEpisode = onDownloadEpisode,
|
onDownloadEpisode = onDownloadEpisode,
|
||||||
|
onEpisodeSelected = onEpisodeSelected,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SharedAnimeBottomActionMenu(
|
private fun SharedAnimeBottomActionMenu(
|
||||||
selected: SnapshotStateList<EpisodeItem>,
|
selected: List<EpisodeItem>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onEpisodeClicked: (Episode, Boolean) -> Unit,
|
onEpisodeClicked: (Episode, Boolean) -> Unit,
|
||||||
onMultiBookmarkClicked: (List<Episode>, bookmarked: Boolean) -> Unit,
|
onMultiBookmarkClicked: (List<Episode>, bookmarked: Boolean) -> Unit,
|
||||||
onMultiMarkAsSeenClicked: (List<Episode>, markAsSeen: Boolean) -> Unit,
|
onMultiMarkAsSeenClicked: (List<Episode>, markAsSeen: Boolean) -> Unit,
|
||||||
|
@ -607,225 +599,97 @@ private fun SharedAnimeBottomActionMenu(
|
||||||
onMultiDeleteClicked: (List<Episode>) -> Unit,
|
onMultiDeleteClicked: (List<Episode>) -> Unit,
|
||||||
fillFraction: Float,
|
fillFraction: Float,
|
||||||
) {
|
) {
|
||||||
val preferences: PreferencesHelper = Injekt.get()
|
val preferences: PlayerPreferences = Injekt.get()
|
||||||
AnimeBottomActionMenu(
|
AnimeBottomActionMenu(
|
||||||
visible = selected.isNotEmpty(),
|
visible = selected.isNotEmpty(),
|
||||||
modifier = Modifier.fillMaxWidth(fillFraction),
|
modifier = modifier.fillMaxWidth(fillFraction),
|
||||||
onBookmarkClicked = {
|
onBookmarkClicked = {
|
||||||
onMultiBookmarkClicked.invoke(selected.map { it.episode }, true)
|
onMultiBookmarkClicked.invoke(selected.map { it.episode }, true)
|
||||||
selected.clear()
|
|
||||||
}.takeIf { selected.any { !it.episode.bookmark } },
|
}.takeIf { selected.any { !it.episode.bookmark } },
|
||||||
onRemoveBookmarkClicked = {
|
onRemoveBookmarkClicked = {
|
||||||
onMultiBookmarkClicked.invoke(selected.map { it.episode }, false)
|
onMultiBookmarkClicked.invoke(selected.map { it.episode }, false)
|
||||||
selected.clear()
|
|
||||||
}.takeIf { selected.all { it.episode.bookmark } },
|
}.takeIf { selected.all { it.episode.bookmark } },
|
||||||
onMarkAsSeenClicked = {
|
onMarkAsSeenClicked = {
|
||||||
onMultiMarkAsSeenClicked(selected.map { it.episode }, true)
|
onMultiMarkAsSeenClicked(selected.map { it.episode }, true)
|
||||||
selected.clear()
|
|
||||||
}.takeIf { selected.any { !it.episode.seen } },
|
}.takeIf { selected.any { !it.episode.seen } },
|
||||||
onMarkAsUnseenClicked = {
|
onMarkAsUnseenClicked = {
|
||||||
onMultiMarkAsSeenClicked(selected.map { it.episode }, false)
|
onMultiMarkAsSeenClicked(selected.map { it.episode }, false)
|
||||||
selected.clear()
|
}.takeIf { selected.any { it.episode.seen || it.episode.lastSecondSeen > 0L } },
|
||||||
}.takeIf { selected.any { it.episode.seen } },
|
|
||||||
onMarkPreviousAsSeenClicked = {
|
onMarkPreviousAsSeenClicked = {
|
||||||
onMarkPreviousAsSeenClicked(selected[0].episode)
|
onMarkPreviousAsSeenClicked(selected[0].episode)
|
||||||
selected.clear()
|
|
||||||
}.takeIf { selected.size == 1 },
|
}.takeIf { selected.size == 1 },
|
||||||
onDownloadClicked = {
|
onDownloadClicked = {
|
||||||
onDownloadEpisode!!(selected.toList(), EpisodeDownloadAction.START)
|
onDownloadEpisode!!(selected.toList(), EpisodeDownloadAction.START)
|
||||||
selected.clear()
|
|
||||||
}.takeIf {
|
}.takeIf {
|
||||||
onDownloadEpisode != null && selected.any { it.downloadState != AnimeDownload.State.DOWNLOADED }
|
onDownloadEpisode != null && selected.any { it.downloadState != AnimeDownload.State.DOWNLOADED }
|
||||||
},
|
},
|
||||||
onDeleteClicked = {
|
onDeleteClicked = {
|
||||||
onMultiDeleteClicked(selected.map { it.episode })
|
onMultiDeleteClicked(selected.map { it.episode })
|
||||||
selected.clear()
|
|
||||||
}.takeIf {
|
}.takeIf {
|
||||||
onDownloadEpisode != null && selected.any { it.downloadState == AnimeDownload.State.DOWNLOADED }
|
onDownloadEpisode != null && selected.any { it.downloadState == AnimeDownload.State.DOWNLOADED }
|
||||||
},
|
},
|
||||||
onExternalClicked = {
|
onExternalClicked = {
|
||||||
onEpisodeClicked(selected.map { it.episode }.first(), true)
|
onEpisodeClicked(selected.map { it.episode }.first(), true)
|
||||||
selected.clear()
|
}.takeIf { !preferences.alwaysUseExternalPlayer().get() && selected.size == 1 },
|
||||||
}.takeIf { !preferences.alwaysUseExternalPlayer() && selected.size == 1 },
|
|
||||||
onInternalClicked = {
|
onInternalClicked = {
|
||||||
onEpisodeClicked(selected.map { it.episode }.first(), true)
|
onEpisodeClicked(selected.map { it.episode }.first(), true)
|
||||||
selected.clear()
|
}.takeIf { preferences.alwaysUseExternalPlayer().get() && selected.size == 1 },
|
||||||
}.takeIf { preferences.alwaysUseExternalPlayer() && selected.size == 1 },
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun LazyListScope.sharedEpisodeItems(
|
private fun LazyListScope.sharedEpisodeItems(
|
||||||
episodes: List<EpisodeItem>,
|
episodes: List<EpisodeItem>,
|
||||||
state: AnimeScreenState.Success,
|
|
||||||
selected: SnapshotStateList<EpisodeItem>,
|
|
||||||
selectedPositions: Array<Int>,
|
|
||||||
onEpisodeClicked: (Episode, Boolean) -> Unit,
|
onEpisodeClicked: (Episode, Boolean) -> Unit,
|
||||||
onDownloadEpisode: ((List<EpisodeItem>, EpisodeDownloadAction) -> Unit)?,
|
onDownloadEpisode: ((List<EpisodeItem>, EpisodeDownloadAction) -> Unit)?,
|
||||||
|
onEpisodeSelected: (EpisodeItem, Boolean, Boolean, Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
items(items = episodes) { episodeItem ->
|
items(
|
||||||
val context = LocalContext.current
|
items = episodes,
|
||||||
|
key = { "episode-${it.episode.id}" },
|
||||||
|
contentType = { MangaScreenItem.CHAPTER },
|
||||||
|
) { episodeItem ->
|
||||||
val haptic = LocalHapticFeedback.current
|
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(
|
AnimeEpisodeListItem(
|
||||||
title = episodeTitle,
|
title = episodeItem.episodeTitleString,
|
||||||
date = date,
|
date = episodeItem.dateUploadString,
|
||||||
watchProgress = lastSecondSeen?.let {
|
watchProgress = episodeItem.seenProgressString,
|
||||||
if (totalSeconds != null) {
|
scanlator = episodeItem.episode.scanlator.takeIf { !it.isNullOrBlank() },
|
||||||
stringResource(
|
seen = episodeItem.episode.seen,
|
||||||
id = R.string.episode_progress,
|
bookmark = episodeItem.episode.bookmark,
|
||||||
formatProgress(lastSecondSeen),
|
selected = episodeItem.selected,
|
||||||
formatProgress(totalSeconds),
|
downloadStateProvider = { episodeItem.downloadState },
|
||||||
)
|
downloadProgressProvider = { episodeItem.downloadProgress },
|
||||||
} 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,
|
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
val dispatched = onEpisodeItemLongClick(
|
onEpisodeSelected(episodeItem, !episodeItem.selected, true, true)
|
||||||
episodeItem = episodeItem,
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
selected = selected,
|
|
||||||
episodes = episodes,
|
|
||||||
selectedPositions = selectedPositions,
|
|
||||||
)
|
|
||||||
if (dispatched) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
onEpisodeItemClick(
|
onEpisodeItemClick(
|
||||||
episodeItem = episodeItem,
|
episodeItem = episodeItem,
|
||||||
selected = selected,
|
|
||||||
episodes = episodes,
|
episodes = episodes,
|
||||||
selectedPositions = selectedPositions,
|
onToggleSelection = { onEpisodeSelected(episodeItem, !episodeItem.selected, true, false) },
|
||||||
onEpisodeClicked = onEpisodeClicked,
|
onEpisodeClicked = onEpisodeClicked,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onDownloadClick = if (onDownloadEpisode != null) {
|
onDownloadClick = if (onDownloadEpisode != null) {
|
||||||
{ onDownloadEpisode(listOf(episodeItem), it) }
|
{ 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(
|
private fun onEpisodeItemClick(
|
||||||
episodeItem: EpisodeItem,
|
episodeItem: EpisodeItem,
|
||||||
selected: MutableList<EpisodeItem>,
|
|
||||||
episodes: List<EpisodeItem>,
|
episodes: List<EpisodeItem>,
|
||||||
selectedPositions: Array<Int>,
|
onToggleSelection: (Boolean) -> Unit,
|
||||||
onEpisodeClicked: (Episode, Boolean) -> Unit,
|
onEpisodeClicked: (Episode, Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
val selectedIndex = episodes.indexOf(episodeItem)
|
|
||||||
when {
|
when {
|
||||||
selected.contains(episodeItem) -> {
|
episodeItem.selected -> onToggleSelection(false)
|
||||||
val removedIndex = episodes.indexOf(episodeItem)
|
episodes.any { it.selected } -> onToggleSelection(true)
|
||||||
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)
|
|
||||||
}
|
|
||||||
else -> onEpisodeClicked(episodeItem.episode, false)
|
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.padding
|
||||||
import androidx.compose.foundation.layout.systemBars
|
import androidx.compose.foundation.layout.systemBars
|
||||||
import androidx.compose.material.icons.Icons
|
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.Edit
|
||||||
import androidx.compose.material.icons.outlined.Save
|
import androidx.compose.material.icons.outlined.Save
|
||||||
import androidx.compose.material.icons.outlined.Share
|
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.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
|
@ -63,7 +66,7 @@ fun AnimeCoverDialog(
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = onDismissRequest) {
|
IconButton(onClick = onDismissRequest) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Close,
|
imageVector = Icons.Outlined.Close,
|
||||||
contentDescription = stringResource(R.string.action_close),
|
contentDescription = stringResource(R.string.action_close),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -82,9 +85,15 @@ fun AnimeCoverDialog(
|
||||||
}
|
}
|
||||||
if (onEditClick != null) {
|
if (onEditClick != null) {
|
||||||
Box {
|
Box {
|
||||||
val (expanded, onExpand) = remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { if (isCustomCover) onExpand(true) else onEditClick(EditCoverAction.EDIT) },
|
onClick = {
|
||||||
|
if (isCustomCover) {
|
||||||
|
expanded = true
|
||||||
|
} else {
|
||||||
|
onEditClick(EditCoverAction.EDIT)
|
||||||
|
}
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Edit,
|
imageVector = Icons.Outlined.Edit,
|
||||||
|
@ -93,20 +102,21 @@ fun AnimeCoverDialog(
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onDismissRequest = { onExpand(false) },
|
onDismissRequest = { expanded = false },
|
||||||
|
offset = DpOffset(8.dp, 0.dp),
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(text = stringResource(R.string.action_edit)) },
|
text = { Text(text = stringResource(R.string.action_edit)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
onEditClick(EditCoverAction.EDIT)
|
onEditClick(EditCoverAction.EDIT)
|
||||||
onExpand(false)
|
expanded = false
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(text = stringResource(R.string.action_delete)) },
|
text = { Text(text = stringResource(R.string.action_delete)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
onEditClick(EditCoverAction.DELETE)
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import eu.kanade.presentation.components.EpisodeDownloadAction
|
||||||
import eu.kanade.presentation.components.EpisodeDownloadIndicator
|
import eu.kanade.presentation.components.EpisodeDownloadIndicator
|
||||||
import eu.kanade.presentation.manga.EpisodeDownloadAction
|
|
||||||
import eu.kanade.presentation.manga.components.DotSeparatorText
|
import eu.kanade.presentation.manga.components.DotSeparatorText
|
||||||
|
import eu.kanade.presentation.util.ReadItemAlpha
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.download.model.AnimeDownload
|
import eu.kanade.tachiyomi.data.animedownload.model.AnimeDownload
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AnimeEpisodeListItem(
|
fun AnimeEpisodeListItem(
|
||||||
|
@ -45,8 +46,8 @@ fun AnimeEpisodeListItem(
|
||||||
seen: Boolean,
|
seen: Boolean,
|
||||||
bookmark: Boolean,
|
bookmark: Boolean,
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
downloadState: AnimeDownload.State,
|
downloadStateProvider: () -> AnimeDownload.State,
|
||||||
downloadProgress: Int,
|
downloadProgressProvider: () -> Int,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onDownloadClick: ((EpisodeDownloadAction) -> Unit)?,
|
onDownloadClick: ((EpisodeDownloadAction) -> Unit)?,
|
||||||
|
@ -66,13 +67,13 @@ fun AnimeEpisodeListItem(
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurface
|
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) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
var textHeight by remember { mutableStateOf(0) }
|
var textHeight by remember { mutableStateOf(0) }
|
||||||
if (bookmark) {
|
if (bookmark) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Bookmark,
|
imageVector = Icons.Filled.Bookmark,
|
||||||
contentDescription = stringResource(R.string.action_filter_bookmarked),
|
contentDescription = stringResource(R.string.action_filter_bookmarked),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
|
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
|
||||||
|
@ -82,8 +83,8 @@ fun AnimeEpisodeListItem(
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.bodyMedium
|
color = textColor,
|
||||||
.copy(color = textColor),
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
onTextLayout = { textHeight = it.size.height },
|
onTextLayout = { textHeight = it.size.height },
|
||||||
|
@ -109,7 +110,7 @@ fun AnimeEpisodeListItem(
|
||||||
text = watchProgress,
|
text = watchProgress,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.alpha(SeenItemAlpha),
|
modifier = Modifier.alpha(ReadItemAlpha),
|
||||||
)
|
)
|
||||||
if (scanlator != null) DotSeparatorText()
|
if (scanlator != null) DotSeparatorText()
|
||||||
}
|
}
|
||||||
|
@ -128,12 +129,10 @@ fun AnimeEpisodeListItem(
|
||||||
if (onDownloadClick != null) {
|
if (onDownloadClick != null) {
|
||||||
EpisodeDownloadIndicator(
|
EpisodeDownloadIndicator(
|
||||||
modifier = Modifier.padding(start = 4.dp),
|
modifier = Modifier.padding(start = 4.dp),
|
||||||
downloadState = downloadState,
|
downloadStateProvider = downloadStateProvider,
|
||||||
downloadProgress = downloadProgress,
|
downloadProgressProvider = downloadProgressProvider,
|
||||||
onClick = onDownloadClick,
|
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.layout.sizeIn
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
import androidx.compose.material.icons.Icons
|
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.Favorite
|
||||||
import androidx.compose.material.icons.filled.FavoriteBorder
|
import androidx.compose.material.icons.outlined.AttachMoney
|
||||||
import androidx.compose.material.icons.filled.Pause
|
import androidx.compose.material.icons.outlined.Block
|
||||||
import androidx.compose.material.icons.filled.Public
|
import androidx.compose.material.icons.outlined.Close
|
||||||
import androidx.compose.material.icons.filled.Schedule
|
import androidx.compose.material.icons.outlined.Done
|
||||||
import androidx.compose.material.icons.filled.Sync
|
import androidx.compose.material.icons.outlined.DoneAll
|
||||||
import androidx.compose.material.icons.filled.Warning
|
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.Icon
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
|
import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
|
||||||
|
@ -42,7 +43,6 @@ import androidx.compose.material3.ProvideTextStyle
|
||||||
import androidx.compose.material3.SuggestionChip
|
import androidx.compose.material3.SuggestionChip
|
||||||
import androidx.compose.material3.SuggestionChipDefaults
|
import androidx.compose.material3.SuggestionChipDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.getValue
|
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.layout.SubcomposeLayout
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
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.components.TextButton
|
||||||
import eu.kanade.presentation.manga.components.DotSeparatorText
|
import eu.kanade.presentation.manga.components.DotSeparatorText
|
||||||
import eu.kanade.presentation.util.clickableNoIndication
|
import eu.kanade.presentation.util.clickableNoIndication
|
||||||
import eu.kanade.presentation.util.quantityStringResource
|
|
||||||
import eu.kanade.presentation.util.secondaryItemAlpha
|
import eu.kanade.presentation.util.secondaryItemAlpha
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
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))
|
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AnimeInfoHeader(
|
fun AnimeInfoBox(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
windowWidthSizeClass: WindowWidthSizeClass,
|
isTabletUi: Boolean,
|
||||||
appBarPadding: Dp,
|
appBarPadding: Dp,
|
||||||
title: String,
|
title: String,
|
||||||
author: String?,
|
author: String?,
|
||||||
artist: String?,
|
artist: String?,
|
||||||
description: String?,
|
|
||||||
tagsProvider: () -> List<String>?,
|
|
||||||
sourceName: String,
|
sourceName: String,
|
||||||
isStubSource: Boolean,
|
isStubSource: Boolean,
|
||||||
coverDataProvider: () -> Anime,
|
coverDataProvider: () -> Anime,
|
||||||
favorite: Boolean,
|
|
||||||
status: Long,
|
status: Long,
|
||||||
trackingCount: Int,
|
|
||||||
fromSource: Boolean,
|
|
||||||
onAddToLibraryClicked: () -> Unit,
|
|
||||||
onWebViewClicked: (() -> Unit)?,
|
|
||||||
onTrackingClicked: (() -> Unit)?,
|
|
||||||
onTagClicked: (String) -> Unit,
|
|
||||||
onEditCategory: (() -> Unit)?,
|
|
||||||
onCoverClick: () -> Unit,
|
onCoverClick: () -> Unit,
|
||||||
doSearch: (query: String, global: Boolean) -> Unit,
|
doSearch: (query: String, global: Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
Box(modifier = modifier) {
|
||||||
Column(modifier = modifier) {
|
|
||||||
Box {
|
|
||||||
// Backdrop
|
// Backdrop
|
||||||
val backdropGradientColors = listOf(
|
val backdropGradientColors = listOf(
|
||||||
Color.Transparent,
|
Color.Transparent,
|
||||||
|
@ -133,15 +121,15 @@ fun AnimeInfoHeader(
|
||||||
.alpha(.2f),
|
.alpha(.2f),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Anime & source info
|
// Manga & source info
|
||||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
|
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
|
||||||
if (windowWidthSizeClass == WindowWidthSizeClass.Compact) {
|
if (!isTabletUi) {
|
||||||
AnimeAndSourceTitlesSmall(
|
AnimeAndSourceTitlesSmall(
|
||||||
appBarPadding = appBarPadding,
|
appBarPadding = appBarPadding,
|
||||||
coverDataProvider = coverDataProvider,
|
coverDataProvider = coverDataProvider,
|
||||||
onCoverClick = onCoverClick,
|
onCoverClick = onCoverClick,
|
||||||
title = title,
|
title = title,
|
||||||
context = context,
|
context = LocalContext.current,
|
||||||
doSearch = doSearch,
|
doSearch = doSearch,
|
||||||
author = author,
|
author = author,
|
||||||
artist = artist,
|
artist = artist,
|
||||||
|
@ -155,7 +143,7 @@ fun AnimeInfoHeader(
|
||||||
coverDataProvider = coverDataProvider,
|
coverDataProvider = coverDataProvider,
|
||||||
onCoverClick = onCoverClick,
|
onCoverClick = onCoverClick,
|
||||||
title = title,
|
title = title,
|
||||||
context = context,
|
context = LocalContext.current,
|
||||||
doSearch = doSearch,
|
doSearch = doSearch,
|
||||||
author = author,
|
author = author,
|
||||||
artist = artist,
|
artist = artist,
|
||||||
|
@ -166,9 +154,19 @@ fun AnimeInfoHeader(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Action buttons
|
@Composable
|
||||||
Row(modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
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)
|
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||||
AnimeActionButton(
|
AnimeActionButton(
|
||||||
title = if (favorite) {
|
title = if (favorite) {
|
||||||
|
@ -176,7 +174,7 @@ fun AnimeInfoHeader(
|
||||||
} else {
|
} else {
|
||||||
stringResource(R.string.add_to_library)
|
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,
|
color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||||
onClick = onAddToLibraryClicked,
|
onClick = onAddToLibraryClicked,
|
||||||
onLongClick = onEditCategory,
|
onLongClick = onEditCategory,
|
||||||
|
@ -186,9 +184,9 @@ fun AnimeInfoHeader(
|
||||||
title = if (trackingCount == 0) {
|
title = if (trackingCount == 0) {
|
||||||
stringResource(R.string.manga_tracking_tab)
|
stringResource(R.string.manga_tracking_tab)
|
||||||
} else {
|
} 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,
|
color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary,
|
||||||
onClick = onTrackingClicked,
|
onClick = onTrackingClicked,
|
||||||
)
|
)
|
||||||
|
@ -196,20 +194,28 @@ fun AnimeInfoHeader(
|
||||||
if (onWebViewClicked != null) {
|
if (onWebViewClicked != null) {
|
||||||
AnimeActionButton(
|
AnimeActionButton(
|
||||||
title = stringResource(R.string.action_web_view),
|
title = stringResource(R.string.action_web_view),
|
||||||
icon = Icons.Default.Public,
|
icon = Icons.Outlined.Public,
|
||||||
color = defaultActionButtonColor,
|
color = defaultActionButtonColor,
|
||||||
onClick = onWebViewClicked,
|
onClick = onWebViewClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Expandable description-tags
|
@Composable
|
||||||
Column {
|
fun ExpandableAnimeDescription(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
defaultExpandState: Boolean,
|
||||||
|
description: String?,
|
||||||
|
tagsProvider: () -> List<String>?,
|
||||||
|
onTagClicked: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
val (expanded, onExpanded) = rememberSaveable {
|
val (expanded, onExpanded) = rememberSaveable {
|
||||||
mutableStateOf(fromSource || windowWidthSizeClass != WindowWidthSizeClass.Compact)
|
mutableStateOf(defaultExpandState)
|
||||||
}
|
}
|
||||||
val desc =
|
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) {
|
val trimmedDescription = remember(desc) {
|
||||||
desc
|
desc
|
||||||
.replace(whitespaceLineRegex, "\n")
|
.replace(whitespaceLineRegex, "\n")
|
||||||
|
@ -222,10 +228,7 @@ fun AnimeInfoHeader(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 8.dp)
|
.padding(top = 8.dp)
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.clickableNoIndication(
|
.clickableNoIndication { onExpanded(!expanded) },
|
||||||
onLongClick = { context.copyToClipboard(desc, desc) },
|
|
||||||
onClick = { onExpanded(!expanded) },
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
val tags = tagsProvider()
|
val tags = tagsProvider()
|
||||||
if (!tags.isNullOrEmpty()) {
|
if (!tags.isNullOrEmpty()) {
|
||||||
|
@ -264,7 +267,6 @@ fun AnimeInfoHeader(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -288,13 +290,14 @@ private fun AnimeAndSourceTitlesLarge(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
MangaCover.Book(
|
MangaCover.Book(
|
||||||
modifier = Modifier.fillMaxWidth(0.4f),
|
modifier = Modifier.fillMaxWidth(0.65f),
|
||||||
data = coverDataProvider(),
|
data = coverDataProvider(),
|
||||||
|
contentDescription = stringResource(R.string.manga_cover),
|
||||||
onClick = onCoverClick,
|
onClick = onCoverClick,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Text(
|
Text(
|
||||||
text = title.takeIf { it.isNotBlank() } ?: stringResource(R.string.unknown),
|
text = title.ifBlank { stringResource(R.string.unknown_title) },
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
modifier = Modifier.clickableNoIndication(
|
modifier = Modifier.clickableNoIndication(
|
||||||
onLongClick = { if (title.isNotBlank()) context.copyToClipboard(title, title) },
|
onLongClick = { if (title.isNotBlank()) context.copyToClipboard(title, title) },
|
||||||
|
@ -311,10 +314,12 @@ private fun AnimeAndSourceTitlesLarge(
|
||||||
.padding(top = 2.dp)
|
.padding(top = 2.dp)
|
||||||
.clickableNoIndication(
|
.clickableNoIndication(
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
if (!author.isNullOrBlank()) context.copyToClipboard(
|
if (!author.isNullOrBlank()) {
|
||||||
|
context.copyToClipboard(
|
||||||
author,
|
author,
|
||||||
author,
|
author,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
|
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
|
||||||
),
|
),
|
||||||
|
@ -341,13 +346,13 @@ private fun AnimeAndSourceTitlesLarge(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = when (status) {
|
imageVector = when (status) {
|
||||||
SAnime.ONGOING.toLong() -> Icons.Default.Schedule
|
SAnime.ONGOING.toLong() -> Icons.Outlined.Schedule
|
||||||
SAnime.COMPLETED.toLong() -> Icons.Default.DoneAll
|
SAnime.COMPLETED.toLong() -> Icons.Outlined.DoneAll
|
||||||
SAnime.LICENSED.toLong() -> Icons.Default.AttachMoney
|
SAnime.LICENSED.toLong() -> Icons.Outlined.AttachMoney
|
||||||
SAnime.PUBLISHING_FINISHED.toLong() -> Icons.Default.Done
|
SAnime.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
|
||||||
SAnime.CANCELLED.toLong() -> Icons.Default.Close
|
SAnime.CANCELLED.toLong() -> Icons.Outlined.Close
|
||||||
SAnime.ON_HIATUS.toLong() -> Icons.Default.Pause
|
SAnime.ON_HIATUS.toLong() -> Icons.Outlined.Pause
|
||||||
else -> Icons.Default.Block
|
else -> Icons.Outlined.Block
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -371,7 +376,7 @@ private fun AnimeAndSourceTitlesLarge(
|
||||||
DotSeparatorText()
|
DotSeparatorText()
|
||||||
if (isStubSource) {
|
if (isStubSource) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Warning,
|
imageVector = Icons.Outlined.Warning,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(end = 4.dp)
|
.padding(end = 4.dp)
|
||||||
|
@ -415,18 +420,21 @@ private fun AnimeAndSourceTitlesSmall(
|
||||||
.sizeIn(maxWidth = 100.dp)
|
.sizeIn(maxWidth = 100.dp)
|
||||||
.align(Alignment.Top),
|
.align(Alignment.Top),
|
||||||
data = coverDataProvider(),
|
data = coverDataProvider(),
|
||||||
|
contentDescription = stringResource(R.string.manga_cover),
|
||||||
onClick = onCoverClick,
|
onClick = onCoverClick,
|
||||||
)
|
)
|
||||||
Column(modifier = Modifier.padding(start = 16.dp)) {
|
Column(modifier = Modifier.padding(start = 16.dp)) {
|
||||||
Text(
|
Text(
|
||||||
text = title.ifBlank { stringResource(R.string.unknown) },
|
text = title.ifBlank { stringResource(R.string.unknown_title) },
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
modifier = Modifier.clickableNoIndication(
|
modifier = Modifier.clickableNoIndication(
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
if (title.isNotBlank()) context.copyToClipboard(
|
if (title.isNotBlank()) {
|
||||||
|
context.copyToClipboard(
|
||||||
title,
|
title,
|
||||||
title,
|
title,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onClick = { if (title.isNotBlank()) doSearch(title, true) },
|
onClick = { if (title.isNotBlank()) doSearch(title, true) },
|
||||||
),
|
),
|
||||||
|
@ -441,10 +449,12 @@ private fun AnimeAndSourceTitlesSmall(
|
||||||
.padding(top = 2.dp)
|
.padding(top = 2.dp)
|
||||||
.clickableNoIndication(
|
.clickableNoIndication(
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
if (!author.isNullOrBlank()) context.copyToClipboard(
|
if (!author.isNullOrBlank()) {
|
||||||
|
context.copyToClipboard(
|
||||||
author,
|
author,
|
||||||
author,
|
author,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
|
onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
|
||||||
),
|
),
|
||||||
|
@ -469,13 +479,13 @@ private fun AnimeAndSourceTitlesSmall(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = when (status) {
|
imageVector = when (status) {
|
||||||
SAnime.ONGOING.toLong() -> Icons.Default.Schedule
|
SAnime.ONGOING.toLong() -> Icons.Outlined.Schedule
|
||||||
SAnime.COMPLETED.toLong() -> Icons.Default.DoneAll
|
SAnime.COMPLETED.toLong() -> Icons.Outlined.DoneAll
|
||||||
SAnime.LICENSED.toLong() -> Icons.Default.AttachMoney
|
SAnime.LICENSED.toLong() -> Icons.Outlined.AttachMoney
|
||||||
SAnime.PUBLISHING_FINISHED.toLong() -> Icons.Default.Done
|
SAnime.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
|
||||||
SAnime.CANCELLED.toLong() -> Icons.Default.Close
|
SAnime.CANCELLED.toLong() -> Icons.Outlined.Close
|
||||||
SAnime.ON_HIATUS.toLong() -> Icons.Default.Pause
|
SAnime.ON_HIATUS.toLong() -> Icons.Outlined.Pause
|
||||||
else -> Icons.Default.Block
|
else -> Icons.Outlined.Block
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -499,7 +509,7 @@ private fun AnimeAndSourceTitlesSmall(
|
||||||
DotSeparatorText()
|
DotSeparatorText()
|
||||||
if (isStubSource) {
|
if (isStubSource) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Warning,
|
imageVector = Icons.Outlined.Warning,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(end = 4.dp)
|
.padding(end = 4.dp)
|
||||||
|
@ -555,6 +565,7 @@ private fun AnimeSummary(
|
||||||
expandedHeight = expandedPlaceable.maxByOrNull { it.height }?.height?.coerceAtLeast(shrunkHeight) ?: 0
|
expandedHeight = expandedPlaceable.maxByOrNull { it.height }?.height?.coerceAtLeast(shrunkHeight) ?: 0
|
||||||
|
|
||||||
val actualPlaceable = subcompose("description") {
|
val actualPlaceable = subcompose("description") {
|
||||||
|
SelectionContainer {
|
||||||
Text(
|
Text(
|
||||||
text = if (expanded) expandedDescription else shrunkDescription,
|
text = if (expanded) expandedDescription else shrunkDescription,
|
||||||
maxLines = Int.MAX_VALUE,
|
maxLines = Int.MAX_VALUE,
|
||||||
|
@ -562,6 +573,7 @@ private fun AnimeSummary(
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
modifier = Modifier.secondaryItemAlpha(),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}.map { it.measure(constraints) }
|
}.map { it.measure(constraints) }
|
||||||
|
|
||||||
val scrimPlaceable = subcompose("scrim") {
|
val scrimPlaceable = subcompose("scrim") {
|
||||||
|
@ -573,7 +585,7 @@ private fun AnimeSummary(
|
||||||
val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down)
|
val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down)
|
||||||
Icon(
|
Icon(
|
||||||
painter = rememberAnimatedVectorPainter(image, !expanded),
|
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,
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
modifier = Modifier.background(Brush.radialGradient(colors = colors.asReversed())),
|
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.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
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.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.util.quantityStringResource
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EpisodeHeader(
|
fun EpisodeHeader(
|
||||||
episodeCount: Int?,
|
episodeCount: Int?,
|
||||||
isEpisodeFiltered: Boolean,
|
onClick: () -> Unit,
|
||||||
onFilterButtonClicked: () -> Unit,
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(onClick = onFilterButtonClicked)
|
.clickable(onClick = onClick)
|
||||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
|
@ -37,20 +30,11 @@ fun EpisodeHeader(
|
||||||
text = if (episodeCount == null) {
|
text = if (episodeCount == null) {
|
||||||
stringResource(R.string.episodes)
|
stringResource(R.string.episodes)
|
||||||
} else {
|
} else {
|
||||||
quantityStringResource(id = R.plurals.anime_num_episodes, quantity = episodeCount)
|
pluralStringResource(id = R.plurals.anime_num_episodes, count = episodeCount, episodeCount)
|
||||||
},
|
},
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
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 android.util.DisplayMetrics
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
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.material.icons.outlined.Settings
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Divider
|
import androidx.compose.material3.Divider
|
||||||
|
@ -30,58 +32,116 @@ import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
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_ALPHA
|
||||||
import eu.kanade.presentation.components.Divider
|
import eu.kanade.presentation.components.Divider
|
||||||
import eu.kanade.presentation.components.EmptyScreen
|
import eu.kanade.presentation.components.EmptyScreen
|
||||||
|
import eu.kanade.presentation.components.LoadingScreen
|
||||||
import eu.kanade.presentation.components.PreferenceRow
|
import eu.kanade.presentation.components.PreferenceRow
|
||||||
|
import eu.kanade.presentation.components.Scaffold
|
||||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
||||||
import eu.kanade.presentation.util.horizontalPadding
|
import eu.kanade.presentation.util.horizontalPadding
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.animeextension.model.AnimeExtension
|
||||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
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.AnimeExtensionDetailsPresenter
|
||||||
import eu.kanade.tachiyomi.ui.browse.animeextension.details.AnimeExtensionSourceItem
|
import eu.kanade.tachiyomi.ui.browse.animeextension.details.AnimeExtensionSourceItem
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AnimeExtensionDetailsScreen(
|
fun AnimeExtensionDetailsScreen(
|
||||||
nestedScrollInterop: NestedScrollConnection,
|
navigateUp: () -> Unit,
|
||||||
presenter: AnimeExtensionDetailsPresenter,
|
presenter: AnimeExtensionDetailsPresenter,
|
||||||
onClickUninstall: () -> Unit,
|
|
||||||
onClickAppInfo: () -> Unit,
|
|
||||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||||
onClickSource: (sourceId: Long) -> Unit,
|
|
||||||
) {
|
) {
|
||||||
val extension = presenter.extension
|
val uriHandler = LocalUriHandler.current
|
||||||
|
|
||||||
if (extension == null) {
|
Scaffold(
|
||||||
EmptyScreen(textResource = R.string.empty_screen)
|
topBar = { scrollBehavior ->
|
||||||
return
|
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) }
|
var showNsfwWarning by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
ScrollbarLazyColumn(
|
ScrollbarLazyColumn(
|
||||||
modifier = Modifier.nestedScroll(nestedScrollInterop),
|
contentPadding = contentPadding,
|
||||||
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
|
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
extension.isUnofficial ->
|
extension.isUnofficial ->
|
||||||
|
@ -97,8 +157,13 @@ fun AnimeExtensionDetailsScreen(
|
||||||
item {
|
item {
|
||||||
DetailsHeader(
|
DetailsHeader(
|
||||||
extension = extension,
|
extension = extension,
|
||||||
onClickUninstall = onClickUninstall,
|
onClickUninstall = { presenter.uninstallExtension() },
|
||||||
onClickAppInfo = onClickAppInfo,
|
onClickAppInfo = {
|
||||||
|
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||||
|
data = Uri.fromParts("package", extension.pkgName, null)
|
||||||
|
context.startActivity(this)
|
||||||
|
}
|
||||||
|
},
|
||||||
onClickAgeRating = {
|
onClickAgeRating = {
|
||||||
showNsfwWarning = true
|
showNsfwWarning = true
|
||||||
},
|
},
|
||||||
|
@ -106,14 +171,14 @@ fun AnimeExtensionDetailsScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
items(
|
items(
|
||||||
items = sources,
|
items = presenter.sources,
|
||||||
key = { it.source.id },
|
key = { it.source.id },
|
||||||
) { source ->
|
) { source ->
|
||||||
SourceSwitchPreference(
|
SourceSwitchPreference(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
source = source,
|
source = source,
|
||||||
onClickSourcePreferences = onClickSourcePreferences,
|
onClickSourcePreferences = onClickSourcePreferences,
|
||||||
onClickSource = onClickSource,
|
onClickSource = { presenter.toggleSource(it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,23 +189,22 @@ fun AnimeExtensionDetailsScreen(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun WarningBanner(@StringRes textRes: Int) {
|
private fun WarningBanner(@StringRes textRes: Int) {
|
||||||
Box(
|
Text(
|
||||||
|
text = stringResource(textRes),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(MaterialTheme.colorScheme.error)
|
.background(MaterialTheme.colorScheme.error)
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(textRes),
|
|
||||||
color = MaterialTheme.colorScheme.onError,
|
color = MaterialTheme.colorScheme.onError,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -164,7 +228,7 @@ private fun DetailsHeader(
|
||||||
),
|
),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
ExtensionIcon(
|
AnimeExtensionIcon(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(112.dp),
|
.size(112.dp),
|
||||||
extension = extension,
|
extension = extension,
|
||||||
|
@ -268,7 +332,9 @@ private fun InfoText(
|
||||||
|
|
||||||
val clickableModifier = if (onClick != null) {
|
val clickableModifier = if (onClick != null) {
|
||||||
Modifier.clickable(interactionSource, indication = null) { onClick() }
|
Modifier.clickable(interactionSource, indication = null) { onClick() }
|
||||||
} else Modifier
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.then(clickableModifier),
|
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.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
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.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
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.Button
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ProvideTextStyle
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
import com.google.accompanist.flowlayout.FlowRow
|
||||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
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.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.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.horizontalPadding
|
||||||
import eu.kanade.presentation.util.plus
|
import eu.kanade.presentation.util.plus
|
||||||
|
import eu.kanade.presentation.util.secondaryItemAlpha
|
||||||
import eu.kanade.presentation.util.topPaddingValues
|
import eu.kanade.presentation.util.topPaddingValues
|
||||||
import eu.kanade.tachiyomi.R
|
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.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.AnimeExtensionsPresenter
|
||||||
import eu.kanade.tachiyomi.ui.browse.animeextension.ExtensionState
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.animeextension.ExtensionUiModel
|
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AnimeExtensionScreen(
|
fun AnimeExtensionScreen(
|
||||||
nestedScrollInterop: NestedScrollConnection,
|
|
||||||
presenter: AnimeExtensionsPresenter,
|
presenter: AnimeExtensionsPresenter,
|
||||||
|
contentPadding: PaddingValues,
|
||||||
onLongClickItem: (AnimeExtension) -> Unit,
|
onLongClickItem: (AnimeExtension) -> Unit,
|
||||||
onClickItemCancel: (AnimeExtension) -> Unit,
|
onClickItemCancel: (AnimeExtension) -> Unit,
|
||||||
onInstallExtension: (AnimeExtension.Available) -> Unit,
|
onInstallExtension: (AnimeExtension.Available) -> Unit,
|
||||||
|
@ -63,21 +66,22 @@ fun AnimeExtensionScreen(
|
||||||
onOpenExtension: (AnimeExtension.Installed) -> Unit,
|
onOpenExtension: (AnimeExtension.Installed) -> Unit,
|
||||||
onClickUpdateAll: () -> Unit,
|
onClickUpdateAll: () -> Unit,
|
||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
onLaunched: () -> Unit,
|
|
||||||
) {
|
) {
|
||||||
val state by presenter.state.collectAsState()
|
|
||||||
val isRefreshing = presenter.isRefreshing
|
|
||||||
|
|
||||||
SwipeRefresh(
|
SwipeRefresh(
|
||||||
modifier = Modifier.nestedScroll(nestedScrollInterop),
|
refreshing = presenter.isRefreshing,
|
||||||
state = rememberSwipeRefreshState(isRefreshing),
|
|
||||||
indicator = { s, trigger -> SwipeRefreshIndicator(s, trigger) },
|
|
||||||
onRefresh = onRefresh,
|
onRefresh = onRefresh,
|
||||||
|
enabled = !presenter.isLoading,
|
||||||
) {
|
) {
|
||||||
when (state) {
|
when {
|
||||||
is ExtensionState.Initialized -> {
|
presenter.isLoading -> LoadingScreen()
|
||||||
ExtensionContent(
|
presenter.isEmpty -> EmptyScreen(
|
||||||
items = (state as ExtensionState.Initialized).list,
|
textResource = R.string.empty_screen,
|
||||||
|
modifier = Modifier.padding(contentPadding),
|
||||||
|
)
|
||||||
|
else -> {
|
||||||
|
AnimeExtensionContent(
|
||||||
|
state = presenter,
|
||||||
|
contentPadding = contentPadding,
|
||||||
onLongClickItem = onLongClickItem,
|
onLongClickItem = onLongClickItem,
|
||||||
onClickItemCancel = onClickItemCancel,
|
onClickItemCancel = onClickItemCancel,
|
||||||
onInstallExtension = onInstallExtension,
|
onInstallExtension = onInstallExtension,
|
||||||
|
@ -86,17 +90,16 @@ fun AnimeExtensionScreen(
|
||||||
onTrustExtension = onTrustExtension,
|
onTrustExtension = onTrustExtension,
|
||||||
onOpenExtension = onOpenExtension,
|
onOpenExtension = onOpenExtension,
|
||||||
onClickUpdateAll = onClickUpdateAll,
|
onClickUpdateAll = onClickUpdateAll,
|
||||||
onLaunched = onLaunched,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ExtensionState.Uninitialized -> {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionContent(
|
private fun AnimeExtensionContent(
|
||||||
items: List<ExtensionUiModel>,
|
state: AnimeExtensionsState,
|
||||||
|
contentPadding: PaddingValues,
|
||||||
onLongClickItem: (AnimeExtension) -> Unit,
|
onLongClickItem: (AnimeExtension) -> Unit,
|
||||||
onClickItemCancel: (AnimeExtension) -> Unit,
|
onClickItemCancel: (AnimeExtension) -> Unit,
|
||||||
onInstallExtension: (AnimeExtension.Available) -> Unit,
|
onInstallExtension: (AnimeExtension.Available) -> Unit,
|
||||||
|
@ -105,31 +108,29 @@ fun ExtensionContent(
|
||||||
onTrustExtension: (AnimeExtension.Untrusted) -> Unit,
|
onTrustExtension: (AnimeExtension.Untrusted) -> Unit,
|
||||||
onOpenExtension: (AnimeExtension.Installed) -> Unit,
|
onOpenExtension: (AnimeExtension.Installed) -> Unit,
|
||||||
onClickUpdateAll: () -> Unit,
|
onClickUpdateAll: () -> Unit,
|
||||||
onLaunched: () -> Unit,
|
|
||||||
) {
|
) {
|
||||||
var trustState by remember { mutableStateOf<AnimeExtension.Untrusted?>(null) }
|
var trustState by remember { mutableStateOf<AnimeExtension.Untrusted?>(null) }
|
||||||
|
|
||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
contentPadding = WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
|
contentPadding = contentPadding + topPaddingValues,
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = items,
|
items = state.items,
|
||||||
key = {
|
|
||||||
when (it) {
|
|
||||||
is ExtensionUiModel.Header.Resource -> it.textRes
|
|
||||||
is ExtensionUiModel.Header.Text -> it.text
|
|
||||||
is ExtensionUiModel.Item -> it.key()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
contentType = {
|
contentType = {
|
||||||
when (it) {
|
when (it) {
|
||||||
is ExtensionUiModel.Item -> "item"
|
is AnimeExtensionUiModel.Header -> "header"
|
||||||
else -> "header"
|
is AnimeExtensionUiModel.Item -> "item"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
key = {
|
||||||
|
when (it) {
|
||||||
|
is AnimeExtensionUiModel.Header -> "animeextensionHeader-${it.hashCode()}"
|
||||||
|
is AnimeExtensionUiModel.Item -> "animeextension-${it.hashCode()}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { item ->
|
) { item ->
|
||||||
when (item) {
|
when (item) {
|
||||||
is ExtensionUiModel.Header.Resource -> {
|
is AnimeExtensionUiModel.Header.Resource -> {
|
||||||
val action: @Composable RowScope.() -> Unit =
|
val action: @Composable RowScope.() -> Unit =
|
||||||
if (item.textRes == R.string.ext_updates_pending) {
|
if (item.textRes == R.string.ext_updates_pending) {
|
||||||
{
|
{
|
||||||
|
@ -151,26 +152,20 @@ fun ExtensionContent(
|
||||||
action = action,
|
action = action,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is ExtensionUiModel.Header.Text -> {
|
is AnimeExtensionUiModel.Header.Text -> {
|
||||||
ExtensionHeader(
|
ExtensionHeader(
|
||||||
text = item.text,
|
text = item.text,
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is ExtensionUiModel.Item -> {
|
is AnimeExtensionUiModel.Item -> {
|
||||||
AnimeExtensionItem(
|
AnimeExtensionItem(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
item = item,
|
item = item,
|
||||||
onClickItem = {
|
onClickItem = {
|
||||||
when (it) {
|
when (it) {
|
||||||
is AnimeExtension.Available -> onInstallExtension(it)
|
is AnimeExtension.Available -> onInstallExtension(it)
|
||||||
is AnimeExtension.Installed -> {
|
is AnimeExtension.Installed -> onOpenExtension(it)
|
||||||
if (it.hasUpdate) {
|
|
||||||
onUpdateExtension(it)
|
|
||||||
} else {
|
|
||||||
onOpenExtension(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is AnimeExtension.Untrusted -> { trustState = it }
|
is AnimeExtension.Untrusted -> { trustState = it }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -190,9 +185,6 @@ fun ExtensionContent(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
onLaunched()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -215,9 +207,9 @@ fun ExtensionContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AnimeExtensionItem(
|
private fun AnimeExtensionItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
item: ExtensionUiModel.Item,
|
item: AnimeExtensionUiModel.Item,
|
||||||
onClickItem: (AnimeExtension) -> Unit,
|
onClickItem: (AnimeExtension) -> Unit,
|
||||||
onLongClickItem: (AnimeExtension) -> Unit,
|
onLongClickItem: (AnimeExtension) -> Unit,
|
||||||
onClickItemCancel: (AnimeExtension) -> Unit,
|
onClickItemCancel: (AnimeExtension) -> Unit,
|
||||||
|
@ -233,10 +225,30 @@ fun AnimeExtensionItem(
|
||||||
onClickItem = { onClickItem(extension) },
|
onClickItem = { onClickItem(extension) },
|
||||||
onLongClickItem = { onLongClickItem(extension) },
|
onLongClickItem = { onLongClickItem(extension) },
|
||||||
icon = {
|
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 = {
|
action = {
|
||||||
ExtensionItemActions(
|
AnimeExtensionItemActions(
|
||||||
extension = extension,
|
extension = extension,
|
||||||
installStep = installStep,
|
installStep = installStep,
|
||||||
onClickItemCancel = onClickItemCancel,
|
onClickItemCancel = onClickItemCancel,
|
||||||
|
@ -244,29 +256,20 @@ fun AnimeExtensionItem(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
ExtensionItemContent(
|
AnimeExtensionItemContent(
|
||||||
extension = extension,
|
extension = extension,
|
||||||
|
installStep = installStep,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionItemContent(
|
private fun AnimeExtensionItemContent(
|
||||||
extension: AnimeExtension,
|
extension: AnimeExtension,
|
||||||
|
installStep: InstallStep,
|
||||||
modifier: Modifier = Modifier,
|
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(
|
Column(
|
||||||
modifier = modifier.padding(start = horizontalPadding),
|
modifier = modifier.padding(start = horizontalPadding),
|
||||||
) {
|
) {
|
||||||
|
@ -276,56 +279,72 @@ fun ExtensionItemContent(
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
Row(
|
// Won't look good but it's not like we can ellipsize overflowing content
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
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(
|
||||||
text = LocaleHelper.getSourceDisplayName(extension.lang, context),
|
text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extension.versionName.isNotEmpty()) {
|
if (extension.versionName.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = extension.versionName,
|
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) {
|
if (warning != null) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = warning).uppercase(),
|
text = stringResource(warning).uppercase(),
|
||||||
style = MaterialTheme.typography.bodySmall.copy(
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
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
|
@Composable
|
||||||
fun ExtensionItemActions(
|
private fun AnimeExtensionItemActions(
|
||||||
extension: AnimeExtension,
|
extension: AnimeExtension,
|
||||||
installStep: InstallStep,
|
installStep: InstallStep,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onClickItemCancel: (AnimeExtension) -> Unit = {},
|
onClickItemCancel: (AnimeExtension) -> Unit = {},
|
||||||
onClickItemAction: (AnimeExtension) -> Unit = {},
|
onClickItemAction: (AnimeExtension) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val isIdle = remember(installStep) {
|
val isIdle = installStep.isCompleted()
|
||||||
installStep == InstallStep.Idle || installStep == InstallStep.Error
|
|
||||||
}
|
|
||||||
Row(modifier = modifier) {
|
Row(modifier = modifier) {
|
||||||
|
if (isIdle) {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { onClickItemAction(extension) },
|
onClick = { onClickItemAction(extension) },
|
||||||
enabled = isIdle,
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = when (installStep) {
|
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.Installed -> stringResource(R.string.ext_installed)
|
||||||
InstallStep.Error -> stringResource(R.string.action_retry)
|
InstallStep.Error -> stringResource(R.string.action_retry)
|
||||||
InstallStep.Idle -> {
|
InstallStep.Idle -> {
|
||||||
|
@ -341,18 +360,15 @@ fun ExtensionItemActions(
|
||||||
is AnimeExtension.Available -> stringResource(R.string.ext_install)
|
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) }) {
|
IconButton(onClick = { onClickItemCancel(extension) }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Close,
|
imageVector = Icons.Outlined.Close,
|
||||||
contentDescription = "",
|
contentDescription = stringResource(R.string.action_cancel),
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.PushPin
|
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.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
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.AnimeSource
|
||||||
import eu.kanade.domain.animesource.model.Pin
|
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.EmptyScreen
|
||||||
import eu.kanade.presentation.components.LoadingScreen
|
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.R
|
||||||
import eu.kanade.tachiyomi.animesource.LocalAnimeSource
|
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.ui.browse.animesource.AnimeSourcesPresenter
|
||||||
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AnimeSourcesScreen(
|
fun AnimeSourcesScreen(
|
||||||
nestedScrollInterop: NestedScrollConnection,
|
|
||||||
presenter: AnimeSourcesPresenter,
|
presenter: AnimeSourcesPresenter,
|
||||||
onClickItem: (AnimeSource) -> Unit,
|
contentPadding: PaddingValues,
|
||||||
|
onClickItem: (AnimeSource, String) -> Unit,
|
||||||
onClickDisable: (AnimeSource) -> Unit,
|
onClickDisable: (AnimeSource) -> Unit,
|
||||||
onClickLatest: (AnimeSource) -> Unit,
|
|
||||||
onClickPin: (AnimeSource) -> Unit,
|
onClickPin: (AnimeSource) -> Unit,
|
||||||
) {
|
) {
|
||||||
val state by presenter.state.collectAsState()
|
val context = LocalContext.current
|
||||||
|
when {
|
||||||
when (state) {
|
presenter.isLoading -> LoadingScreen()
|
||||||
is AnimeSourceState.Loading -> LoadingScreen()
|
presenter.isEmpty -> EmptyScreen(
|
||||||
is AnimeSourceState.Error -> Text(text = (state as AnimeSourceState.Error).error.message!!)
|
textResource = R.string.source_empty_screen,
|
||||||
is AnimeSourceState.Success -> AnimeSourceList(
|
modifier = Modifier.padding(contentPadding),
|
||||||
nestedScrollConnection = nestedScrollInterop,
|
)
|
||||||
list = (state as AnimeSourceState.Success).uiModels,
|
else -> {
|
||||||
|
AnimeSourceList(
|
||||||
|
state = presenter,
|
||||||
|
contentPadding = contentPadding,
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
onClickDisable = onClickDisable,
|
onClickDisable = onClickDisable,
|
||||||
onClickLatest = onClickLatest,
|
|
||||||
onClickPin = onClickPin,
|
onClickPin = onClickPin,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
presenter.events.collectLatest { event ->
|
||||||
|
when (event) {
|
||||||
|
AnimeSourcesPresenter.Event.FailedFetchingSources -> {
|
||||||
|
context.toast(R.string.internal_error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AnimeSourceList(
|
private fun AnimeSourceList(
|
||||||
nestedScrollConnection: NestedScrollConnection,
|
state: AnimeSourcesState,
|
||||||
list: List<AnimeSourceUiModel>,
|
contentPadding: PaddingValues,
|
||||||
onClickItem: (AnimeSource) -> Unit,
|
onClickItem: (AnimeSource, String) -> Unit,
|
||||||
onClickDisable: (AnimeSource) -> Unit,
|
onClickDisable: (AnimeSource) -> Unit,
|
||||||
onClickLatest: (AnimeSource) -> Unit,
|
|
||||||
onClickPin: (AnimeSource) -> Unit,
|
onClickPin: (AnimeSource) -> Unit,
|
||||||
) {
|
) {
|
||||||
if (list.isEmpty()) {
|
ScrollbarLazyColumn(
|
||||||
EmptyScreen(textResource = R.string.source_empty_screen)
|
contentPadding = contentPadding + topPaddingValues,
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val (sourceState, setSourceState) = remember { mutableStateOf<AnimeSource?>(null) }
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier
|
|
||||||
.nestedScroll(nestedScrollConnection),
|
|
||||||
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
|
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = list,
|
items = state.items,
|
||||||
contentType = {
|
contentType = {
|
||||||
when (it) {
|
when (it) {
|
||||||
is AnimeSourceUiModel.Header -> "header"
|
is AnimeSourceUiModel.Header -> "header"
|
||||||
|
@ -99,13 +98,13 @@ fun AnimeSourceList(
|
||||||
key = {
|
key = {
|
||||||
when (it) {
|
when (it) {
|
||||||
is AnimeSourceUiModel.Header -> it.hashCode()
|
is AnimeSourceUiModel.Header -> it.hashCode()
|
||||||
is AnimeSourceUiModel.Item -> it.source.key()
|
is AnimeSourceUiModel.Item -> "source-${it.source.key()}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { model ->
|
) { model ->
|
||||||
when (model) {
|
when (model) {
|
||||||
is AnimeSourceUiModel.Header -> {
|
is AnimeSourceUiModel.Header -> {
|
||||||
SourceHeader(
|
AnimeSourceHeader(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
language = model.language,
|
language = model.language,
|
||||||
)
|
)
|
||||||
|
@ -114,49 +113,60 @@ fun AnimeSourceList(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
source = model.source,
|
source = model.source,
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
onLongClickItem = {
|
onLongClickItem = { state.dialog = AnimeSourcesPresenter.Dialog(it) },
|
||||||
setSourceState(it)
|
|
||||||
},
|
|
||||||
onClickLatest = onClickLatest,
|
|
||||||
onClickPin = onClickPin,
|
onClickPin = onClickPin,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sourceState != null) {
|
if (state.dialog != null) {
|
||||||
|
val source = state.dialog!!.source
|
||||||
AnimeSourceOptionsDialog(
|
AnimeSourceOptionsDialog(
|
||||||
source = sourceState,
|
source = source,
|
||||||
onClickPin = {
|
onClickPin = {
|
||||||
onClickPin(sourceState)
|
onClickPin(source)
|
||||||
setSourceState(null)
|
state.dialog = null
|
||||||
},
|
},
|
||||||
onClickDisable = {
|
onClickDisable = {
|
||||||
onClickDisable(sourceState)
|
onClickDisable(source)
|
||||||
setSourceState(null)
|
state.dialog = null
|
||||||
},
|
},
|
||||||
onDismiss = { setSourceState(null) },
|
onDismiss = { state.dialog = null },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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,
|
modifier: Modifier = Modifier,
|
||||||
source: AnimeSource,
|
source: AnimeSource,
|
||||||
onClickItem: (AnimeSource) -> Unit,
|
onClickItem: (AnimeSource, String) -> Unit,
|
||||||
onLongClickItem: (AnimeSource) -> Unit,
|
onLongClickItem: (AnimeSource) -> Unit,
|
||||||
onClickLatest: (AnimeSource) -> Unit,
|
|
||||||
onClickPin: (AnimeSource) -> Unit,
|
onClickPin: (AnimeSource) -> Unit,
|
||||||
) {
|
) {
|
||||||
BaseAnimeSourceItem(
|
BaseAnimeSourceItem(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
source = source,
|
source = source,
|
||||||
onClickItem = { onClickItem(source) },
|
onClickItem = { onClickItem(source, GetRemoteAnime.QUERY_POPULAR) },
|
||||||
onLongClickItem = { onLongClickItem(source) },
|
onLongClickItem = { onLongClickItem(source) },
|
||||||
action = { source ->
|
action = {
|
||||||
if (source.supportsLatest) {
|
if (source.supportsLatest) {
|
||||||
TextButton(onClick = { onClickLatest(source) }) {
|
TextButton(onClick = { onClickItem(source, GetRemoteAnime.QUERY_LATEST) }) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.latest),
|
text = stringResource(id = R.string.latest),
|
||||||
style = LocalTextStyle.current.copy(
|
style = LocalTextStyle.current.copy(
|
||||||
|
@ -174,46 +184,24 @@ fun AnimeSourceItem(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AnimeSourceIcon(
|
private fun AnimeSourcePinButton(
|
||||||
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(
|
|
||||||
isPinned: Boolean,
|
isPinned: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val icon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin
|
val icon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin
|
||||||
val tint = if (isPinned) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground
|
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) {
|
IconButton(onClick = onClick) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = "",
|
|
||||||
tint = tint,
|
tint = tint,
|
||||||
|
contentDescription = stringResource(description),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AnimeSourceOptionsDialog(
|
private fun AnimeSourceOptionsDialog(
|
||||||
source: AnimeSource,
|
source: AnimeSource,
|
||||||
onClickPin: () -> Unit,
|
onClickPin: () -> Unit,
|
||||||
onClickDisable: () -> Unit,
|
onClickDisable: () -> Unit,
|
||||||
|
@ -221,7 +209,7 @@ fun AnimeSourceOptionsDialog(
|
||||||
) {
|
) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
title = {
|
title = {
|
||||||
Text(text = source.nameWithLanguage)
|
Text(text = source.visualName)
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column {
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue